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