Westfälische Wilhelms-Universität Münster Ausarbeitung Haskell im Rahmen des Vertiefungsmoduls Programmiersprachen Dirk Metzger Themensteller: Prof. Dr. Herbert Kuchen Betreuer: Susanne Gruttmann Institut für Wirtschaftsinformatik Praktische Informatik in der Wirtschaft Inhaltsverzeichnis 1 2 Einleitung ................................................................................................................... 3 1.1 Vorwort und Motivation .................................................................................... 3 1.2 Entwicklung und Historie der Sprache .............................................................. 3 Grundlegende Sprachkonstrukte ................................................................................ 5 2.1 Charakteristika der Sprache ............................................................................... 5 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.2 Eigenheiten in Syntax und Semantik ................................................................. 8 2.2.1 2.2.2 2.2.3 2.2.4 3 Designprinzipien der Sprache ..................................................................... 5 Auswertung von Ausdrücken...................................................................... 6 Funktionen höherer Ordnung ...................................................................... 6 Currying ...................................................................................................... 7 Lambda-Ausdrücke & lokale Variablen ..................................................... 8 Signaturen und Vererbung .......................................................................... 8 Namenskonventionen und Schreibweisen .................................................. 9 Pattern Matching ....................................................................................... 10 Send More Money .................................................................................... 11 Monaden .................................................................................................................. 13 3.1 Notwendigkeit von Monaden ........................................................................... 13 3.2 Definition von Monaden .................................................................................. 13 3.3 Besonderheiten der Definition ......................................................................... 15 3.4 Regeln zu Monaden.......................................................................................... 17 3.5 Arten von Monaden.......................................................................................... 18 3.5.1 3.5.2 3.5.3 3.6 Die IO-Monade ......................................................................................... 18 Die Zustandsmonade................................................................................. 18 Listen als Monaden ................................................................................... 19 Zusammenfassung ............................................................................................ 20 4 Haskell in der Praxis ................................................................................................ 21 5 Fazit ......................................................................................................................... 22 Literaturverzeichnis ........................................................................................................ 23 II Kapitel 1: Einleitung 1 Einleitung 1.1 Vorwort und Motivation Diese Seminararbeit beschäftigt sich mit der Programmiersprache Haskell. Als Vertreter der funktionalen Programmierung bietet Haskell interessante Konzepte und Eigenheiten die hier aufgegriffen werden. Des Weiteren besticht die Sprache durch ihre Andersartigkeit zu verbreiteten und gängigen Programmiersprachen, was die nähere Betrachtung zu einer willkommenen Abwechslung macht. In dieser Ausarbeitung wird zunächst ein Einblick in die Historie der Sprache gegeben. Darauf folgen eine Beschreibung besonderer Sprachkonstrukte und die Auseinandersetzung mit der Syntax sowie der Semantik der Sprache Haskell. Im Hauptteil wird dann genauer auf ein charakterisierendes Merkmal der Sprache eingegangen, nämlich die Verwendung von Monaden. Abschließend wird ein kurzer Einblick in die derzeitige Nutzung der Sprache gegeben, gefolgt von einem Fazit. 1.2 Entwicklung und Historie der Sprache Der Gedanke der funktionalen Programmierung, der Haskell als reine funktionale Programmiersprache angehört, entstand bereits 1978, also weit vor der Entstehung der Sprache. Es wurden von vielen herausragenden Informatikern verschiedene Sprachen der funktionalen Programmierung entwickelt, welche direkten Einfluss ausübten. Die Wichtigsten sind dabei Lisp, ML und Scheme. Lisp war die erste funktionale Sprache. ML und Scheme waren zur Entstehungszeit von Haskell die verbreitetesten Sprachen. Sie wurden an den großen Instituten wie dem MIT und den Universitäten von Indiana und Yale genutzt. Die prägende Idee hinter Haskell war, diese beiden Gruppen von Informatikern an einen Tisch zu bringen und beiden die Möglichkeit zu geben, sich gemeinsam mit einer Programmiersprache auseinander zu setzen. Als im Herbst 1987 auf der „Functional Programming and Computer Architecture Conference“ der Entschluss gefasst wurde, eine neue funktionale Sprache zu entwickeln, wurde als Basis dafür zunächst die solide und gut ausgeprägte Sprache Miranda gewählt. Dem Schöpfer der Sprache Miranda David Turner missfiel jedoch der Gedanke, dass seine Sprache in verschiedene Dialekte und separate Weiterentwicklungen zergliedert werden sollte. Durch die Ablehnung der Zusammenarbeit kam es dann zu der radikalen 3 Kapitel 1: Einleitung Neuentwicklung der Sprache. Die am meisten prägenden Köpfe, welche die Entwicklung dieser neuen Sprache vorantrieben, waren Paul Hudak von der Universität Yale, Simon Peyton Jones von Microsoft Research und Philip Wadler von der Edinburgh Universität. Sie arbeiteten in einem Komitee aus insgesamt 36 Personen. Der Name „Haskell“ entstand auf der ersten Konferenz der neuen Sprache in Yale und ist zu Ehren des Mathematikers Haskell Brooks Curry, welcher im Bereich der kombinatorischen Logik die Grundlagen für funktionale Programmierung geprägt hat [HoH] . Die Neuentwicklung bot viel Spielraum, um auch experimentelle nicht verbreitete Möglichkeiten, wie z.B. Typklassen (vgl. Kap. 2.2.1), zu integrieren. Nach einer Entwicklungszeit von etwa drei Jahren wurde am 1. April 1990 der Haskell Reoprt Version 1.0 veröffentlicht. Danach verschob sich die Diskussion mehr und mehr in die Öffentlichkeit. Die Entscheidungen über die Entwicklung der Sprache wurden jedoch weiterhin von dem Komitee getroffen. Der nächste Meilenstein war im Jahre 1996, als Haskell mit vielen Änderungen und Neuerungen, wie z.B. den Monaden, in seiner Version 1.3 erschien. Zuletzt wurde 2002 der Haskell 98 Report in überarbeiteter Form publiziert und standardisierte die Sprache [Jon02] . Dieser Standard ist bis heute vorherrschend und gültig. Durch die steigende Popularität wächst seit Jahren die Anzahl der Bibliotheken, welche zusätzliche Möglichkeiten und Funktionen bieten. Inzwischen hat die Entwicklung des Nachfolgers von Haskell unter dem Namen Haskell‘ (Haskell Prime) begonnen. Es wird versucht die vielen Zusatzmöglichkeiten zu standardisieren und zu adaptieren und somit einen neuen Sprachstandard zu schaffen. Jener Prozess dauert weiterhin an [Pri09] . Basierend auf dem gegebenen Standard Haskell 98 gibt es derzeit zwei große Compiler/Interpreter, die hauptsächlich genutzt werden. Dies ist zum einen der Interpreter Hugs, welcher von dem Studenten Mark Jones in Oxford geschrieben wurde. Implementierungsbeginn war bereits 1991, wobei die Entwicklung weiterhin fortgesetzt wird. Es sind inzwischen weitere Programmierer hinzugestoßen, die an der derzeitigen Version vom September 2006 mitwirken. Der andere bekannte Compiler/Interpreter ist der Glasgow Haskell Compiler (GHC). Dessen Entwicklung begann bereits im Jahre 1989. Er ist damit älter und ausgeprägter und wird immer noch weiterentwickelt. Aktuell ist die inzwischen frei verfügbare Version 6.10.2 [GHC09] . 4 Kapitel 2: Grundlegende Sprachkonstrukte 2 Grundlegende Sprachkonstrukte 2.1 Charakteristika der Sprache 2.1.1 Designprinzipien der Sprache Es existierten einige Prinzipien, die bei der Entstehung von Haskell überragende Bedeutung hatten und damit die Sprache maßgeblich beeinflusst haben. Das wichtigste Prinzip war die konsequente bedarfsgetriebene Auswertung (Lazy Evaluation), auf welche später detailliert eingegangen wird (vgl. Kap. 2.1.2). Daraus folgte auch das Prinzip der Reinheit der funktionalen Programmiersprache. Diese Konsequenz ergab sich aus dem notwendigen Verzicht auf Seiteneffekte zur korrekten Umsetzung der Lazy Evaluation. Seiteneffekte in Funktionen führen zur Auswertung in Abhängigkeit des Zeitpunktes, da sich in der Zwischenzeit die Variable durch Seiteneffekte verändert haben könnte. Damit die Lazy Evaluation adäquat umgesetzt werden konnte und die Auswertung zu jedem Zeitpunkt gleich stattfindet, mussten Seiteneffekte weichen. Das Prinzip der reinen funktionalen Programmierung impliziert, dass jede Funktion immer zu genau einem Ergebnis ausgewertet wird. Daraus resultiert auch eine deutliche Vereinfachung mathematischer Betrachtungen insbesondere im Hinblick auf die Laufzeit von Funktionen. Das Programmierparadigma der funktionalen Programmierung begünstigt des Weiteren die Modularität. Dies entspricht der Aufteilung von Funktionen und Modulen in Bibliotheken gegliedert nach Aufgabenbereichen. Die Module bieten für den Entwickler auch den maßgebenden Namensraum, in welchem ihren Funktionen eindeutige Namen zugewiesen werden müssen. Es existiert, wie bereits erwähnt, eine Vielzahl von Bibliotheken und zusätzlichen Funktionalitäten für Haskell im Internet [HC01] . Diese können durch Importieren verwendet werden und bieten so eine einfache Möglichkeit, die gegebene Funktionalität zu erweitern. Grundlegend für Haskell ist die starke Typisierung. Jede Variable hat ihren designierten Typ. Dynamische Zuweisungen oder Veränderungen von Typen der Variablen sind nicht möglich. Dies ist insbesondere für das „Pattern Matching“ nötig, welches im späteren Verlauf genauer behandelt wird. (vgl. Kap. 2.2.3) Dennoch kann beim Programmieren an einigen Stellen auf das Explizieren des Typs verzichtet werden, da der Compiler diesen ermitteln kann. 5 Kapitel 2: Grundlegende Sprachkonstrukte 2.1.2 Auswertung von Ausdrücken Die Art der Auswertung in Haskell wird als „Lazy Evaluation“ bezeichnet. Dies bedeutet, dass zu jedem Zeitpunkt immer nur das Benötigte ausgewertet wird. Dabei wird bei Bedarf das zu verarbeitende Sprachkonstrukt von der äußersten Anweisung beginnend analysiert. Tatsächlich ausgewertet wird ein Ausdruck erst dann, wenn er weiterverarbeitet werden muss und trivial auswertbar ist, also keine andere Auswertung vorgezogen werden muss, um diesen auswerten zu können. Die Konsequenz daraus ist ebenso, dass bei der Übergabe von Parametern ganze, unausgewertete Ausdrücke übergeben werden können. Parameter müssen also nicht zwingend vor der Übergabe ausgewertet werden. Diese Art der Weitergabe von Termen an Parameter nennt man Termersetzung. Der Vorteil, der sich daraus ergibt, liegt insbesondere in der Möglichkeit, unendliche Strukturen abzubilden und mit diesen zu arbeiten. Die Auswertung erfolgt nur bei Bedarf. Wie oben bereits beschrieben gilt, dass jeder Ausdruck zeitunabhängig gleich auszuwerten ist. Dies führt dazu, dass gleiche Ausdrücke in Haskell nicht doppelt ausgewertet werden müssen. Sie werden stattdessen im Zeitpunkt der ersten Auswertung, determiniert durch die Lazy Evaluation, an allen Stellen, an denen dieser Ausdruck steht, ersetzt. Die Vorgehensweise wird referenzielle Transparenz genannt und führt, in Kombination mit „Lazy Evaluation“, zu besonders ressourcensparender Verarbeitung. Ö Ö Ö Ö (2 + ((2 + 1) + 1)) + (3 + 1) (2 + (3 + 1)) + (3 + 1) (2 + 4) + 4 6 + 4 10 Programmbeispiel 1 In diesem Beispiel sieht man, dass der Ausdruck (3+1) doppelt vorhanden ist und in der Auswertung zeitgleich ersetzt wird. 2.1.3 Funktionen höherer Ordnung Durch die oben genannte Art der Auswertung ergibt sich eine weitere Möglichkeit der Programmierung in Haskell. Diese nennt man Funktionen höherer Ordnung. Es handelt sich dabei um die Idee, ganze Funktionen anderen Funktionen zu übergeben. Die 6 Kapitel 2: Grundlegende Sprachkonstrukte mögliche Parametrisierung führt z. B. zu sehr leistungsfähigen Möglichkeiten in Kollektionen Daten zu ändern. Das Paradebeispiel ist hierbei die Map-Funktion, welche eine übergebene Funktion auf alle Elemente einer Liste anwendet. map map f xs :: (a -> b) -> [a] -> [b] = [ f x | x <- xs ] Programmbeispiel 2 (Entnommen aus Hugs.Prelude.hs) Diese Funktion bietet auf hoher Ebene eine gute Möglichkeit, schnell sämtliche Elemente einer Liste mit einer beliebigen Funktion, übergeben für f, in gewünschter Weise zu überarbeiten. Dabei muss die Funktion f lediglich ein Variable vom Typ a konsumieren und eine vom Typ b zurück geben. Hierbei können die Typen a und b auch identisch sein [Man04] . 2.1.4 Currying Als Currying wird die Auswertungsart der Funktionen bezeichnet. Hierbei wird eine Funktion mit mehreren Parametern zuerst nur mit einem Parameter ausgewertet und gibt dabei eine Funktion zurück, welche einen Parameter weniger benötigt. Dies wiederholt sich mit dieser Funktion, bis nur noch eine Funktion mit einem Parameter übrig bleibt. Diese konsumiert dann den letzten verbliebenen Parameter. Der Vorteil an dieser Vorgehensweise liegt insbesondere in der Nutzbarkeit von Funktion in Funktionen höherer Ordnung. Eine Funktion mit vielen Parametern kann beispielsweise so parametrisiert werden, dass sie nur noch einen Parameter zu konsumieren hat und dann der Map-Funktion übergeben wird. map (+ 1) [1,2,3,4] Ö [2,3,4,5] Programmbeispiel 3 Die Additionsfunktion wird mit einem der beiden nötigen Eingaben parametrisiert. Dabei entsteht eine Funktion, die nur eine Eingabe benötigt und dann in der mapFunktion genutzt werden kann. Diese führt dann die Addition mit 1 auf alle Elemente der Liste aus. 7 Kapitel 2: Grundlegende Sprachkonstrukte 2.1.5 Lambda-Ausdrücke & lokale Variablen Wie bereits bekannt, können in der reinen funktionalen Programmierung keine Variablen verändert werden. Allerdings gibt es Möglichkeiten, Variablen innerhalb einer Funktion zu erstellen und errechnete Werte an diese zu binden. Dazu existieren verschiedene Variationen. Lokale Variablen können hierbei mit let oder where definiert werden. Die interessanteren Variablendefinitionen ergeben sich allerdings durch die Lambda-Ausdrücke. Es werden Variablennamen für Parameter erzeugt, die nachträglich hinzugefügt werden. Diese lassen sich zwar auch direkt als Parameter referenzieren, jedoch existieren Fälle, wo diese Art von Deklaration dennoch sinnvoll oder nötig ist. Insbesondere bei monadischen Funktionen (vgl. Kap. 3.1) ergeben sich so Möglichkeiten, die übergebenen Parameter explizit zu referenzieren, da hier keine direkte Parametererstellung möglich ist. f = \x y -> let c = 4 in a * x + b * y + c where a = 2 b = a + 3 Programmbeispiel 4 Bei dem Beispiel werden also die zwei Parameter x und y durch den Ausdruck \x y -> als Lambda-Ausdrücke erstellt und im folgenden Funktionenrumpf genutzt. Dort werden noch mittels let eine Variable c und mittels where die beiden Variable a und b definiert [Pro08] . 2.2 Eigenheiten in Syntax und Semantik 2.2.1 Signaturen und Vererbung Eine Haskell-Funktion beginnt mit ihrer Signatur. In dieser wird festgelegt welche Typen von Variablen übergeben werden dürfen und welche als Rückgabe zu erwarten sind. Hierbei dürfen alle Arten von Parametern deklariert werden. Selbst die Verwendung von unbestimmten Parametern, die üblicherweise mit klein geschriebenen Buchstaben deklariert werden, ist erlaubt, was die Erstellung generischer Funktionen ermöglicht. Prinzipiell ist es zulässig, die Signatur wegzulassen und dem Compiler die 8 Kapitel 2: Grundlegende Sprachkonstrukte Signaturerstellung zu überlassen. Allerdings sollte im Sinne des guten Programmierstils dies weitestgehend vermieden werden. Haskell unterstützt ebenfalls Polymorphie, also die Nutzung von Funktionen mit unterschiedlichen Parametertypen. Umgesetzt wurde dies durch das Konzept der Typklassen. Hierbei werden mehrere Typen von Parametern zu einer Klasse zusammengefasst und der Funktion als Parameterdefinition in der Signatur übergeben. Damit wird die Definition von Funktionen für mehrere Typen gleichzeitig ermöglicht. Beispielsweise sind Int, Float, Double u. ä. zusammengefasst zu dem Konstrukt Num, auf dem alle trivialen numerischen Funktionen definiert sind [Bus02] . Neben Polymorphie ist ebenso direkte Vererbung in Haskell möglich. Es existieren zwei Möglichkeiten. Entweder wird explizit die konkrete Umsetzung der Funktionen der Oberklasse formuliert oder man überlässt dem Compiler die Umsetzung der Funktionen und erweitert die Oberklasse lediglich. Eine Enumeration kann beispielsweise die Struktur Eq implizit erweitern, welche Vergleichbarkeit von Elementen bereitstellt und so ihre eigenen Elemente untereinander vergleichbar macht. Dies kann auch explizit genutzt werden, wobei jedoch die Funktionalität der Oberklasse, also die Vergleichbarkeit, speziell für die erbende Klasse angepasst werden muss. 2.2.2 Namenskonventionen und Schreibweisen Haskell unterscheidet zwischen Groß- und Kleinschreibung. Dabei existieren strikte Konventionen, wie man einen Bezeichner zu beginnen hat. Ein groß geschriebener Bezeichner ist ausschließlich den Typen und Konstruktoren vorbehalten, während die klein geschriebenen Bezeichner für Variablen, Funktionen und Parameter stehen. Nützlich ist ebenso, dass die Schreibweise von Ausdrücken sowohl in Infix- als auch Präfixschreibweise unterstützt wird. Die Infixschreibweise, die man eher von mathematischen Operationen gewohnt ist, wird für symbolische Bezeichner ohne Zusätze unterstützt. Diese müssen bei Präfixschreibweise durch runde Klammern gekennzeichnet werden. Die Präfixschreibweise, welche man bei Funktionen gewohnt ist, wird ebenfalls ohne Zusätze unterstützt. Die Kennzeichnung bei Infixschreibweise ist durch hochgestellte Rückkommata gegeben. 9 Kapitel 2: Grundlegende Sprachkonstrukte x + y map f xs == (+) x y == f `map` xs Programmbeispiel 5 Die Funktion + kann also auch in Klammern angegeben werden und dann in Präfixschreibweise genutzt werden. Die map-Funktion wird durch die Rückkommata in der Infixschreibweise gekennzeichnet. Die Entscheidung der üblichen Schreibweise findet im Übrigen bei der Definition der Funktion statt. 2.2.3 Pattern Matching Insbesondere wichtig für die Struktur von Programmen ist in Haskell das so genannte „Pattern Matching“. Es impliziert, dass für jede Funktion mehrere unterschiedliche Funktionsköpfe, und damit auch Funktionsrümpfe, genutzt werden dürfen und der Interpreter entscheidet, welcher Funktionskopf zum Zeitpunkt der Auswertung genutzt wird. Die Signatur der Funktionen bleibt hierbei unberührt. Lediglich die Unterscheidung des Zustands des Parameters, z. B. bei Listen in leere und gefüllte Listen, determiniert die Ausführung des Rumpfes. map map f [] map f (x:xs) :: (a -> b) -> [a] -> [b] = [] = f x : map f xs Programmbeispiel 6 Bei der Ausführung wird die Liste der Definitionen von oben durchgegangen und die erste passende Signatur gewählt. Sollte also die Funktion mit einer leeren Liste parametrisiert werden, so greift die erste Signatur und es wird wiederum eine leere Liste zurückgegeben. Im Falle einer nicht leeren Liste greift dann die zweite Signatur. Sollte keine der Signaturen passen, so wird ein Fehler ausgegeben und das Programm bricht ab. 10 Kapitel 2: Grundlegende Sprachkonstrukte 2.2.4 Send More Money Das Beispiel „Send More Money“ möchte ich hier kurz als Beispielimplementierung besprechen, um zu zeigen, was mit Haskell möglich ist. module Money where import Control.Monad (guard) digs :: [Int] digs = [0..9] toint :: [Int] -> Int toint = foldl (\ i j -> 10*i+j) 0 solve :: [[Int]] solve = do s <- digs e <- digs ; guard (notElem e [s]) n <- digs ; guard (notElem n [s,e]) d <- digs ; guard (notElem d [s,e,n]) m <- [1..9]; guard (notElem m [s,e,n,d]) o <- digs ; guard (notElem o [s,e,n,d,m]) r <- digs ; guard (notElem r [s,e,n,d,m,o]) y <- digs ; guard (notElem y [s,e,n,d,m,o,r]) guard (toint [s,e,n,d] + toint [m,o,r,e] == toint [m,o,n,e,y]) return $ map toint[[s,e,n,d],[m,o,r,e],[m,o,n,e,y]] Programmbeispiel 7 Etwas genauer möchte ich auf die Funktion toint eingehen. Diese berechnet aus einer übergebenen Liste mit Zahlen die Gesamtrepräsentation, wobei jede Zahl in der Liste ihrer Stelle entsprechend um Zehnerpotenzen verschoben wird. Genutzt wird hierbei die Funktion foldl. Diese konsumiert eine Funktion mit zwei Parametern, ein initiales Ergebnis und eine Liste. Dabei wendet sie die Funktion auf das initiale Ergebnis und jeweils das erste Element der Liste an. Jenes geschieht rekursiv mit dem Rest der Liste, bis die Liste leer ist. Somit wird Elementweise das vorherige Ergebnis um eine Zehnerpotenz verschoben und das aktuelle Element dazu addiert. 11 Kapitel 2: Grundlegende Sprachkonstrukte In der Funktion solve werden mittels der guard-Funktionalität die nötigen Regeln aufgestellt, so dass das Programm selbstständig die Lösung des Problems sucht. Wichtig ist hierbei, dass jeder Buchstabe nicht als Buchstabe, sondern als Bezeichner für eine Liste mit möglichen Zahlen für diesen Buchstaben genutzt wird. Somit werden alle möglichen Zahlen jeweils in eine Liste geschrieben und dann durch den guard aussortiert, je nach Regel, die diesem übergeben worden ist. Zu beachten ist des Weiteren, dass diese Abfolge sequenziell abgearbeitet werden muss, damit auf die bereits vergebenen Zahlen Bezug genommen werden kann. Dies wird durch den DoBlock gelöst, welcher aus der Verwendung von Monaden stammt und Sequenzierung ermöglicht (Vgl. Kap. 3.1). Eine weiterführende Abhandlung findet sich in einer Ausgabe des „The Monad Reader“ [Dou08] . 12 Kapitel 3: Monaden 3 Monaden 3.1 Notwendigkeit von Monaden Aus der Umsetzung des rein funktionalen Programmierparadigma ergeben sich auch Schwierigkeiten. Dabei führt insbesondere der Verzicht auf Seiteneffekte zu verschiedenen Problemen. Zu diesen zählt z. B. die Realisierung von Ein- und Ausgaben. Des Weiteren ist eine individuelle Fehlerbehandlung sehr aufwendig umzusetzen. Maßgeblich ist auch die Art der Auswertung eine Problemquelle für einige Programmierkonstruktionen. Die Reihenfolge bei der Auswertung kann von essenzieller Wichtigkeit sein. Z. B. bei Eingaben kann sie aber durch Lazy Evaluation nicht garantiert werden (Vgl. Kap. 2.1.2). Aus diesen Gründen wurden in Haskell Monaden implementiert. Sie entstammen der mathematischen Kategorientheorie und bieten für das gegebene Problem eine adäquate Lösung. 3.2 Definition von Monaden Entlehnt aus der Mathematik ist eine Monade definiert als ein Tripel [Ste09] . Das Tripel besteht in erster Linie aus der Deklaration einer Monade. Diese gleicht der Deklaration eines neuen Datentyps. Hierbei wird mindestens eine Variable mitgeführt, welche den Wert der Berechnung einer Funktion trägt. Des Weiteren können allerdings auch noch zusätzliche Variablen mitgeführt werden, um weitere Informationen zu tragen. Es könnte z. B. die Fehlermeldung bei einer Fehlermonade sein. type <Name> a = Return a Programmbeispiel 8 Diese einfachste Art der Monade publiziert einen neuen Typ mit einem designierten Namen und deklariert die Variable a, welche das Ergebnis der Berechnung trägt. In diesem Fall kann die Monade ausschließlich das Ergebnis tragen und keinerlei zusätzliche Informationen. In einer Fehlermonade beispielsweise würde die Typdeklaration folgendermaßen aussehen: 13 Kapitel 3: Monaden data Fehler a = Return a | Raise String Programmbeispiel 9 Wie hier zu erkennen ist, wird entweder der Typ weitergereicht, oder die Fehlerbeschreibung. Im Falle eines Fehlers wird das Ergebnis der Funktion also verworfen. Die Typen Return und Raise werden hierbei als mögliche Typen für „Fehler“ deklariert und tragen jeweils eine Variable. Weiterhin zu beachten ist, dass die Deklaration mit dem Schlüsselwort data anstelle des Wortes type stattfindet. Das liegt daran, dass Typdeklarationen mittels type keine Wahl für ihre gespeicherten Daten lassen. Die Daten haben immer die gleiche Signatur. Dies ist bei der Datentypdeklaration mittels data anders, da dort verschiedene Arten von Daten in dem Konstrukt abgelegt werden können. Neben der Deklaration der Monade existieren zwei weitere Teile des Tripels. Der Zweite dient zur Erzeugung der Monade. Er wird meist mit dem Schlüsselwort return (klein geschrieben) benannt und sorgt dafür, dass das Ergebnis einer Berechnung innerhalb einer Funktion in einen Monadentyp verpackt wird. Die Funktion nimmt also einen Datentyp entgegen und erzeugt daraus einen Monadentyp, welcher den Datentyp beinhaltet. Dieser wird als Ergebnis der Funktion zurück gegeben. An dieser Stelle sei weiterhin auf die pure funktionale Programmierung verwiesen, welche mit dieser Art von Berechnung konsistent bleibt. Zu jedem Zeitpunkt, an dem diese Funktion aufgerufen wird, wird sie in gleicher Weise berechnet, vorausgesetzt die übergebene Variable trägt denselben Wert. Im Allgemeinen sieht die Signatur dieser Funktion wie folgt aus: return :: Monad m => a -> m a Programmbeispiel 10 (Entnommen aus Hugs.Prelude.hs) Die Funktion muss also aus einem gegebenen Datentyp a eine Monade vom Typ m mit dem Datentyp a erstellen. Dadurch, dass m durch das Konstrukt Monad m => von dem Typ Monad oder einer seiner Unterklassen sein muss, wird sichergestellt, dass eine Monade zurückgeliefert wird. Der dritte und letzte Teil der Definition einer Monade bezieht sich auf die Verknüpfung von Monaden. Wie oben bereits beschrieben ist die Ausführungsreihenfolge bei Monaden eine essenzielle Funktionalität. Daraus folgt die Notwendigkeit, dass eine 14 Kapitel 3: Monaden Monade weiß, wie sie sich zu verhalten hat, wenn sie mit anderen Monaden verknüpft wird. Es kann vorkommen, dass eine Monade das Ergebnis einer vorher bearbeiteten Monade benötigt. In unserem Beispiel der Fehlermonade würde bei der Auslösung eines Fehlerzustands der Fehler an die nachfolgende Monade weitergereicht werden. Die beiden Arten der Verknüpfung werden in Haskell folgendermaßen deklariert: (>>) :: m a -> m b -> m b (>>=) :: m a -> (a -> m b) -> m b Programmbeispiel 11 (Entnommen aus Hugs.Prelude.hs) Die obere Definition bezieht sich auf die einfache Verknüpfung, bei der kein Ergebnis weitergereicht wird. Die Definition besagt, dass eine Monade a und eine Monade b als Parameter übergeben werden und als Rückgabe die Monade b haben. Es bedeutet nichts anderes, als dass Monade a vor Monade b ausgeführt wird, allerdings keine Veränderung nach sich zieht und demnach, nach Ausführung der Monade b der Zustand nach Monade b herrschen muss [Hug98] . Die untere Definition ist ein wenig umfangreicher. Hierbei wird der Wert von der Monade a als Ergebnis an die Monade b weitergegeben. Daher muss es sich bei Monade b um eine parametrisierte Monade handeln. Dadurch ergibt sich, dass der Wert, den Monade a berechnet, direkt in die Berechnung von Monade b eingeht. Der Endzustand muss natürlich dem nach Monade b entsprechen. Auch hier ist das pure funktionale Programmierparadigma erhalten geblieben, trotz der Tatsache, dass Monade a direkten Einfluss auf Monade b hat. Dennoch würde in dieser Konstellation die Auswertung zu jedem Zeitpunkt gleich verlaufen. 3.3 Besonderheiten der Definition Insgesamt ist mit den drei Teilen der Definition eine Monade aus mathematischer Sicht vollständig deklariert. In Haskell gibt es allerdings noch eine zusätzliche Deklaration, die zu der Monadendefinition hinzugefügt wurde. Diese repräsentiert einen Fehler der Monade, welcher bei Auftreten weitergereicht wird. Allerdings ist der allgemeine Fall eines Fehlers bereits in der Grundimplementation der Klasse Monad eingebaut, so dass die explizite Definition des Fehlerfalls nicht in jeder neuen Monade deklariert werden muss. Die Implementierung des Fehlerfalls sieht folgendermaßen aus: 15 Kapitel 3: Monaden fail :: String -> m a fail s = error s Programmbeispiel 12 (Entnommen aus Hugs.Prelude.hs) Durch die Definition kann bei jeder Monade ein Fehler ausgelöst werden, wenn irgendetwas fehlschlägt. Es sei noch angefügt, dass das Konstrukt error ein allgemeines Fehlerkonstrukt ist, das den Interpreter zwingt, das Programm anzuhalten. Der Grund des Fehlers kann in dem nachfolgenden String angegeben werden. Allumfassend kann eine Monade also implementiert werden, indem man eine Typdeklaration (type) oder Datentypdeklaration (data), eine Deklaration für die Erstellung der Monade (return) und eine Deklaration für die Verknüpfung von Monaden (>>=) definiert. Zur letzten Deklaration bleibt zuletzt zu sagen, dass die Verknüpfung mit Übergabe der Parameter deklariert werden muss, da die Verknüpfung ohne Übergabe leicht emuliert werden kann. Dies ist ebenfalls bereits in der Grundimplementation von Monad realisiert und sieht wie folgt aus: p >> q = p >>= \ _ -> q Programmbeispiel 13 (Entnommen aus Hugs.Prelude.hs) Diese Deklaration bedeutet, dass das Ergebnis der Monade p an die Monade q weitergeben wird, allerdings an keine Variable gebunden wird und somit nicht relevant ist. Der Unterstrich ist eine sogenannte Wildcard und steht für Variablen, die für die weitere Berechnung nicht relevant sind [Hug98] . data Fehler a = Return a | Raise String instance Monad Fehler where Return a >>= k = k a Raise s >>= k = Raise s return = Return Programmbeispiel 14 Diese Beispielimplementation einer Fehlermonade verdeutlicht das gesamte Tripel. Zuerst wird die Datenstruktur deklariert mit zwei unterschiedlichen Zuständen. Darauf wird sie, erbend von der Monad-Klasse, mit den beiden nötigen Deklarationen zur Erzeugung und Verknüpfung von Monaden ausgestattet. Hierbei wird wieder das 16 Kapitel 3: Monaden Pattern Matching (vgl. Kap. 2.2.3) genutzt für die unterschiedlichen Zustände der Datenstruktur. 3.4 Regeln zu Monaden Da Monaden aus den mathematischen Theorien entlehnt sind, gibt es drei Axiome denen jede Monade genügen muss. Diese fassen die wichtigsten Eigenschaften, die eine Monade ausmacht, zusammen [Meh01] . Das erste Axiom postuliert, dass eine Monade, angehängt an eine return-Anweisung, der Monade selbst gleichen muss. return x >>= f = f x Programmbeispiel 15 Es besagt, dass return x, welches ja eine Monade mit dem Wert x erzeugt, ihren Wert x an die nachfolgende Monade weitergibt. Somit muss dies dem Konstrukt entsprechen, bei welchem man der nachfolgenden Monade selbst den Wert x übergibt. Das zweite Axiom ist eng verwandt mit dem ersten und besagt, dass eine Monade mit einer angehängten return-Anweisung der Monade selbst entsprechen muss. m >>= return = m Programmbeispiel 16 Die Monade m wird ausgeführt und reicht ihren Wert weiter an die returnAnweisung, welche aus dem Wert wieder eine Monade erstellt. Somit muss das Ergebnis der Monade m selbst gleichen. Zusammenfassend erzeugen das erste und das zweite Axiom ein neutrales Element return, welches sowohl rechts- als auch linksassoziativ ist und vollkommen neutral gegenüber den anderen Operationen. Das letzte Axiom bezieht sich auf die Reihenfolge der Auswertung von Monaden. Ausgangssituation sind drei verknüpfte Monaden. Dabei werden zwei Monaden zu einem Block zusammengefasst. Diese Zusammenfassung darf allerdings, unabhängig davon welche beiden Monaden zusammengefasst werden, nicht den Wert der Auswertung insgesamt verändern. 17 Kapitel 3: Monaden m1 >>= (m2 >>= m3) = (m1 >>= m2) >>= m3 Programmbeispiel 17 Die Monade m1 gibt ihren Wert an den Block (m2,m3) weiter. Dies muss dem Fall gleichen, dass der Block (m1,m2) seinen Wert an m3 weitergibt. Das gesamte Ergebnis muss also in jedem Fall gleich bleiben. 3.5 Arten von Monaden Es gibt in Haskell eine Vielzahl von Monaden, die für verschiedene Funktionalitäten eingesetzt werden. Hier werden die Wichtigsten kurz vorgestellt [Hug98] . 3.5.1 Die IO-Monade Die wichtigste Monade ist die für Ein- und Ausgabe zuständige IO-Monade. Sie ermöglicht es, dass man mit Haskell Ein- und Ausgabeströme behandeln kann, wie z.B. das Schreiben in und das Lesen aus Dateien. Dabei ist insbesondere die Reihenfolge der Abarbeitung wichtig. Durch die Nähe von Ein- und Ausgaben zu Systemstrukturen und den Umfang der Implementation wird an dieser Stelle auf eine tiefere Behandlung verzichtet und auf den Quellcode [MIO] verwiesen. 3.5.2 Die Zustandsmonade Eine weitere Funktion, welche Monaden in Haskell ermöglichen, ist die Realisierung von Programmzuständen durch Zustandsmonaden. Wie bereits erwähnt, ist die Umsetzung einer eigenen Fehlerbehandlung mit sehr viel Aufwand verbunden. Dies wird durch die Zustandsmonaden bedeutend vereinfacht. Aber nicht nur Fehlerbehandlung, sondern auch eigene Programmzustände lassen sich mit der Zustandsmonade abbilden. Die Zustandsmonaden lassen sich in zwei Gruppen klassifizieren. Die Erste entspricht der Maybe-Monade, die gekennzeichnet ist durch zwei Zustände. Diese sind der normale Zustand, welcher den Wert der Auswertung mit sich trägt und einen zweiten Zustand, welcher keine Werte trägt, sondern als solcher die Information beherbergt. 18 Kapitel 3: Monaden data Maybe a = Nothing | Just a deriving (Eq, Ord, Read, Show) instance Monad Maybe where Just x >>= k = k x Nothing >>= k = Nothing return = Just fail s = Nothing Programmbeispiel 18 (Entnommen aus Hugs.Prelude.hs) Der Zustand Just führt die Auswertung der Funktion mit sich, während der Zustand Nothing keine Variablen an sich gebunden hat. In diesem ist die Information der Auswertung nicht relevant. Die zweite Klasse von Zustandsmonaden entspricht in ihrer Form der Either-Monade und beherbergt ebenfalls zwei oder aber mehr Zustände. Neben dem Normalzustand, welcher den Wert der Auswertung trägt, sind noch einer oder mehrere andere Zustände vorhanden. Diese können beliebige Informationen mit sich tragen. Dies könnten z. B. Fehlermeldungen sein. data Either a b = Left a | Right b deriving (Eq, Ord, Read, Show) instance Monad Either where Left a >>= k = k a Right b >>= k = k b return = Left Programmbeispiel 19 (Entnommen aus Hugs.Prelude.hs) Die Monade trägt je nach Zustand unterschiedliche Informationen mit sich, welche auch von unterschiedlichem Typ sein können. Im Falle eines Einsatzes als Fehlermonade wäre also der Wert a im Zustand Left des Programmbeispiels 19 der normale Zustand. Bei Auftreten eines Fehlers würde eine Fehlermeldung an b gebunden und in den Zustand Right gewechselt [Her05] . 3.5.3 Listen als Monaden Selbst Listen als eine der elementarsten Sprachmöglichkeiten in Haskell sind durch Monaden definierbar. Zu beachten ist die Möglichkeit, dass Monaden auch rekursiv deklariert werden können. Die Monade kann also als Daten eines Zustands selbst auftreten. Dadurch ergibt sich die hypothetisch unendliche Struktur einer Liste, wobei die konkrete Ausprägung einer Liste dann mit einer leeren Liste zu beenden ist. 19 Kapitel 3: Monaden data [a] = [] | a : [a] deriving (Eq, Ord) instance Monad [ ] where (x:xs) >>= f = f x ++ (xs >>= f) [] >>= f = [] return x = [x] fail s = [] Programmbeispiel 20 In dieser Implementierung wird zuerst die übliche Syntax der Liste als Datenstruktur deklariert. Durch die Verknüpfungs- und die Erstellungsdeklaration werden die typischen Funktionen einer Liste geboten. Hierzu gehören im Speziellen die Abtrennung des ersten Elementes und die Ausführung einer Operation auf diesem. 3.6 Zusammenfassung Monaden bieten in Haskell eine sehr mächtige Möglichkeit, um die funktionale Programmierung durch sequentielle Folgen zu ergänzen. Dabei wird auf die Auswertungsart in all seinen Konsequenzen Rücksicht genommen und es bleibt der reine Gedanke des funktionalen Ansatzes von Haskell unberührt. Durch die Definition des Monadentripels bietet sich eine standardisierte Möglichkeit, neue Monaden zu erstellen und zu nutzen. Die Regeln, denen jede Monade genügen muss, komplettieren die Vorschriften, mit welchen sich diese als Möglichkeit der Sprache Haskell nutzen lassen. Die in diesem Kapitel genannten Beispiele zeigen einen kleinen Ausschnitt des Potentials von Monaden. 20 Kapitel 4: Haskell in der Praxis 4 Haskell in der Praxis Da Haskell im universitären Kontext entstanden ist, blieb bisher auch dort die wichtigste Zielgruppe von Nutzern. Haskell wird an verschiedenen Universitäten auf der ganzen Welt als funktionale Programmiersprache gelehrt. Laut einer Studie nutzen etwa 22% der amerikanischen und 11% der deutschen Universitäten Haskell in der Lehre. Hierbei ist eines der wichtigsten Ziele den Studenten eine andere Perspektive auf die Programmierung zu eröffnen. Allerdings wird Haskell auch gerne für formale Semantiken und die Lehre des Compilerbaus genutzt. Besonders erwähnt wäre an dieser Stelle, dass 85 % der Studenten die Haskell lernen bereits Programmiererfahrung besitzen, allerdings nur 23 % Haskell bereits kennen [HoH] . Literatur zu Haskell existiert hauptsächlich an Universitäten in Form von Publikationen, Papers und Kommentaren. Nichtsdestoweniger wurden, insbesondere nach dem Haskell 98 Report, eine Vielzahl von Lehrbüchern und Einführungen zu Haskell geschrieben. Durch die Standardisierung sind die meisten davon immer noch uneingeschränkt gültig, obwohl sie u. U. schon bis zu 10 Jahre alt sind. Bei Firmen und bei der Erstellung großer Softwareprogramme hat Haskell allerdings nie eine große Bedeutung erreichen können. Die zwei wichtigsten Projekte sind zum einen das frei verfügbare Versionierungswerkzeug „Darcs“ und zum anderen der Perl 6 – Interpreter/Compiler „Pugs“. Beide nutzen sehr erfolgreich die Möglichkeiten. Dabei bleibt die formale, mathematische Ausrichtung die größte Stärke von Haskell. Des Weiteren gibt es einige kleinere Firmen, die auf Haskell setzen und damit recht erfolgreich sind. Insbesondere ist eine Firma zu erwähnen, die ihren Focus in den Bereich der Kryptographie und der Hochsicherheitssoftware gelegt hat und mit Haskell beachtliche Erfolge erzielt. Die Gemeinschaft um die Sprache Haskell ist sehr aktiv und es gibt zahlreiche Workshops, digitale Fachzeitschriften wie „The Monad Reader“, Blogs, IRC Channel und Wettbewerbe mit Haskellteilnahme. Dabei schätzte man im Jahre 2005 den Kern der Haskellgemeinschaft auf 600 aktive Nutzer. 21 Kapitel 5: Fazit 5 Fazit Haskell bietet als Sprache viele mächtige Möglichkeiten um auf hohem Niveau Programme zu schreiben, die gut lesbar und gut testbar sind. Insbesondere die Nutzung eines Interpreters vereinfacht den Einstieg enorm. Die Idee der funktionalen Programmierung hält allerdings viele Nutzer davon ab, Haskell als vollständige Programmiersprache ernst zu nehmen und sich mit ihr auseinander zu setzen. Die andersartige Herangehensweise an die Programmierung schreckt viele potentielle Nutzer ab. Dabei sind zu Beginn einige Konstrukte recht unübersichtlich und als Anfänger sind komplexe Programme schwierig zu verstehen. Insbesondere die mächtigen Möglichkeiten wie Monaden, Currying und Lambda-Ausdrücke sind mit viel Mühe zu erlernen. Mit dieser Anfangsproblematik fertigzuwerden ist wohl das Hauptproblem von Haskell, welches dafür sorgt, dass der große Durchbruch ausbleibt. Daraus folgt, dass sich die Nutzung von Haskell in Produktivumgebungen kaum durchgesetzt hat. Nicht zu verachten ist, trotz des verhältnismäßig geringen Nutzungspotentials in der Praxis, der Einfluss, den Haskell auf andere Sprachen ausgeübt hat. So sind z.B. Lösungen und Ideen aus Haskell in Python, C# und Java adaptiert worden. So zeigt sich, dass die Ideen und Konzepte hinter Haskell weiter fortbestehen werden, selbst wenn Haskell eines Tages weiter aus dem Focus des Geschehens rückt [HoH] . Ich persönlich denke, dass die Sprache größere Beachtung verdient hätte und eine höhere Verbreitung, gerade wegen ihrer Andersartigkeit zu bekannten Programmiersprachen wie Java oder C++, ein besseres Gesamtverständnis für Programmierung schaffen würde. 22 Literaturverzeichnis Literaturverzeichnis [Dou08] Auclair, Doug. Monad Plus: What a Super Monad! The Monad.Reader. 2008, Issue 11. [Bus02] Buschmann, Christian. Abstrakte Datentypen und das Typsystem von Haskell. Fachbereich Informatik, FH Wedel. s.l. : Prof. Dr. A. Kolb, Prof. Dr. U. Schmidt, Prof. Dr. W. Ülzmann, 2002. Seminararbeit. [GHC09] GHC Team. The Glasgow Haskell Compiler. [Online] [Cited: April 8, 2009.] http://www.haskell.org/ghc/. [HC01] Haskell Community. Hackage DB. [Online] [Cited: April 8, 2009.] http://hackage.haskell.org/packages/archive/pkg-list.html. [Her05] Herrmann, Dr. Christoph. Funktionale Programmierung - Monaden. Fachbereich Informatik, Uni Passau. s.l. : Prof. Christian Lengauer, 2005. Vorlesung. [MIO] Jones, Mark P. -- Monadic I/O --. Hugs.Prelude.hs. 1999. [Hug98] Jones, Mark P. Hugs.Prelude.hs. 1999. [Jon02] Jones, Simon Peyton. Haskell 98 Language and Libraries - The Revised Report. Cambridge : s.n., 2002. [Man04] Manuel M.T. Chakravarty, Gabriele C. Keller. Einführung in die Programmierung mit Haskell. München : Pearson Studium, 2004. 3-8273-7137-6. [Meh01] Mehnle, Julian. Monaden. Fachbereich Informatik, TU München. s.l. : Prof. Dr. T. Nipkow, 2001. Seminararbeit. [Pro08] Prof. Dr. Robert Giegerich, Jens Stoye. Programmieren in Haskell. Technische Fakultät, Universität Bielefeld. s.l. : Prof. Dr. Robert Giegerich, 2008. Vorlesung. [Ste09] Ram, Stefan. Monaden. [Online] FU Berlin. [Cited: April 8, 2009.] userpage.fuberlin.de/~ram/pub/pub_jf47ht81Ht/monaden. [HoH] Simon Peyton Jones, Paul Hudak, John Hughes, Philip Wadler. A History of Haskell: Being Lazy with Class. 2007. [Pri09] Verschiedene. Haskell Prime. [Online] Haskell Community. [Cited: April 8, 2009.] http://hackage.haskell.org/trac/haskell-prime/. 23