Ausarbeitung - Institut für Wirtschaftsinformatik

Werbung
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
Herunterladen