II Grundlagen funktionaler Sprachen

Werbung
Inhaltsverzeichnis
I Einleitung ........................................................................................................................................ 2
II Grundlagen funktionaler Sprachen ............................................................................................... 3
1 Einordnung in das Sprachspektrum .......................................................................................... 3
2 Das Lambda-Kalkül .................................................................................................................... 3
3 Das Programm als Menge von Funktionen................................................................................ 4
4 Datenstrukturen ......................................................................................................................... 5
III Merkmale Funktionaler Sprachen ................................................................................................ 7
1 Referentielle Transparenz ......................................................................................................... 7
2 Programmverifikation ................................................................................................................ 8
3 Ein-/ Ausgabe ............................................................................................................................ 9
4 Auswertung .............................................................................................................................. 10
5 Das Hindley-Milner-Typsystem ................................................................................................ 11
IV Fazit ............................................................................................................................................ 12
Quellenverzeichnis ......................................................................................................................... 14
Literatur....................................................................................................................................... 14
Internet ........................................................................................................................................ 14
1
I Einleitung
"Zeit ist Geld" trivial wie wahr. Vor allem in der Softwareentwicklung entstehen hohe Kosten auf
Grund von langen Entwicklungszeiten. In diesem Zusammenhang bekommt das so genannte "rapid
prototyping" eine bedeutende Rolle. Auf diesem Gebiet haben funktionale Sprachen gegenüber
imperativen Sprachen einen enormen Vorteil. Auf Grund der hohen Abstraktion ist es möglich kurze
und dadurch übersichtliche und meist auch fehlerfreie Quelltexte zu entwerfen. Funktionale
Programme orientieren sich fast ausschließlich an der mathematischen Lösung des Problems, so dass
sich der Programmierer nicht mit der Speicherplatzverwaltung beschäftigen muss, wie es
beispielsweise bei C++ der Fall ist.
Ein weitere Vorteil funktionaler Sprachen ist die Möglichkeit Programme zu Verifizieren, d.h. einen
mathematischen Beweis für die Semantik zu führen.
Trotz dieser Vorteile sind funktionale Sprachen im Gegensatz zu den modernen objektorientierten
Sprachen nur wenig verbreitet. In dieser Seminararbeit soll daher eine Einführung in die Prinzipien
funktionaler Sprachen vorgenommen werden. Diese Arbeit hat nicht das Ziel, dem Leser das
funktionale Programmieren beizubringen, sondern versucht den Leser in die Lage zu versetzen, eine
fundierte Antwort auf die Frage, was eine Funktionale Sprache ist zu geben.
Beispielhaft wird die funktionale Sprache Haskell näher betrachtet.
Es werden keine Kenntnisse in Funktionaler Programmierung vorausgesetzt, wobei Grundbegriffe der
Informatik und Mathematik bekannt sein sollten.
In dem ersten Teil der Arbeit sollen dem Leser die wichtigsten Grundkenntnisse funktionaler
Programmierung und auch von Haskell vermittelt werden, damit man eine erste Vorstellung eines
funktionalen Programms bekommt. Diese Grundkenntnisse sind auch von Nöten, um sich von den
Themen, im zweiten Teil der Arbeit eine Vorstellung machen zu können. Wie bereits erwähnt hat
diese Seminararbeit eine einführenden Charakter. Die einzelnen Themen können daher nur kurz
dargestellt werden, für die Vertiefung wird auf weiterführende Literatur verwiesen.
An Literatur sollte "The craft of functional programminmg" von Thompson besonders erwähnt
werden. Thompson führt sehr behutsam in die Sprache Haskell ein und zeigt dabei die
charakteristischen Merkmale funktionaler Programmierung auf. Für diese Seminararbeit ist außerdem
"Grundlagen funktionaler Programmierung" von Martin Erwig, so wie das Skript, von Frau Prof. Dr.
Loogen, zur Vorlesung "Deklaratives Programmieren WS 03/04", der Universität Marburg von
Bedeutung. Natürlich muss darüber hinaus auch die Webseite www.haskell.org erwähnt werden,
welche sehr umfangreiche Informationen über Haskell gibt.
2
II Grundlagen funktionaler Sprachen
1 Einordnung in das Sprachspektrum
Das weite Spektrum der Programmiersprachen, lässt sich grob in zwei Kategorien aufteilen. Zum
einem gibt es die imperativen Sprachen und zum anderen die deklarativen Sprachen.
Die imperativen Sprachen werden nochmals in konventionelle Sprachen, wie Pascal oder Fortran und
in die populären Objektorientierten Sprachen wie, C++ oder Java aufgeteilt.
Die Basis imperativer Sprachen bildet das Prinzip des „von Neumann“, bei dem Programme aus
Folgen von Befehlen (imperare) bestehen, die konsequent nacheinander ausgeführt werden. Damit
werden Berechnungen als Abfolge lokaler Wertzuweisungen beschrieben. Die Hauptkontrollstruktur
ist dabei die While-Schleife. [vergl. Gumm 590]
Auch die deklarativen Sprachen werden nochmals aufgeteilt. Zum einem in die Logik Sprachen,
deren bekanntester Vertreter PROLOG ist und zum anderen in die funktionalen Sprachen wie ML,
oder die im folgenden beschriebene Sprache Haskell. Außerdem gab es Versuche, die funktionalen
Sprachen und Logik-Sprachen in eine Sprache zu integrieren. Zu diesen so genannten funktionallogischen Sprachen gehören Ideal oder Curry.
Die Merkmahle deklarativer Sprachen beruhen auf einer Rechnerunabhängigen Fundierung. Im
Unterschied zu den imperativen Sprachen, ist die Basis eine mathematische Theorie. Berechnungen
werden daher nicht als Abfolge von Wertzuweisungen, sondern als Manipulation von Werten
beschrieben. Da es keine Zuweisungen gibt, sind Kontrollstrukturen wie while, repeat, usw. sinnlos.
Die Hauptkontrollstruktur ist die Rekursion. [vergl. Gumm 591]
Wie schon in der Einleitung erwähnt soll im weiteren als Beispiel die funktionale Sprache Haskell
benutzt werden. Die Mathematische Theorie bildet hier das Lambda-Kalkül.
2 Das Lambda-Kalkül
Das Lambda-Kalkül ist die Basis funktionaler Programmiersprachen. Alonzo Churche und Stephen
Kleene entwickelten dieses Kalkül in den 30 Jahren zur Formalisierung des Funktionenbegriffs. Mit
dem Lambda-Kalkül definierten sie, was eine Funktion ist. Dabei stellten sie mit den Regeln zur
Manipulation von Werten gleichzeitig ein vollständiges System zur Beschreibung und Ausführung
mathematischer Berechnungen und Verfahren dar, welches im Sinne des Konzepts der
Berechenbarkeit ebenso mächtig ist wie die modernen Programmiersprachen.
Funktionale Programmiersprachen sind daher genau genommen nur syntaktische Verzierungen des
Lambda-Kalküls, welche den Umgang mit den Regeln vereinfachen, an der Mächtigkeit des Kalküls
jedoch nichts ändern. [vergl. Erwig 95]
Eine Betrachtung der Definition des Lambda-Kalkül würde an dieser Stelle, auf Grund des Umfangs
zu weit gehen und ist für das weitere Verständnis auch nicht nötig. Eine ausführliche Definition findet
man bei Martin Erwig.
3
3 Das Programm als Menge von Funktionen
„Die funktionale Programmierung bezieht einen Großteil ihrer Attraktivität aus der Tatsache, dass der
ihr zugrunde liegende Funktionenbegriff sehr eng an den aus der Mathematik angelehnt ist.“ [Erwig S
7]. Auch wenn die Definition des Lambda-Kalkül ausgelassen wurde, soll daher nun eine
Umgangssprachliche Definition einer Funktion folgen, wie es für das Verständnis funktionaler
Programmierung ausreichend ist.
Eine Funktion ist eine Abbildungsvorschrift, welche eine oder mehrere Eingaben (Parameter) in eine
eindeutige Ausgabe (Resultat) umwandelt. Die Regeln der Abbildungsvorschrift werden in der
Funktionsdefinition festgelegt. Die Anwendung der Abbildungsvorschrift wird Funktionsapplikation
genannt.[ http://de.wikipedia.org/wiki/Funktion_%28Mathematik%29]
Ein funktionales Programm besteht aus einer Menge, oder besser gesagt einer Folge von
Funktionsdefinitionen und Funktionsapplikationen. Die Funktionen lassen sich wie man es aus der
Mathematik gewohnt wie folgt definieren: f:: A -> B [Thompson S. 3]
Beispiele für die Definition und Applikation von Funktionen in Haskell. Zeilen die mit -- beginnen
sind Kommentarzeilen:
--Beispiel 1 Addition zweier natürlicher Zahlen
-- Die erste Zeile der Definition ist die Typdeklaration, Definitionsbereich und Wertrebereich
-- werden Festgelegt
add :: Int -> Int -> In
add x1 x2 = x1 + x2
--Beispiel 2
-- Maximumsfunktion. Definition mit Fallunterscheidung
maxi :: Int -> Int ->
Int
maxi n
m | n >= m = n
| otherwise = m
--Beispiel 3
--Applikation von add
add 3 5
-- liefert wie wohl kaum anders erwartet 8
add bzw. maxi sind die Namen der Funktionen, die so genannten Funktionsbezeichner. Danach Folgen
die Parameter. Diese sind bei add x1 und x2, bei maxi sind dies n und m. Das Resultat der add
Funktion ist der Wert x1+x2 und bei maxi je nach Fallunterscheidung n oder m.
An diesen beiden Beispielen ist zu erkennen, wie stark die Schreibweise der Definition an die
mathematische Notation erinnert.
Am dritten Beispiel sieht man wie eine Funktionsapplikation in Haskell notiert wird. Zu beachten ist,
dass eine Applikation mit Ausklammern notiert wird. Man schreibt (f x y) anstelle von f(x, y).
An dieser Stelle gilt es darauf hinzuweisen, dass es noch eine weitere Art gibt, Funktionen zu
Definieren, die des pattern matching. Um diese Art der Funktionsdefinition sinnvoll darzustellen,
muss man Datenstruckturen betrachten. Dies geschieht im Folgendem Kapitel.
Zunächst sollen noch zwei Kernkonzepten der funktionalen Programmierung betrachtet werden. Zum
einem die Rekursion und zum anderen Funktionen höherer Ordnung.
Wie bereits erwähn ist die Rekursion die Hauptkontrollstruktur. Bei einer Rekursion werden die Werte
von Argumenten, d.h. die Parameter, auf Basisargumente zurückgeführt. Hierzu ein Beispiel zur
Berechnung der Fakultät.
4
--Beispiel 4
-- Fakultätsfunktion
fak :: Int ->
Int
fak x | x == 0 = 1
| otherwise = fak (x-1) * x
Im Falle, dass x ungleich 0 ist, wird fak erneut aufgerufen, allerdings wird zuvor (in der Klammer)
der Parameter x um 1 verringert. Gleichzeitig wird die rekursiv aufgerufene Funktion fak mit dem
aktuellen Parameter x multipliziert. Dieser Vorgang wiederholt sich so lange, bis der Parameter x dem
Wert 0 entspricht. Dies ist der Basiswert, welcher als Resultat 1 liefert. [vergl Loogen S. 5]
Nun wird es ein wenig komplexer. In funktionalen Sprachen werden Funktionen und Werte
gleichgestellt, d.h. Funktionen, sind so genannte first class citizens und werden wie basiswerte oder
Datenstruckturen behandelt. Sie können als Parameter, oder als Resultat anderer Funktionen auftreten.
Dies kennt man auch aus der Mathematik, z.B. bei der Komposition zweier Funktionen. Dadurch wird
die Definition allgemeiner Berechnungsschemata möglich und ein modularer Programmierstil
unterstützt. [vergl. Thompson S. 168-174]. Solche Funktionen werden als Funktionen höherer
Ordnung bezeichnet. Ein Beispiel ist die mehr oder weniger sinnvolle aber dafür leicht verständliche
Funktion twice:
--Beispiel 5
twice :: (a -> a) -> a -> a
twice
f
x = f (f x)
Die Funktion twice nimmt zwei Argumente, eine Funktion f und einen Basiswert x. Die Funktion f
wird zweimal auf x angewendet.
Erwähnt werden sollte noch, das es in Haskell die Möglichkeit gibt Funktionen partiell anzuwenden,
d.h. es wird zugelassen, dass eine Funktion auf weniger Argumente angewendet wird als zur
Auswertung notwendig ist. Weiterführend wird hier auf Thompson[S. 175-180] verwiesen.
4 Datenstrukturen
In funktionalen Sprachen werden Datenstrukturen mit Datenkonstruktoren gebildet. Sie sind
Funktionen, die frei interpretiert werden, d.h. das Ergebnis der Applikation eines n-stelligen
Konstruktors K auf Ausdrücke a1…an ist die Struktur K( a1…an).
In jeder funktionales Sprache, ist die Liste als Datenstruktur vordefiniert. Listen werden mit Hilfe von
zwei Konstruktoren gebildet. Zum einem mit der Konstruktorkonstante Nil, zur Darstellung der leeren
Liste und des weiteren mit dem Konstruktor Cons, der bei Applikation auf ein Listenelement und einer
Liste, eine neue Liste durch Hinzufügen an den Anfang erzeugt.
Die Listenkonstruktoren Nil und Cons haben einen polymorphen Typ, d.h. für den Typ der
Listenelemente werden Typvariablen verwendet. Als Typvariablen können in Haskell beliebige
Bezeichner, die mit einem Kleinbuchstaben beginnen benutzt werden.
Nil hat den Typ [ ] :: [t] für beliebige Typen t. Damit kann die leere Liste einfach mit [ ] dargestellt
werden. Cons hat den Typ (:) :: t -> [t] -> [t] für beliebige Typen t. Eine Liste wird damit mit (x:xs)
bezeichent, wobei x das vorderste Listenelement (Head) vertritt und xs die Restliste (Tail) darstellt.
[vergl. Loogen S. 25] Mit diesen Konstruktoren ist es nun einfach eine Funktion zu definieren, welche
ein Element an eine Liste anfügt.
-- Beislpiel 6
5
-- An der Typdeklaration sieht man, dass der Funktion ein Element a und eine Liste [a]
-- übergeben wird, das Resultat ist eine Liste.
list :: a -> [a] -> [a]
list x [] = [x]
list x (y:ys) = (x:y:ys)
In der zweiten Zeile der Definition, wird falls die übergebene Liste leer ist, eine Liste mit einem
Element erzeugt. In der dritten Zeile wird der Cons Konstrukor zweimal hintereinander angewendet.
Erst zwischen y:ys und dann zwischen x:(y:ys) .Die Klammern entsprechen hier nicht der Haskell
Notation, sie sollen vielmehr die Rechtsassoziativität von Cons verdeutlichen.
An der Funktionendefinition im Beispiel 6 fällt auf, dass die Funktion list durch mehrere Gleichungen,
welche verschiedene Fälle abdecken definiert wird. Diese Art Funktionen zu definieren wird als
pattern matching bezeichnet. Die Reihenfolge der Gleichungen bei pattern matching ist wichtig, da
diese in der angegebnen Reihenfolge getestet werden und das Parametermuster der zweiten Gleichung
allgemeiner ist als das erste. Pattern matching kann oft auch durch Fallunterscheidungen ersetzt
werden, doch ist gerade bei Datenstrukturen das pattern matching der schönere programmier Stil.
Für Funktionen über Listen werden in der Regel zwei Definitionsgleichungen benötigt, eine welche
den Fall der leeren Liste abdeckt und eine welche den Fall der nicht-leeren Liste mit Parametermuster
(x:xs) abdeckt. [vergl. Loogen S. 26]
Im folgenden Beispiel wird die Länge einer Liste ermittelt. Die Funktion hat den polymorphen Typ a
und bildet auf eine natürliche Zahl ab. Sie ist durch pattern matching definiert und ruft sich selbst
rekursiv auf.
--Beispiel 7
length :: [a] -> Int
length
[] -> 0
length (x:xs) -> 1 + length xs
Nach dem gleichen Prinzip wie Listen definiert sind, kann man auch eignen Konstruktoren definieren,
diese werden algebraische Datenstrukturen genannt. Solche Datenstrukturen sollen hier aber nicht
vertieft werden. Die Deklaration und Anwendung wird bei Thompson[S. 299-303] ausführlich erklärt.
6
III Merkmale Funktionaler Sprachen
1 Referentielle Transparenz
Die fundamentale Eigenschaft, auf der sich alle anderen Merkmale funktionaler Sprachen aufbauen, ist
die referentielle Transparenz. Referenzielle Transparenz bedeutet, dass der Wert eines Ausdrucks nur
von seiner Umgebung und nicht vom Zeitpunkt seiner Auswertung abhängt. Das Resultat einer
Funktion ergibt sich damit nur aus den Parametern. Dieses Verhalten entspricht dem, von Funktionen
in der Mathematik. [vergl. Loogen S. 7]
Um das Verständnis von referentieller Transparenz zu konkretisieren, lohnt es sich ein Gegenbeispiel
zu betrachten.
Bei imperativen Sprachen gilt dieses Prinzip der referenziellen Transparenz nicht. Hier ist der Wert
eines Ausdrucks vom Zeitpunkt seiner Auswertung abhängig.
So ist z.B.
folgende
Wertzuweisungsoperation erlaubt:
i = i + 1;
Diese Gleichung stellt jedoch mathematisch betrachtet eine Gleichung dar, die nicht erfüllbar ist. Noch
deutlicher wird die zeitliche Abhängigkeit bei imperativen Programmen, wenn Seiteneffekte
entstehen, wie z.B. bei folgendem Java-Programm:
class SEffekt {
int count=0;
public int f ( int x ) {
return ++count;
}
}
Die Funktion f erhöht bei jedem Aufruf die globale Variable um eins. Auch wenn ein imperativer
Programmierer dieses Konstrukt für ganz normal hält, hat es einen entscheidenden Nachteil: Mann
kann das Resultat dieser Funktion für einen beliebigen Zeitpunkt nicht vorhersagen. Da der Wert von f
vom vorhergehenden Aufruf abhängt, gilt hier nicht f(1)==f(1)!
In funktionalen Sprachen bestehen Programme grundsätzlich aus einer Reihe von Werte- und
Funktionsdefinitionen. Es gibt keine Zuweisungen an Variablen. Die durch die referenzielle
Transparenz erreicht mathematische Fundierung bewirkt eine wohldefinierte formale Semantik von
Programmen. Damit ist es möglich Korrektheitsbeweise sowie Analysen von Programmeigenschaften
durchzuführen. [vergl. Erwig S. 8-9]
7
2 Programmverifikation
Die referenzielle Transparenz ermöglicht, dass Ausdrücke durch „gleichwertige“ andere Ausdrücke
ersetzt werden können, ohne daß dies Auswirkungen auf die Semantik des Programms hat. Dieses so
genannte Substitutionsprinzip bildet die Basis für die Programmverifikation. [vergl. Erwig S. 61]
Durch die Verifikation kann die Korrektheit eins Programms mathematisch bewiesen werden. Dies
geschieht mit Hilfe der vollständigen Induktion. So kann z.B. die Korrektheit der oben beschriebenen
Funktion fak zur Berechnung der Fakultät leicht nachgewiesen werden.
Behauptung: Die folgende Funktion berechnet die Fakultät von k
-- Fakultätsfunktion
fak :: Int ->
Int
fak x | x == 0 = 1
| otherwise = fak (x-1) * x
Induktion über k:
Induktionsanfang:
k=0 => 0! = 1 und fak 0 = 1
Induktionssahnnahme: gilt k! so auch fak k
Induktionsschritt:
<=>
<=>
<=>
k = k +1
(k + 1)!
(k + 1) * k!
(k + 1) * fak k
fak (k + 1)
Somit ist die Behauptung bewiesen!
Analog zum Prinzip der vollständigen Induktion für natürliche Zahlen, wird das Prinzip der
Listeninduktion zum Beweis von Aussagen über endliche Listen eingesetzt. Listen sind wie folgt
induktiv über dem Grundtyp t definiert [vergl. Erwig S. 66]:
1. [] ist Liste über t ([] :: [t])
2. für beliebige Elemente x :: t und xs :: [t] ist auch (x:xs) eine Liste über t
3. für alle x :: t, xs :: [t] gilt: (x:xs) ungleich []
4. nur die mit 1. und 2. erzeugten Elemente sind Listen über t
Ein einfaches Beispiel für die Listeninduktion ist der Nachweis der Assoziativität der
Listenverkettung. Die Listenverkettung ist in Haskell eine vordefinierte infix Funktion, die wie folgt
definiert ist:
-- Die Klammern um den Funktionsbezeichner, bedeuten das ++ infix ist
(++) :: [a] -> [a] -> [a]
[] ++ ys = ys
(x:xs) ++ ys = x:(xs++ys)
Behauptung: ++ ist assoziativ, d.h. für beliebige endliche Listen xs, ys, zs :: [a] gilt:
xs ++ (ys ++ zs) = (xs ++ ys) ++ zs
Listeninduktion über xs:
8
Induktionsanfang: xs = []
=> [] ++ (ys ++ zs) =
ys ++ zs
= ([] ++ ys) ++ zs
Induktionsannahme: xs ++ (ys ++ zs) = (xs ++ ys) ++ zs
Induktionsschritt: xs = x:xs`
(x:xs`) ++ (ys ++zs) = x:(xs` ++ (ys ++ zs))
= x: ((xs’ ++ ys) ++ zs)
=(x: (xs’ ++ ys)) ++ zs
=((x:xs`) ++ ys) ++ zs
Somit ist die Behauptung Bewiesen!
3 Ein-/ Ausgabe
Wie gerade gesehen wird mit der referentiellen Transparenz ein enormer Vorteil gegenüber
imperativen Sprachen erreicht. Es ist jedoch nicht leicht die referenzielle Transparenz für die Ein-/
Ausgabe aufrecht zu erhalten.
Mit Ein-/ Ausgabaktionen sind in jeder Programmiersprache immer imperative Anweisungen
verbunden. Umgangsprachig definiert läuft eine interaktive Ein-/ Ausgabe wie folg ab:
1. lese ein Zeichen
2. mache etwas damit
3. gib es wieder aus
Für funktionale Sprachen stellt dies ein schwieriges Problem dar, da diese imperativen Anweisungen
die referenzielle Transparenz verletzen würden und Seiteneffekte bewirken könnten.
Haskell ist eine Funktionale Sprache bei der man es geschafft hat diese Problem zu lösen. Dafür wird
ein Mechanismus zur Kapselung von Berechnungen verwendet, die so genannte I/O-Monade. Diese
vermittelt zwischen der funktionalen Welt und der imperativen Welt, wobei der Übergang eine
Einbahnstraße ist. Funktionale Werte können in Monaden eingebettet werden, aber aus einer Monade
können keine Werte in die in Funktionale Welt übertragen werden.
In der Monade selbst, wird eine sequentielle Ausführungsreihenfolge für Ein-/Ausgabeaktionen
festgelegt. Diese muss in jeder Implementierung eingehalten werden. [verg Loogen S. 47-52]
Die Grundlegenden Deklarationen für I/O-Monaden sind:
und
data IO a
-- I/O-Aktionen, die einen Wert vom Typ a liefern
return :: a -> IO a
-- Um Werte in eine Monade einzubetten, wird diese Funktion verwendet
An den Deklarationen ist zu erkennen, das IO ein Typ ist, mit dem Monaden repräsentiert werden.
Ein besseres Bild vom Umgang mit Monaden bekommt man wenn man die vordefinierten Funktionen
betrachtet.
Elementare vordefinierten Ein-/Ausgabefunktionen sind:
putChar
:: Char -> IO ()
9
putStr
getChar
getLine
:: String -> IO ()
:: IO Char
:: IO String
Die Funktionen putChar und putStr geben ein Zeichen bzw. eine Zeichenkette auf der
Standardausgabe aus und liefern das leere Tupel () als Resultat. Die Funktionen getChar und getLine
lesen ein Zeichen bzw. eine Zeichenkette von der Standardeingabe und liefern dieses als Resultat.
Die Funktion putStr kann mit Hilfe der putChar Funktion auch leicht selbst definiert werden:
putStr
putStr []
putStr (c:cs)
:: [Char] -> IO ()
= return ()
= do putChar c
putStr cs
Dieses Beispiel zeigt, dass obwohl es schwierig ist die referentielle Transparenz bei I/O aufrecht zu
erhalten, das Programmieren mit I/O-Aktionen nicht schwer ist. Gerade für die I/O gibt es eine Reihe
von Standard-Bibliotheken für Haskell, aus denen man Module importieren kann. Auf der Webseite
http://www.haskell.org/libraries/#guis
existiert eine Übersicht über die bisher existierenden Bibliotheken.
4 Auswertung
Im folgendem Kaptitel macht sich die referenzielle Transparenz funktionaler Sprachen wieder
vorteilhaft bemerkbar. Da keine Seiteneffekte auftreten, ist die Auswertungsreihenfolge von
Ausdrücken nicht von Bedeutung. Es kommt daher nur darauf an ob es überhaupt eine erfolgreiche
Auswertung gibt.
Die Auswertungsstrategie bestimmt, in welcher Reihenfolge die einzelnen Teilausdrücke (Redexe)
ausgewertet werden. Es gibt zwei Standartstrategien: Zum einem die „leftmost innermost“ und zum
anderen die „leftmost outermost“ Strategie.
Wie aus dem Namen vielleicht schon hervor geht, wird bei der „leftmost innermost“ zuerst dieses
Redex ausgewertet, welches am weitesten innen vorkommt und unter diesen am weitesten links steht.
Bei der „leftmost outermost“ wird dem entsprechend zuerst das am weitesten außen stehende und
davon das am weitesten links stehende Redex ausgewertet. Im folgendem ein Beispiel zur
Veranschaulichung.
(length ((fak 2) : (fac 3))
Bei dem obigen Ausdruck wird mit der „leftmost innermost“ Strategie zuerst (fak 4) ausgewertet, dann
(fak 7) und dann (length [2,6]) .
Mit der „leftmost outermost“ Strategie wird zuerst (length
ausgewertet.
), dann (fak 2) und dann (fac 3)
Es fällt auf, dass bei der „leftmost outermost“ Stratgie die Länge der Liste schon nach dem ersten
Auswertungsschritt ermittelt wurde, die folgenden Schritte sind dafür nicht nötig und werden daher
auch nicht mehr ausgewertet. Hier handelt es sich um eine so genannte bedarfsgesteuerte Auswertung
(Lazy Evaluation).
10
Bei funktionalen Sprachen wird im Gegensatz zu den imperativen Sprachen die Bedarfgesteuerte
Auswertung realisiert. Man muss jedoch klarstellen das die Bedarfsgesteuerte Auswertung nicht
effizienter ist, da die Verwaltung der nicht ausgewerteten Ausdrücke einen hohen Aufwand hat. Es
ergeben sich jedoch andere sehr nützliche Vorteile. So ist z.B. die Verarbeitung partieller
Informationen erlaubt, d.h. es ist möglich mit noch nicht ausgewerteten Ausdrücken zu Rechnen.
Ein anderer sehr interessanter Vorteil ist das Arbeiten mit unendlichen Datenstruckturen. Folgende
Fibonacci-Funktion ist damit in Haskell möglich:
fib :: Int -> Int -> [Int]
fib n
m = n : fib m (n+m)
5 Das Hindley-Milner-Typsystem
Als leztes soll nun noch ein Blick auf das H-M-Typsystem geworfen werden. Bei Haskell, wie auch
bei den meisten anderen funktionalen Sprachen kommt dieses System zum Einsatz. Da es sich um ein
polymorphes Typsystem handelt wird zuerst der unterschied zwischen der ad-hoc Polymorphie und
der parametrischen Polymorphie erklärt.
Die Ad-Hoc-Polymorphie zeichnet sich durch Überladene Operatoren aus. So wird z.B. für den
Gleichheitsoperator (==) eine Syntax für alle Typen verwendet. Für verschiedene Typen ist der
Operator verschieden definiert. Entsprechend dem Kontext wird die passende Definition ausgewählt.
Parametrische Polymorphie, liegt bei Funktionen oder Datenstruckturen vor, wenn die Parameter der
Funktion bzw. die Elemente der Datenstrucktur eine Typvariable ist. D.h. bei der Definition ist noch
nicht klar welcher Typ übergeben wird. So kann man z.B. in Haskell die Funktion length, welche die
Länge einer Liste bestimmt mit folgender Typdeklaration definieren:
length :: [a] -> Int
Das „a“ ist die Typvariable und steht für beliebige Listenelemente.
Nun zurück zum Hindley-Milner-Typsystem. Dies unterscheidet sich von den konventionellen
Sprachen dadurch, dass parametrische Polymorphie zugelassen wird. Der bedeutendste Unterschied ist
jedoch, daß Typen automatisch abgeleitet werden können. Dieser Automatismus wird Typinferenz
genannt. Die Typinferenz hat enorme Vorteile. Durch das Bestimmen von Typen zu ungetypten
Ausdrücken während der Compilezeit werden Laufzeitfehler vermieden und Programmfehler, die sich
durch Typfehler äußern frühzeitig erkannt. Die Typinferenz ist ein sehr kompliziertes und komplexes
Verfahren, eine ausführliche Beschreibung findet man im Skript von Frau Loogen.
Die oben beschriebene Ad-Hoc-Polymorphie wird durch das Hindley-Milner-Typsystem nicht
unterstützt. Um diese Polymorphie zu realisieren stellt Haskell Typlklassen bereit, in denen
Operatoren mehrfach definiert sind.
11
IV Fazit
„Functional languages take another large step towards a higher-level-programming model.
Programs are easier to design, write and maintain, but the languages offers the programmer less
control over
the machine. For most programs the result is perfectly acceptable.“
[http://www.haskell.org/aboutHaskell.html]. Zu diesem Fazit kommt der Autor der Haskell.org
Webseite nach einem Vergleich von funktionalen und imperativen Sprachen. Ein repräsentatives
Beispiel für den Vergleich, ist der Quicksort Algorithmus:
Quicksort in Haskell
qsort [] = []
qsort (x:xs) = qsort elts_lt_x ++ [x] ++ qsort elts_greq_x
where
elts_lt_x = [y | y <- xs, y < x]
elts_greq_x = [y | y <- xs, y >= x]
Quicksort in C
qsort( a, lo, hi ) int a[], hi, lo;
{
int h, l, p, t;
if (lo < hi) {
l = lo;
h = hi;
p = a[hi];
do {
while ((l < h) && (a[l] <= p))
l = l+1;
while ((h > l) && (a[h] >= p))
h = h-1;
if (l < h) {
t = a[l];
a[l] = a[h];
a[h] = t;
}
} while (l < h);
t = a[l];
a[l] = a[hi];
a[hi] = t;
qsort( a, lo, l-1 );
qsort( a, l+1, hi );
}
}
Das Programm in Haskell spiegelt genau die Arbeitsweise des Algorithmus wieder, es ist kurz und
übersichtlich, benötigt allerdings viel Speicher. Das Programm in C ist dagegen vom Ablauf her nicht
so schnell zu durchschauen, es benötigt allerdings sehr viel weniger Speicher.
Egal ob Vor- oder Nachteil, beides ergibt sich bei funktionalen Sprachen aus der referenziellen
Transparenz. Referenzielle Transparenz ist die zentrale Idee funktionaler Programmierung. Um
funktionales Programmieren zu verstehen muss man verstanden haben was referenzielle Transparenz
12
bedeutet. Für viele Programmierer ist es jedoch eine große Umstellung sich vom imperativen
Programmablauf zu lösen und sich in das deklarative Denken hinein zu versetzen.
Ob sich eine Umstellung lohnt hängt vom Problem welches gelöst werden soll ab. Ein Studie von
Ericsson hat allerdings ergeben, dass mit der Einführung der funktionalen Sprache Erlang die
Programmierproduktivität bei der Erstellung von Telekommunikationssoftware um einen Faktor 9 bis
25 gesteigert werden konnte.[vergl. http://www.haskell.org/aboutHaskell.html]
Ein Aspekt der aktuellen Forschung ist die implizite Parallelität. Wie wohl kaum anders erwartet,
ermöglicht die referenzielle Transparenz, dass unabhängige Programmteile auch unabhängig
ausgewertet werden können. In diesem Bereich arbeitet Frau Prof. Dr. Loogen an der Universität
Marburg.
Ein ganz anderer Aspekt ist der Einsatz von funktionaler Sprachen in Schulen und Universitäten aus
didaktischen Gründen. Denn eine Auseinandersetzung mit deklarativer Programmierung trägt auch
beim imperativen Programmieren zu einem schönerem Programmierstil bei. So kann z.B. die
Rekursion zu einem selbstverständlichem Werkzeug werden. Der Programmierstiel ist aus sicht der
Wartung nicht zu unterschätzen.
In diesem Sinne sollte diese Seminararbeit dazu beitragen, das Interesse an funktionalen Sprachen zu
wecken.
13
Quellenverzeichnis
Literatur
Erwig, Martin. Grundlagen funktionaler Programmierung; München, 1999
Gumm, Heinz-Peter. Einführung in die Informatik; München, 2002
Loogen, Rita. Praktische Informatik III: Deklarative Programmierung; Marburg, WS 2003/2004
Thompson, Simon. Haskell Craft of Functional Programming 2.Edition; Edingburgh, 1999
Internet
http://www.haskell.org
http://www.wikipedia.de
14
Herunterladen