Prinzipien funktionaler Programmieren Am Beispiel der Sprache Haskell HfT Stuttgart Seminar: Wissenschaftliches Schreiben Seminar Leiterin: Frau Prof. Dr. Burger Student: Yves Alexander Matrikelnr.: 371275 Inhaltsverzeichnis I Einleitung...................................................................................................................................... 2 II Grundlagen funktionaler Sprachen.......................................................................................... 3 1 Funktionale Sprachen und „Rapid-Prototyping“ ................................................................ 3 2 Einordnung in das Sprachspektrum ..................................................................................... 4 3 Das Lambda-Kalkül ................................................................................................................ 5 4 Das Programm als Menge von Funktionen........................................................................... 5 5 Datenstrukturen ...................................................................................................................... 7 III Merkmale Funktionaler Sprachen .......................................................................................... 8 1 Referentielle Transparenz ...................................................................................................... 8 2 Programmverifikation ............................................................................................................ 9 3 Ein-/ Ausgabe ......................................................................................................................... 10 4 Auswertung ............................................................................................................................ 11 IV Fazit .......................................................................................................................................... 13 Literaturverzeichnis ..................................................................................................................... 14 Bücher ....................................................................................................................................... 14 Internet ...................................................................................................................................... 14 1 I Einleitung Angesichts hoher Kosten in der Softwareentwicklung, wird die Projektplanung immer wichtiger. Dabei bekommt das so genannte „Rapid Prototyping“ eine immer bedeutendere Rolle, da mit einem Prototyp eine sehr konkrete Spezifikation erreicht wird. Auf diesem Gebiet des „Rapid Prototyping“ 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 weiterer 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. Dabei wird beispielhaft die funktionale Sprache Haskell näher betrachtet. Es werden jedoch 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 in Haskell vermittelt werden. Diese 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 einen 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 Funktionale Sprachen und „Rapid-Prototyping“ Wie in der Einleitung schon gesagt eignen sich funktionale Sprachen sehr gut für das „Rapid-Prototyping“ im Rahmen der Softwareentwicklung. Der Grund liegt darin, dass es relativ leicht ist die algorithmische Arbeitsweise eines funktionalen Programms zu verstehen, auch wenn keine Vorkenntnisse in funktionaler Programmierung bzw. in der Sprache Haskell vorhanden sind. Um dies zu demonstrieren, soll nun gleich zu beginn ein Haskell Programm und ein C Programm verglichen werden. Quicksort in Haskell (zum Sortieren einer Liste): qsort [] = [] qsort (x:xs) = qsort less_then_x ++ [x] ++ qsort greater_then_x where less_then_x = [y | y <- xs, y < x] greater_then_x = [y | y <- xs, y >= x] Quicksort in C (zum Sortieren einer Liste) 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 ); } } 3 Vergleicht man das Haskell Programm und das C Programm, fällt sofort auf, dass das Haskell Programm mit viel weniger Zeilen auskommt als das C Programm. Das C Programm ist ohne Kenntnisse der Sprache C nur schwer zu verstehen. Dagegen erinnert die Syntax des Haskell Programms, zum teil stark an mathematische Notationen, was enorm zum Verständnis beiträgt. In der ersten Zeile des Haskell Programms wird der Fall der leeren Liste „[]“ behandelt, diese ist schon sortiert und wird als Resultat zurückgegeben. In der zweiten Zeile wird die nicht leere Liste „(x:xs)“ behandelt. Dabei ist x das erste Listenelement und xs die Restliste. Auf der rechten Seite der Definitionsgleichung kann man genau die Idee des Quicksortalgorithmus erkennen. Das erste Listenelement x wird als Pivo-Element genommen. Links davon wird eine Liste „less_then_x“ angehängt in der alle Elemente kleiner als x sind, jedoch unsortiert. Auf diese Liste wird qsort rekusriv angewendet. Analog wird rechts vom Pivo-Element eine Liste „greater_then_x“ angehängt. Die Listen „less_then_x“ und „greater_then_x“ werden in den unteren Zeilen erzeugt. Die dazugehörende Syntax entspricht der Mengendefinition in der Mathematik. Bevor nun genauer der Aufbau und die Arbeitweise eines Funktionalen Programms erläutert wird, soll erst eine Einordnung in das Sprachspektrum vorgenommen werden, um einen Überblick über die verschiedenen Konzepte von Programmiersprachen zu bekommen. 2 Einordnung in das Sprachspektrum Das weite Spektrum der Programmiersprachen, lässt sich grob in zwei Kategorien aufteilen. Zum einen 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 bestehen, die konsequent nacheinander ausgeführt werden. Damit werden Berechnungen als Abfolge lokaler Wertzuweisungen beschrieben. Die Hauptkontrollstruktur ist dabei die While-Schleife. 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 funktional-logischen 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. 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. 4 3 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. 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. 4 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. 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 ist, wie folgt definieren: f:: A -> B Eine Funktion zur Addition zweier natürlicher Zahlen hat damit in Haksell folgende Syntax: add :: Int -> Int -> Int add x1 x2 = x1 + x2 Die erste Zeile der Definition ist die Typdeklaration, dabei werden Definitionsbereich und Wertrebereich festgelegt. In der zweiten Zeile werden die beiden Parameter mit dem „+“ Operator addiert, dabei ist die rechte Seite der Gleichung gleichzeitig das Resultat der Funktion. Auch Programmverzweigungen erinnern stark an die mathematische Notation. So gibt es kein Schlüsselwort wie beispielweise „if“ mit dem eine Programmverzweigung realisiert werden kann. Hierfür wird eine Fallunterscheidung benutzt deren Syntax im folgenden Beispiel zu sehen ist. maxi :: Int -> Int -> Int maxi n m | n >= m = n | otherwise = m 5 Diese Funktion liefert das Maximum der beiden Parameter n und m. Trift n >= m zu, ist n das Resultat, andernfalls ist m das Resultat. Eine Funktionsapplikation hat in Haskell folgende Syntax. add 3 5 Hier wird die oben definierte Funktion „add“ auf die Parameter 3 und 5 angewendet. Das Resultat ist 8. 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 Datenstrukturen betrachten. Dies geschieht im folgenden 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ähnt, 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. 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. In funktionalen Sprachen werden Funktionen und Werte gleichgestellt, d.h. Funktionen, sind so genannte „first class citizens“ und werden wie basiswerte oder Datenstrukturen 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. 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: 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 verwiesen. 6 5 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 funktionaler 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) bezeichnet, 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. -- 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 obigen Funktionendefinition 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 Programmierstil. 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. 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. length :: [a] -> Int length [] -> 0 length (x:xs) -> 1 + length xs Nach dem gleichen Prinzip wie Listen definiert sind, kann man auch eigene Konstruktoren definieren, diese werden algebraische Datenstrukturen genannt. Solche Datenstrukturen sollen hier aber nicht vertieft werden. Die Deklaration und Anwendung wird bei Thompson ausführlich erklärt. 7 III Merkmale Funktionaler Sprachen 1 Referentielle Transparenz Nach der kurzen Einführung in die Grundlagen der funktionalen Programmierung in Kapitel II, werden nun die Merkmale funktionaler Sprachen betrachtet. Hier wird der grundlegende Unterschied zu imperativen Sprachen deutlich und die bedeutendste Antwort, auf die Fragen was eine funktionale Sprache ist, gegeben. Diese Frage kann mit dem Schlagwort referentielle Transparenz kurz beantwortet werden. Die referentielle Transparenz ist die fundamentale Eigenschaft, auf der sich alle anderen Merkmale funktionaler Sprachen aufbauen. Referentielle 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. 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 referentielle Transparenz erreichte mathematische Fundierung, bewirkt eine wohldefinierte formale Semantik von Programmen. Damit ist es möglich Korrektheitsbeweise, sowie Analysen von Programmeigenschaften durchzuführen. 8 2 Programmverifikation Die referentielle 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. 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 Induktionsannahme: gilt k! so auch fak k, für k > 0 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: 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 9 Listeninduktion über xs: 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 referentielle Transparenz für die Ein-/ Ausgabe aufrecht zu erhalten. Mit Ein-/ Ausgabeaktionen 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 referentielle 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. 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. 10 Ein besseres Bild vom Umgang mit Monaden bekommt man, wenn man die vordefinierten Funktionen betrachtet. Elementare vordefinierten Ein-/Ausgabefunktionen sind: putChar putStr getChar getLine :: Char -> IO () :: 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, 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 StandardBibliotheken 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 folgenden Kaptitel macht sich die referentielle 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 folgenden 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 ), dann (fak 2) und dann (fac 3) ausgewertet. 11 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). Bei funktionalen Sprachen, wird im Gegensatz zu den imperativen Sprachen, die bedarfgesteuerte Auswertung realisiert. Man muss jedoch klarstellen, dass 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 Datenstrukturen. Folgende Fibonacci-Funktion ist damit in Haskell möglich: fib :: Int -> Int -> [Int] fib n m = n : fib m (n+m) 12 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. Ob sich der Einsatz funktionaler Sprachen lohnt, hängt also 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. Abgesehen vom kommerziellen Nutzen funktionaler Sprachen, kann der Einsatz in Schulen und Universitäten, didaktisch sinnvoll sein. Denn eine Auseinandersetzung mit deklarativer Programmierung, trägt auch beim imperativen Programmieren zu einem schöneren Programmierstil bei. So kann z.B. die Rekursion zu einem selbstverständlichen Werkzeug werden. Ein guter Programmierstil, ist wiederum aus kommerzieller sicht nicht zu unterschätzen, da somit eine effizientere Wartung der Software garantiert werden kann. Ein Aspekt der aktuellen Forschung ist die implizite Parallelität. Die referentielle Transparenz funktionalen Sprachen ermöglicht, dass unabhängige Programmteile auch unabhängig ausgewertet werden können. In diesem Bereich arbeitet Frau Prof. Dr. Loogen an der Universität Marburg. In diese Bereiche der Forschung, konnte in dieser Seminararbeit, auf Grund des Umfangs nicht eingegangen werden. Viel mehr sollte die Idee der funktionalen Programmierung dargestellt werden. Diese, ist die referentielle Transparenz, welche nach einem längeren Umgang mit funktionalen Sprachen, für den Programmierer sehr komfortabel werden kann. 13 Literaturverzeichnis Bücher 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