Technische Aspekte


... [ Einführung in die funktionale Programmiersprache Haskell ] ... [Thema Why functional programming matters ] ... [ Abschließende Betrachtung ] ...

Übersicht: Einleitung

 

 

Besondere Eigenschaften funktionaler Sprachen am Beispiel von Haskell

Haskell gehört zu der Klasse der nicht-strikten funktionalen Programmiersprachen. Dies bedeutet das die Argumente einer Funktion nur bei Bedarf ausgewertet werden, was unter anderem. Geschwindigkeitsvorteile ermöglicht. Spricht man allgemein von Vorteilen einer Programmiersprache, dann ist dies stets auch in Verbindung mit der gestellten Aufgabe zu sehen. Funktionale Sprachen sind von ihrer Platzierung her sehr weit weg von der Maschine auf der sie eingesetzt werden einzuordnen. Es steht vornehmlich das "Was" und nicht das "Wie" im Vordergrund einer Berechnung. Dies kann für bestimmte Probleme sehr vorteilhaft sein, da durch die hohe Ausdruckskraft der funktionalen Sprachen das Problem als solches nicht selten in schlankerer Art und Weise dargestellt werden kann als es vielleicht mit einer prozedurale Beschreibung möglich wäre.
Ich möchte daher nicht so sehr von Vorteilen und Nachteilen sondern eher von folgenden Eigenschaften sprechen:
  • Hohe Ausdruckskraft
  • Keine unerlaubten Speicherzugriffe oder fehlerhafte Freigaben da eine automatische Speicherverwaltung und Bereinigung stattfindet.
  • Oftmals leichter zu verstehender Programmcode nachdem die Notation erlernt wurde
  • Keine Seiteneffekte (Verringerung der Möglichkeiten schwer auffindbare Fehler zu produzieren)
  • Bedingte Auswertung - nur die Programmteile die zum Erreichen des Ergebnisses nötig sind werden auch durchlaufen (Lazy Evaluation)
  • Typpolymorphie steigert die Wiederverwendbarkeit von Programmteilen (Ein Algorithmus arbeitet auf unterschiedlichen Datentypen)
  • Unendliche Datenstrukturen sind definierbar
  • Integration von Programmteilen die in anderen Sprachen geschrieben und kompiliert wurden ist unter Haskell möglich.
Funktionale Sprachen werden vornehmlich in der Lehre und Ausbildung von Informatikern eingesetzt, da sie sich hervorragend dazu eignen auch komplexere Problemstellungen ausdrucksstark und übersichtlich abzubilden. Der funktionale Ansatz zwingt den Anwender "anders" zu denken als er es vielleicht bisher tat.
Dennoch und vielleicht auch gerade deswegen lassen sich aus der Entwicklung funktional basierter Programme viele Überlegungen gewinnen, die ebenso das prozedurale Implementieren und Denken verbessern können.

Möglichkeiten der Programmaufteilung am Beispiel von Haskell und Pascal

Um komplexe Anwendungen erstellen zu können ist einige wichtige Vorraussetzung von Seiten der verwendeten Programmiersprache, Modularisierungskonzepte bereitszustellen, die es ermöglichen "Information-hiding" als auch unabhängiges Arbeiten mehrerer Entwickler durch Funktionstrennung über Modulkonzepte zu ermöglichen In Pascal wird dies über das Unitkonzept realisiert. Haskell bietet ebenfalls ein Modulkonzept an. Besonders hierbei ist, das nicht nur das Modul selbst vorgibt welche Funktionen und Datentypen es nach aussen hin bekannt gibt, sondern ebenso beim Import Einschränkungen vorgenommen werden können. Jedoch kann nicht mehr importiert werden als das Modul grundsätzlich zum Export freigegeben hat.
 
 
Beispiel des Modulkonzeptes in Haskell Beispiel des Modulkonzeptes in Pascal
Definition eines Moduls : 
-- alle Funktionen und Datentypen des Moduls exportieren 
module Tree where ... 

-- nur einige Funktionen und Datentypen exportieren

-- Mit dem Datentyp Tree werden noch zusätzlich seine 
-- Daten-Konstruktoren Leaf und Branch exportiert
module Tree (Tree (Leaf,Branch),fringe) where 
data Tree a = Leaf a | Branch Tree a Tree a  fringe = ...
Verwendung des Moduls durch andere Programmteile:
-- nur einige Funktionen und Datentypen importieren
module Main (main) where 
import Tree (Tree(Leaf,Branch),fringe) 
main = ... 

-- alle Funktionen und Datentypen importieren 
module Main (main) where 
import Tree 
main = ...
Definition eines Moduls (Unit):

UNIT Unitname; 
   INTERFACE 

   TYPE TestTyp= ... 
   PROCEDURE X(..); 
   FUNCTION  Y(..):..; 
   VAR  globaleVariable:STRING; 

   IMPLEMENTATION 
   ... 
   END. 

Verwendung des Moduls (Unit) durch andere 
Programmteile: 

PROGRAM Testprogramm;
USES Unitname;
...



Die Bedeutung strikter Synchronisation und die von "Alleskleber"

In der funktionalen Programmierung wird häufig der Begriff des "glueing" oder um es direkt zu übersetzen des "verklebens" verwendet.
Eine Art des "Verklebens" von Funktionen, so wie es in dem Skriptum von John Hughes "Why functional programming matters." beschrieben wird ist die Definition von Funktionen höherer Ordnung. In der funktionalen Programmierung sind dies Funktionen die wiederrum Funktionen als Argument akzeptieren oder selbst Funktionen als Ergebnis zurückliefern. Ein aus dem genannten Skript entnommenes Beispiel in der Sprache Miranda stellt den Vorgang anhand einer Listenoperation dar.

Die erste Form des "Verklebens" - Das "Verkleben" von Funktionen

Es sei folgende allgemeine Listendefinition gegeben:
listof X ::= nil | cons X (listof) X

Die Summe aller Elemente der Liste sei folgendermaßen definiert:
sum nil = 0
sum (cons num list) = num + sum list

Aus der rekursiven Definition der Summenbildung lässt sich ableiten das es sich hierbei zum einen um einen generellen rekursiv arbeiten Algorithmus der alle Elemente einer Liste bearbeitet und eine Funktion zur Ermittlung einer Summe handelt. Aus dieser Erkenntnis heraus lässt sich das Problem weiter modularisieren.

Der rekursive Teil wird in diesem Fall konventionell als "reduce" bezeichnet
sum = reduce add 0

Wobei Add die Funktion zur Summenbildung darstellt:
add x y = x + y

Die Definition von reduce sieht folgendermaßen aus:
(reduce f x) nil = x
(reduce f x) (cons a l) = f a ((reduce f x) l)

Auf Basis des rekursiven Algorithmus lassen sich nun auch ohne weitere Programmierung andere Operationen auf Listen durchführen:

Die zweite Form des "Verklebens" - Das "Verkleben" von Programmen

Eine weitere Form innerhalb der funktionalen Programmierung Dinge miteinander zu Verbinden ist bezieht sich auf die Verbindung von Programmen.

Ein Beispiel:

Wenn g und f Programme sind, dann ist (g . f) ein Programm bei dem f die Eingabedaten für g generiert.
    g (f input)

Hierbei spielt ein Mechanismus eine wichtige Rolle der als "strikte Synchronisation" bezeichnet wird. Bei der "strikten Synchronisation" muß sich der Programmierer nicht darum kümmern wie die Daten die die Funktion f produziert zwischengespeichert werden müssen um von g ausgewertet werden zu können. Funktion f stellt stets nur soviele Daten zur Verfügung wie g anfordert. Die beiden Programme laufen somit in "strikter Synchronisation". f kann zudem auch ein Programm sein das eine unendliche Menge an Daten produziert. Sobald g beendet wird, wird auch f beendet. Diese Art der Auswertung sorgt also dafür das das Programm f nur so lange läuft wie unbedingt erforderlich. Sie wird auch als "Lazy Evaluation" bezeichnet und stellt einen sehr wichtigen Baustein für die Modularisierung von Programmen innerhalb der funktionalen Programmierung dar.


Bedingte Auswertung am Beispiel eines einfachen Spielbaumes

Anhand eines einfachen Beispieles soll die Leistungsfähigkeit der Lazy Evaluation und Funktionen höherer Ordnung dargestellt werden.
Grundlage des Beispieles bildet ein Algorithmus zur Spielzugauswahl. Er wurde dem Skriptum "Why function programming matters" von John Hughes entnommen.

Der folgende Algorithmus wird auch als alpha-beta Algorithmus bezeichnet und dient der Abschätzung wie sich ein Spiel möglicherweise entwickeln könnte.
Aus dieser Information können dann optimale Spielzüge ermittelt werden. Zur Veranschaulichung wird hier das Spiel Tic Tac Toe verwendet.

Die gedankliche Implementierung:

Die Spielpositionen werden als Object vom Typ Position abgebildet.
Eine Funktion moves soll die von einer Position aus möglichen weiteren Spielschritte ermitteln, die für einen einzelnen Spielzug möglich sind.

    moves: position -> listof position

Bei dem hier als Beispiel verwendeten Spielprinzip ist es stets aufgrund des abwechselnden Setzen von Steinen stets möglich zu Ermitteln welcher Spieler als nächster an der Reihe ist, daher ist es nicht erforderlich diese Information in irgendeiner Form separat zu speichern.

Als nächstes soll eine Baumstruktur abgebildet werden, die alle von einer Position ausgehenden möglichen Spielzüge speichern kann.
Die Baumstruktur kann durch eine wiederholte Anwendung der Function moves aufgebaut werden. Begonnen wird hierbei mit der in der Wurzel des Baumes gespeicherten Position.

Die allgemeine Definition der Rekursion zur Baumerstellung kann folgendermaßen ausgedrückt werden:

    reptree f a = node a (map (reptree f) (f a))

Um nun von einer beliebigen Position aus einen kompletten Spielzugbaum zu erstellen wird die Funktion moves eingesetzt.

    gametree p = reptree moves p
Desweiteren wird ein Funktion benötigt die in der Lage ist zu Ermitteln ob eine bestimmte Folgeposition für den Spieler der an der Reihe ist vorteilhaft ist oder nicht.
Gehen wir davon aus das eine solche Funktion mit dem Namen static gegeben sei und folgendermaßen definiert ist:

    static: position -> number
Für unseren einfachen Fall könnte diese Funktion eine 1 für eine Position zurückliefern bei denen der Computer bereits gewonnen, eine -1 für das Verlieren und andernfalls eine 0. Nachdem ein Spielzugbaum generiert worden ist, kann dieser mit Hilfe der Funktion maptree staticin einen Baum von Zahlen umgewandelt werden, über den sich dann der nächste vorteilhafte Spielzug ermitteln lässt. Maptree sei eine als gegeben angenommene Funktion die die einzelnen Elemente eines Baumes durchläuft und auf sie eine übergebene Funktion wie zum Beispiel static anwendet. Die grundsätzliche Tatsache das Spielbäume auch von unendlicher Länge sein können wird noch im Folgenden betrachtet. Bei einem Spiel wie Tic Tac Toe sind die Spielzugbäume endlich, jedoch bei Spielen wie zum Beispiel Schach sind unendliche Spielpartien prinzipiell möglich.

Um nun einen passenden Spielzug auswählen zu können werden noch Funktionen benötigt die Ermitteln welcher nächster Spielzug denn nun tatsächlich der Beste ist. Ausgehend von den möglichen Werten -1 (Computer verloren) , 0 , +1 (Computer gewonnen) wird für die Spielzugwahl des Computers eine Funktion benötigt die
die größte Zahl aus einer Liste von Spielzugbewertungen heraussucht. Der Gegenspieler würde eine Funktion einsetzen die das Minimum ermittelt.
Es wird also der Spielverlauf gesucht der zu einem Sieg führt hierbei werden alle möglichen Unterpositionen der aktuellen Spielposition untersucht.
Führt eine Position zu einem Sieg des Computers (maximise), dann wird diese als nächster Spielzug ausgewählt.

Diese minimise und maximise Funktionen seien folgendermaßen definiert:

    maximise (node n nil) = n

    maximise (node n sub) = max (map minimize sub)
    minimize (node n nil) = n
    minimize (node n sub) = min (map maximize sub)

Zum jetzigen Zeitpunkt wäre es bereits möglich eine Funktion zu definieren die einen passenden Spielzug auswählt. Sie könnte folgendes Aussehen haben:

    evaluate =  maximise . maptree static . gametree
Es gibt letzendlich noch ein Problem in bezug auf sehr umfangreiche oder sogar unendliche Baumstrukturen. Die vorstehende Definition würde dann entweder erst nach sehr langer Zeit oder gar nicht zu einem Ergebnis kommen. Daher kommt eine weitere Funktion zum Einsatz die es ermöglicht nach einer bestimmten Tiefe die Rekursion abzubrechen. Dies ist nur aufgrund des Lazy Evaluation Mechanismus so einfach möglich wie es in Folge beschrieben ist.
Die erforderliche Funktion sei prune(engl. kürzen, abschneiden).

    prune 0 (node a x) = node a nil
    prune n (node a x) = node a (map (prune (n-1)) x)
Die verbesserte Auswertungsfunktion wird nun folgendemaßen geschrieben:

    evaluate = maximise . maptree static . prune 5 . gametree
Sie betrachtet maximal noch 5 weitere Spielzüge, bevor sie ein Ergebnis liefert.
 

Das beschriebene Beispiel verdeutlicht die Leistungsfähigkeit der Lazy Evaluation und den Einsatz vonFunktionen Höherer Ordnung als Hilfmittel der Modularisierung und Optimierung von Programmen.


Die Integration fremsprachiger Bibliotheken in Haskell

Um in Haskell auf fremdsprachige Bibliotheken und Objekte zugreifen zu können, oder auch eigene Entwicklungen in Haskell für die Verwendung in mehrsprachigen Umgebungen bereitszustellen existiert ein komplettes Entwicklungspaket mit der Bezeichnung "Haskell-Skript". Mit Hilfe dieses Paketes ist es möglich ActiveX, COM und Automations Komponenten zu starten, zu benutzen und selber zu schreiben.

Das Paket besteht derzeit aus folgenden Komponenten:

 
Weitere Informationen zu dem angebotenen Funktionsumfang der einzelnen Module sind auf der folgenden Internet-Seite zu finden!
http://www.haskell.org/haskellscript/index.html

Welche Entwicklungsumgebungen stehen dem Anwender zur Verfügung

Für die Entwicklung von Programmen stehen dem Anwender abhängig von der verwendeten funktionalen Sprache eine Reihe an Compilern, Interpretern als auch unterstützenden Hilfsprogrammen wie Profilern zur Verfügung. Viele der verfügbaren Versionen stehen im Internet frei zum Download bereit und
die zugrundeliegenden Quellen werden ebenfalls angeboten.

Die folgende unvollständige Liste soll ein paar der verfügbaren Entwicklungshilfen auflisten:

Für Haskell:
Der Haskell Interpreter (HUGS)
Ein kleiner portabler in C geschriebener Haskell Interpreter der auf fast allen Rechnern lauffähig ist.
Glasgow Haskell Compiler (GHC)
Ein selbst in Haskell geschriebener Compiler, der eine volle Implementierung von Haskell beeinhaltet und dessen Quellcode frei zur Verfügung steht.
Glasgow Parallel Haskell (GPH)
Eine Implementierung zur Erweiterung der Spracheiegenschaften von Haskell um die Realisierung von Parallelität zu ermöglichen.
nhc98
Ein Haskell 98 Compiler der von Niklas Röjemo entwickelt wurde.
HBI und HBC (Chalmer's Haskell Interpreter und Compiler)
Ein von Lennart Augustsson entwickelter Haskell Interpreter / Compiler, der Haskell 1.4 voll implementiert.

Für Standard ML:
Standard ML of New Jersey
Implementiert ab Version 110 Standard ML '97
Moscow ML
Ein Compiler der Standard ML implementiert
ML Kit
Ein Entwicklungskit das Standard ML '97 implementiert.
MLton
Ein Compiler für Standard ML ' 97
PolyML
Ein Compiler der ab Version 4.0 Standard ML '97 implementiert.
 


... [ Die funktionale Programmiersprache Haskell ]  ... [ Thema Why functional ... ] ... [ Technische Aspekte ] ... [ Abschließende Betrachtung ] ...