Funktionale Programmierung - WWW-Docs for TU

Funktionale Programmierung - WWW-Docs for TU
Funktionale Programmierung
Wolfgang Jeltsch
23. November 2008
Inhaltsverzeichnis
1. Einführung
1.1. Programmierparadigmen . . . . . . . . . . . .
1.2. Geschichte der funktionalen Programmierung
1.3. Über dieses Dokument . . . . . . . . . . . . . .
1.4. Auf den Geschmack kommen . . . . . . . . . .
1.5. Werkzeuge . . . . . . . . . . . . . . . . . . . . .
2. Erste Schritte
2.1. Typen und Ausdrücke
2.2. Basistypen . . . . . . .
2.3. Funktionen . . . . . . .
2.4. Deklarationen . . . . .
2.5. Mehr zu Typen . . . .
2.6. Aufgaben . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3
3
3
4
5
5
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
6
6
7
9
13
16
18
3. Das Typsystem
3.1. Algebraische Datentypen
3.2. Klassen . . . . . . . . . . .
3.3. Sorten . . . . . . . . . . . .
3.4. Aufgaben . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
21
21
27
30
33
4. Nicht-strikte Semantik und verzögerte Auswertung
4.1. Vorüber gehende Notizen . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2. Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35
35
36
A. Verwendete Sonderzeichen
38
B. Zitate über Haskell
40
.
.
.
.
.
.
2
1. Einführung
1.1. Programmierparadigmen
Programmiersprachen lassen sich in zwei Klassen einteilen, nämlich in imperative und
deklarative Programmiersprachen. Während imperative Programme angeben, wie ein
gegebenes Problem gelöst werden soll, beschreiben deklarative Programme eher das
Problem selbst und überlassen das Wie zu weiten Teilen dem Compiler, Interpreter
oder Laufzeitsystem.
Zu den imperativen Programmiersprachen zählen objektorientierte Sprachen wie Java, C# und Python, prozedurale Programmiersprachen wie PASCAL und C, aber auch
exotischere Sprachen, wie die stapelorientierten Sprachen Forth und PostScript. Schließlich sind auch die Maschinencodes von von-Neumann-Rechnern und ihre zugehörigen
Assemblersprachen imperativen Programmiersprachen.
Zu den deklarativen Sprachen gehören funktionale Sprachen, logische Sprachen und
Constraintsprachen. Logikprogramme und Constraintprogramme enthalten logischen
Aussagen über bestimmte Unbekannte. Die Ausführung solcher Programme besteht im
Wesentlichen im Finden von Lösungen für diese Unbekannten. Während in logischen
Programmen als Daten nur Terme in Frage kommen, arbeitet die Constraintprogrammierung auch mit anderen Wertebereichen, wie z.B. dem der rationalen Zahlen.
Bei der funktionalen Programmierung stehen Funktionen (im mathematischen Sinne) im Vordergrund. Diese können als Daten behandelt werden, was das „Rechnen mit
Funktionen“ erlaubt. Zum Beispiel lässt sich die Funktionsverkettung ◦, die man aus
der Mathematik kennt, in einer funktionalen Sprache als Funktion definieren, welche
zwei Funktionen als Argumente erhält und eine Funktion als Resultat liefert. Der Steuerfluss wird typischerweise nicht explizit angegeben. Die Ausführung eines funktionalen
Programms besteht im Wesentlichen aus dem Auswerten von Ausdrücken.
Wenn das Ergebnis eines Ausdrucks vom aktuellen Systemzustand unabhängig ist
und das Auswerten des Ausdrucks keine Seiteneffekte bewirkt, spricht man von rein
funktionaler Programmierung. Verschiedene funktionale Programmiersprachen ermöglichen es, mit teilweise unbekannten Datenstrukturen zu rechnen. Diese werden als
nicht-strikte Programmiersprachen bezeichnet.
1.2. Geschichte der funktionalen Programmierung
Die Anfänge funktionaler Programmierung liegen in der zweiten Hälfte der 1950er Jahre, als die Programmiersprache LISP entwickelt wurde. Diese basiert auf dem λ-Kalkül,
einem von Alonzo Church und Stephen Kleene entwickelten Berechenbarkeitsmodell.
3
Ein weiterer wichtiger Meilenstein war die Entwicklung von ML im Jahr 1973 durch
Robin Milner. Ursprünglich Metasprache für den Theorembeweiser LCF,1 wurde ML
später als vollwertige Programmiersprache verwendet. Wichtiges Merkmal von ML
ist ein statisches, polymorphes Typsystem, welches Bezüge zu generischen Klassen
in Java hat. Von ML gibt es mittlerweile eine Reihe verschiedener Dialekte, wie z.B.
Standard ML (SML), Caml und Objective Caml (OCaml).
Im Jahr 1977 wurde John Backus der Turing Award verliehen. Anlässlich dieses Ereignisses hielt er eine Vorlesung mit dem Titel Can Programming be Liberated from the
von Neumann Style? [2]. In dieser wies er auf Probleme der imperativen Programmierung hin und propagierte einen funktionalen Programmierstil am Beispiel der von ihm
entwickelten Programmiersprache FP. Das motivierte etliche Wissenschaftler, auf dem
Gebiet der funktionalen Programmierung zu forschen.
Ende der 1970er Jahre begann auch die Arbeit an nicht-strikten funktionalen Programmiersprachen. Nahezu jedes Forschungsteam entwickelte seine eigene Sprache, was u.a.
zu den Programmiersprachen Miranda, Lazy ML und Clean führte. Aufgrund dieser
Zersplitterung wurde 1987 mit der Entwicklung einer einheitlichen nicht-strikten, rein
funktionalen Sprache begonnen. Diese wurde Haskell genannt.
Die erste Version von Haskell – Haskell 1.0 – wurde 1990 fertig gestellt, gefolgt von den
Versionen 1.1 bis 1.4 und schließlich Haskell 98. Haskell 98 ist nach wie vor die aktuelle
stabile Sprachversion, welche aber mittlerweile durch Addenda ergänzt wurde. Darüber
hinaus existiert eine Vielzahl von Spracherweiterungen, welche aktiv genutzt werden
und z.T. in naher Zukunft standardisiert werden sollen. Detailierte Informationen zur
Geschichte von Haskell (und auch von funktionaler Programmierung allgemein) gibt
der Artikel A History of Haskell: Being Lazy with Class [6].
1.3. Über dieses Dokument
In diesem Dokument soll die funktionale Programmierung am Beispiel von Haskell
vorgestellt werden. Haskell wurde gewählt, weil es viele innovative Ideen umsetzt und
darüber hinaus aktiv entwickelt und genutzt wird.
Die Darstellung von Haskell in diesem Dokument erhebt keinen Anspruch auf
Vollständigkeit. Eine genaue Spezifikation von Haskell 98 enthält der Haskell Report [12]. Spracherweiterungen sind z.B. im Benutzerhandbuch des Glasgow Haskell
Compilers [17] dokumentiert. Die Codebeispiele machen von verschiedenen Bibliotheken Gebrauch. Nicht alles, was einer Bibliothek entnommen wurde, wird hier explizit vorgestellt. Bibliotheksschnittstellen sind u.a. im Haskell Report und in der APIDokumentation der Haskell Hierarchical Libraries [18] beschrieben.
Um dem Anspruch schöner Typografie gerecht zu werden, wird für die Codebeispiele keine hässliche Monospace-Schrift, sondern vielmehr mathematischer Formelsatz
verwendet. Ermöglicht wird dies auf einfache Weise durch das Werkzeug lhs2TEX [5].
Dieses ersetzt auch z.T. ASCII-Symbolsequenzen durch ansprechendere Symbole. So
1
„ML“ steht für „meta language“.
4
wird z.B. der Konjunktionsoperator nicht && wie in echtem Haskell-Quelltext, sondern ∧ wie in der formalen Logik geschrieben. Eine Tabelle mit den vorgenommenen
Ersetzungen findet sich in Anhang A. Außerdem werden gelegentlich Bezeichner mit
Indizes wie z.B. num1 verwendet. Die Indexschreibweise dient nur der besseren Lesbarkeit, stellt also keine spezielle syntaktische Konstruktion dar. Statt num1 würde man in
echtem Haskell-Code num1 schreiben.
1.4. Auf den Geschmack kommen
Warum sollte man sich mit funktionaler Programmierung und Haskell beschäfigen?
Nun, da gibt es unterschiedliche Gründe. Vielleicht sucht man eine Sprache mit der man
kurze und klare Programme schreiben kann. Eine Sprache, deren Compiler einem dank
statischem Typsystem die meiste Fehlersucharbeit bereits abnimmt. Eine Sprache, die
aufgrund der klaren Separierung von Seiteneffekten viele Fehler gar nicht erst entstehen
lässt. Eine Sprache, mit der man aufgrund sehr abstrakter Ausdrucksmöglichkeiten
hochgradig wiederverwendbaren Code schreiben kann.
Will man einen Überblick über derartige Vorteile erhalten, sollte man sich vielleicht
den Drei-Stunden-Vortrag A Taste of Haskell und die dazugehörigen Folien [13] anschauen. Oder man liest den 24 Jahre alten, aber immernoch aktuellen Artikel von John
Hughes namens Why Functional Programming Matters [7].
1.5. Werkzeuge
Zum Arbeiten mit Haskell empfehle ich den Glasgow Haskell Compiler (GHC). Dieser
ist schon seit einiger Zeit kein reiner Compiler mehr, sondern enthält auch eine interaktive Umgebung namens GHCi. Diese erlaubt es, mit der Sprache, Bibliotheken und selbst
geschriebenem Code zu experimentieren, indem man Ausdrücke auswerten und Typen
ermitteln lässt. GHCi enthält außerdem einen Debugger. Manche hier vorgestellten Features werden auch nur vom GHC unterstützt. Für nähere Informationen zum Glasgow
Haskell Compiler siehe das GHC-Benutzerhandbuch [17] und Real World Haskell [11]!
5
2. Erste Schritte
2.1. Typen und Ausdrücke
2.1.1. Referentielle Transparenz
Wie in anderen Programmiersprachen auch, gibt es in Haskell Ausdrücke, welche einen
Wert (ein Ergebnis) besitzen. In Haskell ist dieser Wert allerdings unabhängig von
jeglicher Art Zustand. Das ist ein wesentlicher Unterschied zu herkömmlichen Programmiersprachen, vielleicht der wesentliche. Nehmen wir im Gegensatz dazu den
Java-Ausdruck stringStack.pop(). Dessen Ergebnis hängt vom aktuellen Zustand des
Objekts stringStack ab. Mehr noch, die Auswertung des Ausdrucks verändert diesen
Zustand.
In Haskell dagegen hat ein und derselbe Ausdruck bei ein und derselben Variablenbelegung auch ein und denselben Wert und die Auswertung des Ausdrucks hat keine
Seiteneffekte. Diese Eigenschaft bezeichnet man als referentielle Transparenz (engl. referential transparency). Sie macht Haskell zu einer rein funktionalen Programmiersprache (engl.
purely functional programming language). Referentielle Transparenz ist eigentlich nichts
besonderes. Man hat sie z.B. auch in der Mathematik.
Referentielle Transparenz beinhaltet auch, dass es keine Zuweisungen von Werten an
Variablen gibt. Variablen werden an Werte gebunden, aber solche Bindungen können
nicht im Nachhinein modifiziert werden. Das entspricht Konstanten in der imperativen
Programmierung und Variablen in der Mathematik. Ein Beispiel soll das illustrieren: Es
sei die Funktion f mittels ∀n ∈ N : f (n) = n2 definiert. Die Funktionsanwendung f (5)
ist äquivalent zu n2 mit der Bindung n = 5. Zwar kann an anderer Stelle f auf andere
Argumente angewendet und n damit an andere Werte gebunden werden, jedoch kann
ein Ausdruck wie n2 die Belegung von n nicht modifizieren, sondern nur nutzen.
Dank referentieller Transparenz spielt es keine Rolle, wann Ausdrücke ausgewertet werden. Das ermöglicht die Einführung von verzögerter Auswertung, welche wir in
Abschnitt ?? kennen lernen werden. Außerdem bewirkt referentielle Transparenz, dass
man Variablen durch die sie definierenden Ausdrücke ersetzen kann, ohne die Bedeutung des Programms zu verändern. In Sprachen wie Java ist das nicht möglich. Nehmen wir an die Variable top ist durch die Anweisung top = stringStack.pop(); definiert
worden. Der Ausdruck top + top liefert die Verkettung der vom Stapel genommenen
Zeichenkette mit sich selbst. Ersetzt man nun top durch stringStack.pop(), erhält man
stringStack.pop() + stringStack.pop() was zwei Zeichenketten vom Stapel nimmt und die
Verkettung dieser beiden Zeichenketten liefert.
Referentielle Transparenz vereinfacht die Codeoptimierung durch den Compiler, wozu auch das Parallelisieren von Berechnungen gehört. Weiterhin erleichtert sie das Um-
6
strukturieren von Code, das Führen von Korrektheitsbeweisen und das automatisierte
und manuelle Testen. Sie erhöht auch die Verständlichkeit des Codes, da man nicht berücksichtigen muss, in welchem Zustand ein bestimmter Ausdruck ausgewertet wird.
Schließlich eröffnet sie neue Wege für die Formulierung nebenläufiger Prozesse in Form
von sicherem Softwaretransaktionsspeicher (engl. Software Transactional Memory).
2.1.2. Statische Typisierung
Haskell ist eine statisch typisierte Programmiersprache. Das heißt, dass Typprüfung
komplett zur Compilierzeit stattfindet.1 Wird ein Programm erfolgreich compiliert, ist
damit die Abwesenheit einer bedeutenden Klasse von Programmierfehlern nachgewiesen. Das ist ein wesentlicher Vorteil bei der Entwicklung korrekter, sicherer und
zuverlässiger Software.
Besitzt ein Ausdruck e einen Typ τ, schreibt man dafür e :: τ. Eine solche Aussage wird
Typurteil (engl. type judgment) genannt.
2.1.3. Bezeichner
Bezeichner beginnen mit einem Buchstaben oder einem Unterstrich (_), gefolgt von
beliebig vielen Buchstaben, Ziffern, Unterstrichen und Apostrophen.Ein einzelner Unterstrich ist aber nicht als Bezeichner erlaubt, da er eine besondere Bedeutung hat2 .
Haskell unterscheidet zwischen zwei Arten von Bezeichnern, denen, die mit einem
Kleinbuchstaben oder Unterstrich beginnen, und denen, die mit einem Großbuchstaben beginnen. Jede der beiden Arten ist bestimmten Sprachkonstrukten zugeordnet.
So müssen Variablenbezeichner mit einem Kleinbuchstaben oder Unterstrich beginnen, während Typbezeichner wie Integer immer mit einem Großbuchstaben anfangen.
Die Unterscheidung der beiden Bezeichnerarten macht in verschiedenen Situation reservierte Wörter überflüssig und ermöglicht dadurch z.T. die sehr „leichtgewichtige“
Syntax von Haskell.
2.2. Basistypen
2.2.1. Zahlen
Der Typ Integer ist der Typ aller ganzen Zahlen. Eine Begrenzung des Wertebereichs
auf prozessorfreundliche Verhältnisse findet hier also nicht statt. Die Größe der Zahlen
ist lediglich durch den verfügbaren Speicherplatz begrenzt. Will man die leichten Effizienzeinbußen von Integer nicht in Kauf nehmen, kann man den Typ Int verwenden.
Dieser umfasst alle ganzen Zahlen in einem implementierungsabhängigen Intervall mit
garantiertem Mindestumfang, welches i.d.R. an die verwendete Prozessorarchitektur
angepasst ist. Außer den Ganzzahltypen existieren noch die Typen Float und Double für
1
Tatsächlich existiert auch eine Erweiterung um dynamische Typisierung, aber da Haskells Typsystem
sehr ausdrucksstark ist, muss man fast nie von ihr Gebrauch machen.
2
Siehe Unterabschnitt 3.1.4!
7
Gleitkommazahlen. Weiterhin gibt es Typen für rationale und komplexe Zahlen. Diese
werden in Abschnitt ?? diskutiert.
Es existieren Literale für ganze und gebrochene Zahlen. Diese entsprechen im Wesentlichen den Literalen anderer gängiger Programmiersprachen wie Java. Ein wichtiger
Unterschied ist, dass es keine Literale für negative Zahlen gibt. Der Ausdruck −1 ist
beispielsweise kein Literal, sondern die Anwendung des Negationsoperators3 auf das
Literal 1.
2.2.2. Zeichen
Der Typ Char umfasst alle Unicode-Zeichen [1]. Zeichenliterale entsprechen im Wesentlichen denen in Java.
2.2.3. Wahrheitswerte
Weiterhin gibt es den Typ Bool mit den Wahrheitswerten False und True. Für Wahrheitswerte gibt es z.B. die Operationen ¬, ∧ und ∨. Außerdem gibt es Ausdrücke der Form
if e then e1 else e2 . Deren Ergebnis ist das Resultat von e1 , falls das Ergebnis von e True
ist, und das Resultat von e2 , falls es False ist.4
2.2.4. Tupel
Unterstützung für Tupel ist in Haskell eingebaut. Ein Tupel kann so geschrieben werden,
wie man es aus der Mathematik kennt. So ist z.B. (1.5, 3) ein Tupelausdruck. Tupel haben
Typen der Form (τ1 , . . . , τn ), wobei n die Anzahl der Tupelkomponenten ist und τ1 bis
τn die Typen der einzelnen Tupelkomponenten sind.
Die mögliche Anzahl von Tupelkomponenten ist laut Haskell Report nach oben unbegrenzt. Allerdings setzt der Glasgow Haskell Compiler ein Limit, welches aber so
hoch ist, dass es praktisch nie erreicht wird. Es gibt in Haskell keine 1-Tupel, jedoch
existiert ein 0-Tupel. Dieses wird mit () bezeichnet und Unit genannt. Sein Typ heißt
ebenfalls (). Der Unit-Typ spielt eine ähnliche Rolle wie Javas void-Typ.
2.2.5. Listen
Listen werden direkt in Haskell unterstützt. Die leere Liste wird [ ] geschrieben. Nichtleere Listen können durch die kommaseparierte Folge ihrer Elemente, eingeschlossen in
eckige Klammern, dargestellt werden. Der Ausdruck [2, 3, 5, 7] stellt also die Liste der
ersten vier Primzahlen dar. Für jeden Typ τ ist [τ] der Typ der Listen, deren Elemente
vom Typ τ sind.
Der Operator : fügt ein Element vorn an eine Liste an und der Operator ++ verkettet zwei Listen. Die Ausdrücke 2 : [3, 5, 7] und [2, 3] ++ [5, 7] sind also äquivalent zu
3
4
Siehe Unterabschnitt 2.3.3!
Man beachte, dass es sich hier nicht um if-then-else-Anweisungen handelt, da es hier überhaupt nicht
um Steuerfluss geht! Es handelt sich um Ausdrücke ähnlich der Ausdrücke der Form e ? e1 : e2 in Java.
8
[2, 3, 5, 7]. Sowohl : als auch ++ ist rechtsassoziativ. Zu beachten ist, dass der Zeitaufwand für eine Listenverkettung linear mit der Größe der ersten Liste steigt.5
Listen von Zahlen6 können auch mittels arithmetischer Folgen (engl. arithmetic sequences) formuliert werden. Im einfachsten Fall gibt man einen Anfangs- und einen
Endwert an, wie in dem Ausdruck [0 . . 9], welche die Liste der ersten zehn natürlichen Zahlen liefert. Ist der Anfangswert größer als der Endwert, ergibt sich die leere
Liste. Man kann auch eine andere Schrittweite wählen, wie z.B. in [0, 2 . . 9], welches
für [0, 2, 4, 6, 8] steht, und [9, 7 . . 0], was [9, 7, 5, 3, 1] ergibt. Die Schrittweite wird also
jeweils durch die Differenz der ersten beiden Zahlen angegeben.
Weiterhin können Listen durch Listenkomprehensionen (engl. list comprehensions) dargestellt werden. Eine Listenkomprehension hat die Struktur [e0 | q1 , . . . , qn ], wobei e ein
Ausdruck ist und q1 bis qn Qualifizierer (engl. qualifiers) sind. Qualifizierer sind entweder
Generatoren (engl. generators) oder Wächter (engl. guards).7 Ein Generator hat die Form
x ← e. Er bewirkt, dass die Variable x die Elemente der Liste e durchläuft. Ein Wächter
ist ein boolescher Ausdruck, der Einschränkungen für die durch Generatoren eingeführten Laufvariablen vorgibt. Der Ausdruck e0 in [e0 | q1 , . . . , qn ] bestimmt, wie aus den
Laufvariablen die Elemente der Resultatliste gebildet werden.
Ein Beispiel für eine Listenkomprehension ist der Ausdruck
[(num1 , num2 , num) | num1 ← [1 . . 10],
num2 ← [num1 . . 10],
num ← [1 . . 10],
num1 ↑ 2 + num2 ↑ 2 ≡ num ↑ 2].
Dieser liefert die Liste aller pythagoräischen Zahlentripel, deren Komponenten kleiner
oder gleich 10 sind (wenn auch nicht auf sehr effiziente Weise).
Zeichenketten sind in Haskell Listen von Zeichen, also Werte vom Typ [Char]. Die
berühmte Hallo-Welt-Zeichenkette kann damit [’H’, ’a’, ’l’, ’l’, ’o’, ’ ’, ’W’, ’e’, ’l’, ’t’, ’!’] geschrieben werden. Weil das nicht sehr schön aussieht, gibt es Zeichenkettenliterale, die
denen von Java ähneln. Man kann also auch "Hallo Welt!" schreiben.
2.3. Funktionen
2.3.1. Grundlegendes
Funktionen sind natürlich zentral in einer funktionalen Programmiersprache. Man beachte, dass aufgrund der referentiellen Transparenz Haskell-Funktionen mathematischen Funktion entsprechen! Funktionen in Haskell „machen“ also nicht irgendetwas,
5
Der Grund dafür wird in Unterabschnitt 3.1.5 deutlich werden.
Tatsächlich müssen die Elemente nicht unbedingt Zahlen sein, sondern allgemein Werte, deren Typ eine
Instanz der Klasse Enum ist. Dadurch funktioniert z.B. auch [’a’ . . ’z’], welches die Liste aller lateinischen
Kleinbuchstaben liefert. Für eine Diskussion von Klassen siehe Abschnitt 3.2!
7
Tatsächlich gibt es außerdem noch Deklarationsgruppen, aber diese werden wir erst in Unterabschnitt 2.4.4 behandeln.
6
9
sondern bilden nur Argumente auf Resultate ab. Wesentliches Merkmal jeder funktionalen Programmiersprache ist die Eigenschaft, dass Funktionen „Bürger erster Klasse“
(engl. first class citizens) sind. Damit ist gemeint, dass Funktionen ganz normale Werte
wie Zahlen oder Zeichenketten sind. Sie können Teil von Datenstrukturen sein sowie
als Argumente oder Resultate von Funktionen vorkommen. Das ist eine sehr mächtige Eigenschaft, die viele elegante Problemlösungen ermöglicht. Funktionen, welche
wiederum Funktionen als Argumente erhalten oder als Resultate liefern, nennt man
Funktionen höherer Ordnung.
Da eine Funktion auch nur ein Wert ist, muss sie natürlich einen Typ haben. Sind
τ und τ0 Typen, so ist τ → τ0 der Typ aller Funktionen, deren Argumente vom Typ
τ und deren Resultate vom Typ τ0 sind. Außerdem muss es auch Ausdrücke geben,
die Funktionen konstruieren. Das sind die λ-Ausdrücke. Ein λ-Ausdruck hat die Form
λx → e, wobei x eine Variable und e ein Ausdruck ist. Er stellt die Funktion dar, die für
jedes x das Resultat e liefert, wobei x in e vorkommen und dadurch die Abhängigkeit
des Resultats vom Argument bewirken kann. Ein Beispiel für einen λ-Ausdruck ist
λnum → 2 ∗ num, welcher die Verdoppelungsfunktion repräsentiert.
Eine Funktionsanwendung wird durch Hintereinander-Schreiben der Funktion und
des Arguments dargestellt. Zum Beispiel liefert length "Hallo!" die Länge der Zeichenkette "Hallo!", also 6. Man kann Funktionsanwendung auch wie in der Mathematik
und vielen Programmiersprachen durch das Setzen von Klammern um das Argument
ausdrücken wie z.B. in length ("Hallo!"). Allerdings haben diese Klammern dann nichts
mit Funktionsanwendung zu tun, sondern legen lediglich die Auswertungsreihenfolge
wie in (4 + 5) ∗ 7 fest. Sie sind also oft einfach überflüssig. Sie trotzdem zu setzen, gilt
als schlechter Stil.
Um den Wert der Anwendung eines λ-Ausdrucks auf ein Argument zu berechnen,
wird β-Reduktion verwendet. β-Reduktion transformiert einen Ausdruck (λx → e0 ) e in
den Ausdruck e0 [e/x].
2.3.2. Currying
Funktionen in Haskell haben immer genau ein Argument. Das ist eigentlich auch nichts
besonderes, denn in der Mathematik ist es streng genommen genauso. Für die Additionsfunktion auf ganzen Zahlen gilt z.B. +Z : Z × Z → Z. Sie erhält also jeweils ein
Argument aus der Menge Z × Z, also ein geordnetes Paar ganzer Zahlen. Man könnte
die Idee der Mathematiker übernehmen und mehrstellige Funktionen durch Funktionen mit Tupelargumenten ausdrücken. Die besagte Additionsfunktion hätte dann den
Typ (Integer, Integer) → Integer.
Das wird aber nicht getan.8 Stattdessen wird eine Technik verwendet, die als Currying9
8
Eine konsequente Anwendung dieser Methode ist auch gar nicht möglich. Wie wir später sehen werden,
wird ein Tupel selbst durch eine bestimmte Funktion – einen sogenannten Tupelkonstruktor – erzeugt.
Dieser muss die Tupelkomponenten als Argumente erhalten. Würde man nun mehrere Argumente als
Tupel übergeben, müsste der Konstruktor, um ein Tupel zu erzeugen, bereits ein Tupel als Argument
erhalten. Die Katze würde sich also in den Schwanz beißen.
9
Die Bezeichnung „Currying“ leitet sich von dem Namen des Logikers Haskell Curry ab, nach dem
10
bekannt ist. Statt einer Funktion f :: (τ1 , τ2 , . . . , τn ) → τ verwendet man die daraus
abgeleitete Funktion
λx1 → (λx2 → (. . . → (λxn → f (x1 , x2 , . . . , xn )) . . .))
vom Typ
τ1 → (τ2 → (. . . → (τn → τ) . . .)),
welche wir im Folgenden mit f 0 bezeichnen wollen. Wendet man diese Funktion auf ein
erstes Argument e1 an, erhält man nach β-Reduktion wieder eine Funktion, nämlich
λx2 → (. . . → (λxn → f (e1 , x2 , . . . , xn )) . . .),
welche den Typ
τ2 → (. . . → (τn → τ) . . .)
besitzt. Diese kann dann auf das zweite Argument angewendet werden, erzeugt eine
Funktion, die auf das dritte Argument angewendet werden kann usw. Nach dem sukzessiven Anwenden auf alle Argumente erhält man das eigentliche Resultat. Es gilt also
für Ausdrücke e1 :: τ1 bis en :: τn :
(. . . (( f 0 e1 ) e2 ) . . .) en = f (e1 , e2 , . . . , en )
Wie man sieht, verwenden wir jetzt für die Behandlung mehrerer Argumente keine
Tupel mehr.
Die Verwendung von Currying wird in Haskell syntaktisch unterstützt. Funktionsanwendung ist linksassoziativ, weshalb statt (. . . ((e e1 ) e2 ) . . .) en einfach e e1 e2 . . . en
geschrieben werden kann. λ-Ausdrücke erstrecken sich soweit wie möglich nach rechts.
Daher kann man statt
λx1 → (λx2 → (. . . → (λxn → e0 ) . . .))
auch
λx1 → λx2 → . . . → λxn → e0
schreiben. Für letztgenannten Ausdruck existiert dann wiederum die abkürzende Notation
λx1 x2 . . . xn → e0 .
Außerdem ist der Operator → rechtsassoziativ, sodass τ1 → (τ2 → (. . . → (τn → τ) . . .))
zu τ1 → τ2 → . . . → τn → τ vereinfacht werden kann.
Als Beispiel wollen wir nun mittels Currying eine Funktion mean darstellen, die das
arithmetische Mittel zweier Zahlen darstellt. Diese ist
λnum1 → λnum2 → (num1 + num2 ) / 2.
auch Haskell sowie die funktional-logische Programmiersprache Curry [4] benannt sind. Allerdings
wurde Currying als erstes von Moses Schönfinkel entdeckt, weswegen Peter Thiemann auch von „voll
geschönfinkelten Funktionen“ spricht [19, S. 155].
11
Will man nun beispielsweise das arithmetische Mitteln von 1.5 und 3.1 berechnen lassen,
wendet man zunächst diese Funktion auf 1.5 an, was durch den Ausdruck
(λnum1 → (λnum2 → (num1 + num2 ) / 2)) 1.5
dargestellt wird. β-Reduktion liefert (nach Entfernen der äußeren Klammern) den Ausdruck
λnum2 → (1.5 + num2 ) / 2.
In einem zweiten Schritt wird dieser auf 3.1 angewendet, was zu
(λnum2 → (1.5 + num2 ) / 2) 3.1
und mittels β-Reduktion zu
(1.5 + 3.1) / 2
führt. Dieser Ausdruck wird dann zu 2.3 vereinfacht.
Currying ermöglicht partielle Anwendungen (engl. partial applications) von Funktionen.
Eine Funktion, die wie mean als zweistellige Funktion gedacht ist, kann, da sie tatsächlich
einstellig ist, auf nur ein Argument angewendet werden. So liefert dann z.B. mean 0 die
Funktion λnum → (0 + num) / 2 = λnum → num / 2, also die Halbierungsfunktion.
Ein anderes Beispiel ist das Anwenden der Additionsfunktion auf 1, was dann die
Nachfolgerfunktion ergibt. Allgemein kann eine als n-stellig gedachte Funktion auf
weniger als n Argumente angewendet werden und man erhält eine Spezialisierung
der ursprünglichen Funktion durch Festlegen eines Teils ihrer Argumente. Partielle
Anwendungen sind eine beliebte Technik in der funktionalen Programmierung und
ermöglichen oft sehr elegante Formulierungen.
2.3.3. Operatoren
Der Negationsoperator ist der einzige Präfixoperator in Haskell. Das Negieren mittels
dieses Operators ist lediglich syntaktischer Zucker für die Anwendung der vordefinierten Funktion negate. Postfixoperatoren existieren in Haskell nicht.
Haskell erlaubt die Definition von Infixoperatoren10 durch den Programmierer in
ähnlicher Weise wie das Einführen neuer Bezeichner. Auch Standardoperatoren wie +
und ∗ sind kein fester Bestandteil der Sprache, sondern in Haskells Standardbibliothek
definiert.
Operatoren unterscheiden sich von Bezeichnern durch die verwendbaren Zeichen.
Die meisten Zeichen, die nicht in Bezeichnern verwendet werden können, sind als Bestandteil von Operatoren erlaubt. Nicht erlaubt sind allerdings Zeichnenfolgen, welche
eine besondere syntaktische Bedeutung haben. So ist z.B. das Zeichen | in Operatoren
erlaubt, aber | an sich ist kein gültiger Operator, da es Teil der Syntax für Listenkomprehensionen ist. Diese Einschränkung ist nicht verwunderlich. Die verbotenen
Symbolfolgen sind für Operatoren das, was reservierte Wörter für Bezeichner sind.
10
Wenn im Folgenden von Operatoren die Rede ist, sind immer Infixoperatoren gemeint.
12
Auch Operatoren werden wie Bezeichner in zwei Arten eingeteilt. Operatoren, die
mit einem Doppelpunkt beginnen, entsprechen den Bezeichnern, die mit einem Großbuchstaben beginnen, und alle anderen Operatoren den anderen Bezeichnern. Der Doppelpunkt ist sozusagen das einzige „Großsymbol“.
Für jeden Operator bezeichnet () die Funktion, die durch den Operator dargestellt
wird. Damit steht z.B. (+) für die Additionsfunktion und es kann (+) 1.5 3.1 statt
1.5 + 3.1 geschrieben werden. Umgekehrt lässt sich jeder Bezeichner einer zweistelligen
Funktion in einen Operator umwandeln, indem er in Backquotes (‘)11 eingeschlossen
wird. So kann beispielsweise statt mod 5 4 auch 5 ‘mod‘ 4 geschrieben werden.
Weiterhin können Operatorschnitte (engl. sections) gebildet werden. Ist ein Operator
und e ein Ausdruck, so sind (e) und (e) Schnitte. (e) steht für λx → e x und (e) für
λx → x e. Damit ist beispielsweise (1/) die Bildung des Reziproken12 und (<0) der Test
auf Negativität. Bezüglich des Operators − gibt es eine Ausnahme. Zwar ist − als Infixoperator zulässig (und wird auch als solcher verwendet), da − aber auch Präfixoperator
ist, ist (−e) kein Schnitt, sondern eine geklammerte Präfixoperatoranwendung.13
2.4. Deklarationen
2.4.1. Lokale Deklarationen
Variablen können mittels let-Ausdrücken gebunden werden. Als Beispiel soll der Ausdruck
let mean = λnum1 num2 → (num1 + num2 ) / 2 in (mean 1.5 3.1, mean 4.0 2.7)
dienen. Der Wert dieses Ausdrucks ist der Wert des Ausdrucks hinter in, wobei die
Variable mean entsprechend der Definition hinter let gebunden ist.
Mehrere Definitionen innerhalb eines let-Ausdrucks sind möglich. Der folgende Ausdruck stellt ein Paar aus einem arithmetischen und einem geometrischen Mittelwert dar:
let
arithmeticMean = λnum1 num2 → (num1 + num2 ) / 2
geometricMean = λnum1 num2 → sqrt (num1 ∗ num2 )
in (arithmeticMean 1.5 3.1, geometricMean 4.0 2.7)
Woher weiß der Haskell-Compiler, wo die Definition von arithmeticMean aufhört
und die von geometricMean anfängt? Es weiß es anhand des Layouts. Die Position von
Zeilenumbrüchen und die Tiefe von Einrückungen hat einen Einfluss auf die Bedeutung
von Haskell-Code. Das trägt zu der bereits erwähnten „leichtgewichtigen Syntax“ bei.
Die genauen Layout-Regeln findet man im Haskell Report. Meistens erhält man das
11
Ein Backquote oder Backtick ist kein Apostroph, sondern ein Gravis.
Hierfür existiert allerdings auch die Funktion recip.
13
Solche Unregelmäßigkeiten haben bereits wiederholt zu Forderungen geführt, den Negationsoperator
abzuschaffen.
12
13
korrekte Layout, indem man seinen Code „sinnvoll“ formatiert, also etwas macht, was
man sowieso tun sollte.
Für let-Ausdrücke und alle anderen Konstruktionen, die Layout verwenden, gibt es
jeweils eine layout-unabhängige Alternativschreibweise mit geschweiften Klammern
und Semikola, die natürlich ebenfalls im Haskell Report beschrieben ist.
2.4.2. Rekursion
Die in einem let-Ausdruck definierten Bezeichner können in den rechten Seiten der
Definitionsgleichungen verwendet werden. Auf diese Weise wird Rekursion ermöglicht.
Beispielsweise steht der Ausdruck
let
fib = λnum → if num < 2 then num else fib (num − 1) + fib (num − 2)
in fib 10
für die zehnte Fibonacci-Zahl.
2.4.3. Syntaktischer Zucker
Für Variablendeklarationen existieren zusätzliche syntaktische Konstruktionen. Wir demonstrieren diese anhand der Definition einer Funktion roots, welche die reellen Wurzeln reeller quadratischer Gleichungen berechnet:
roots linCoeff constCoeff | discr < 0 = [ ]
| discr ≡ 0 = [center]
| discr > 0 = [center − discrRoot, center + discrRoot] where
center
= negate linCoeff / 2
discr
= center ↑ 2 − constCoeff
discrRoot = sqrt discr
Diese ist zu folgender Deklaration äquivalent:
roots = λlinCoeff constCoeff → let
center
= negate linCoeff / 2
discr
= center ↑ 2 − constCoeff
discrRoot = sqrt discr
in if discr < 0
then [ ]
else if discr ≡ 0
then [center]
else if discr > 0
then center − discrRoot :
center + discrRoot : [ ]
else ⊥
14
Hierbei steht ⊥für einen Ausdruck, dessen Auswertung einen Laufzeitfehler darstellt.
Allgemein ist folgendes fest zu stellen:
• Wird eine Funktion durch einen λ-Ausdruck definiert, so kann man die Argumentvariablen auf die linke Gleichungsseite verschieben und die rechte Gleichungsseite
durch die rechte Seite des λ-Ausdrucks ersetzen.
• Es können mittels Wächter (engl. guards) verschiedene Alternativen unterschieden
werden. Die Bedingungen für die einzelnen Alternativen werden als Ausdrücke
vom Typ Bool formuliert. Die erste Alternative, deren Bedingung erfüllt ist, wird
genommen. Es ist ein Laufzeitfehler, wenn alle Bedingungen falsch sind.
• In einer where-Klausel können lokale Deklarationen angegeben werden. Deren
Wirkungsbereich erstreckt sich über alle Alternativen, inklusive der Bedingungen.
Erwähnt werden soll noch, dass es eine vordefinierte Konstante otherwise gibt, welche
äquivalent zu True ist. Diese ist für die Verwendung als letzte Bedingung in Fallunterscheidungen mit Wächtern gedacht, um einen Sonst-Fall zu realisieren. Mittels otherwise
lässt sich z.B. die Fibonacci-Funktion mittels
fib num | num < 2 = num
| otherwise = fib (num − 1) + fib (num − 2)
definieren.
2.4.4. Deklarationen in Listenkomprehensionen
Ein Qualifizierer in einer Listkomprehension kann neben einem Generator und einem
Wächter auch eine Gruppe von Deklarationen sein. Eingeleitet wird eine solche Gruppe
von einem let, welches von den eigentlichen Deklarationen gefolgt wird. Durch solche
Deklarationen gebundene Variablen können in allen folgenden Qualifizierern sowie in
dem links von | stehenden Ausdruck verwendet werden.
2.4.5. Module
Haskell-Programme gliedern sich in Module. Der Quelltext jedes Moduls wird typischerweise in einer separaten Datei abgelegt. Module dienen der Gliederung des Codes, dem
Einführen von Namensräumen sowie dem Verbergen von Implementierungsdetails
(Information-Hiding).
Um einen Eindruck von Haskells Modulsystem zu erhalten, betrachten wir das folgende kleine Modul:
module Combinatorics (
factorial,
binomCoeff
) where
15
import Data.List as List
factorial num
= rangeProduct 1 num
binomCoeff total sel = rangeProduct (total − sel + 1) total ‘div‘ factorial sel
rangeProduct from to = List.product [from . . to]
Das Modul trägt den Namen Combinatorics und exportiert zwei Funktionen, factorial
und binomCoeff . Es importiert das Modul Data.List, welches Unterstützung für Listen
bietet, und gibt ihm lokal den kürzeren Namen List. Es folgen Funktionsdeklarationen
analog zu lokalen Deklarationen in let-Ausdrücken. Da rangeProduct nicht in der Exportliste auftaucht, ist es nur lokal verwendbar. In der Definition von rangeProduct befindet
sich der qualifizierte Bezeichner (engl. qualified identifier) List.product. Dieser steht für die
Funktion product aus dem lokal unter dem Namen List bekannten Modul. Eine Qualifizierung mit einem Modulnamen kann weggelassen werden, wenn der Bezeichner nicht
in verschiedenen importierten Modulen für verschiedene Dinge steht.
Das Modul Prelude ist ein Modul mit Basisdeklarationen, welches von jedem anderen
Modul implizit importiert wird. Tatsächlich ist die Funktion product auch im Prelude
enthalten, sodass der Import von Data.List in obigem Beispiel unnötig ist.
2.4.6. Fixity-Deklarationen
Die Priorität und Assoziativität von Operatoren, auch von solchen, die mittels Backquotes gebildet wurden, kann über Fixity-Deklarationen fest gelegt werden. Eine FixityDeklaration beginnt mit einem der reservierten Wörter infixl, infixr und infix, welche
für linkassoziative, rechtsassoziative und nicht-assoziative14 Infixoperatoren stehen.
Darauf folgt die Angabe einer Priorität im Bereich 0 bis 9 und anschließend eine kommaseparierte Liste von Operatoren. Ein Beispiel für eine Fixity-Deklaration ist infixl 6 +, −,
welche Priorität und Assoziativität für Addition und Subtraktion fest legt.
Die Prioritätsangabe in Fixity-Deklarationen kann auch weggelassen werden, in welchem Fall sie mit 9 angenommen wird. Hat ein Operator überhaupt keine FixityDeklaration, so ist er linksassoziativ mit Priorität 9. Funktionsanwendung hat Priorität 10 und bindet damit stärker als jeder Operator.
2.5. Mehr zu Typen
2.5.1. Parametrische Polymorphie
Typen können mittels anderer Typen parametrisiert werden. Als Beispiele haben wir
bereits Tupeltypen, Listentypen und Funktionstypen kennen gelernt. Im Listentyp
[Integer] ist beispielsweise der Typ Integer Parameter von [·]. Entnimmt man jetzt einer
14
In Situationen, in denen Links- bzw. Rechtsassoziativität eines Operators über die implizite Klammerung
eines Ausdrucks entscheidet, bewirkt fehlende Assoziativität, dass das Programm wegen Mehrdeutigkeit nicht akzeptiert wird.
16
Liste dieses Typs ein Element, so ist klar, dass dieses vom Typ Integer ist. Die Notwendigkeit einer Typkonvertierung mit Verträglichkeitsprüfung zur Laufzeit, wie man sie
beispielsweise in früheren Versionen von Java hatte, gibt es hier also nicht. Das Parametrisieren von Typen hat Ähnlichkeiten mit den in Java 1.5 eingeführten generischen
Klassen.
Nun gibt es aber Situationen, in denen ein Typparameter, wie z.B. ein Elementtyp,
keine Rolle spielen soll. Beispielsweise soll eine Funktion reverse, die eine Liste umdreht,
mit Listen jedes Elementtyps arbeiten und eine Funktion swap, die die Komponenten
eines Paares vertauscht, soll mit beliebigen Paaren zurecht kommen. Haskell ermöglicht
dies durch parametrische Polymorphie (engl. parametric polymorphism).
Die Funktion reverse erhält den Typ [el] → [el]. Da el mit kleinem Anfangsbuchstaben beginnt, handelt es sich um eine Typvariable. Diese steht für einen beliebigen Typ.
Der Typ von reverse bedeutet also: „Für jeden Typ el kann reverse auf eine Liste vom
Typ [el] angewendet werden und liefert wieder eine Liste vom Typ [el].“ Da es sich
hier um eine Allaussage handelt, wird statt [el] → [el] auch explizit ∀el.[el] → [el]
geschrieben.15 Es wird mit diesem Typ also ausgesagt, dass Elementtyp von Argument
und Resultat übereinstimmen müssen, aber ansonsten egal sind. Analog hat swap den
Typ (val1 , val2 ) → (val2 , val1 ) bzw. ∀val1 , val2 .(val1 , val2 ) → (val2 , val1 ). Die beiden Komponenten des Arguments dürfen also durchaus unterschiedliche Typen haben (müssen
es aber nicht), diese müssen aber in umgekehrter Reihenfolge als Typen der Resultatkomponenten auftreten.
Da reverse den Typ [el] → [el] besitzt, hat reverse automatisch auch die Typen
[Integer] → [Integer], [Char] → [Char], [[el]] → [[el]] usw. und swap hat in ähnlicher Weise auch Typen wie (val, val) → (val, val) oder (Integer, val2 ) → (val2 , Integer).
Allgemein gilt für einen Ausdruck e und einen Typ τ mit Typvariablen α1 bis αn die
Aussage e :: τ genau dann, wenn für alle Typen τ1 bis τn
e :: τ[τ1 /α1 ] . . . [τn /αn ]
gilt. Es ist also möglich, dass ein und derselbe Ausdruck mehrere Typen besitzt. Allerdings ist einer von ihnen immer der allgemeinste, was heißt, dass alle anderen durch
Substitution der Typvariablen aus ihm hervor gehen. Der allgemeinste Typ von reverse
ist [el] → [el], der allgemeinste von swap ist (val1 , val2 ) → (val2 , val1 ).
2.5.2. Typableitung und Typsignaturen
Ein Haskell-Compiler16 ist in der Lage, zu einem Ausdruck dessen allgemeinsten Typ
zu berechnen, was als Typableitung (engl. type inference) bezeichnet wird. Dadurch entfällt oft die Notwendigkeit expliziter Typangaben im Quelltext, ohne dass die Vorzüge
statischer Typprüfung verloren gehen.
15
Diese Notation ist in Haskell 98 nicht erlaubt. Allerdings wird sie vom Glasgow Haskell Compiler
unterstützt. In Abschnitt ?? werden Spracherweiterungen vorgestellt, die sich des Allquantors bedienen.
16
Wenn in diesem Dokument von Haskell-Compilern die Rede ist, sind praktisch immer auch Interpreter
gemeint, denn diese compilieren Haskell-Quelltext zunächst in Zwischencode, der dann interpretiert
wird. Statische Prüfungen wie z.B. Typprüfung finden während dieser Compilierphase statt.
17
Sind Typangaben aber erforderlich oder gewünscht, können sie mittels Typsignaturen
dargestellt werden. Es gibt zwei Arten von Typsignaturen. Bei der ersten Art handelt
es sich um Ausdrücke der Form e :: τ, wobei e wieder ein Ausdruck und τ ein Typ ist.
Der Ausdruck e :: τ hat den gleichen Wert wie e und besitzt den Typ τ. Dieser muss
der Typ von e oder eine Spezialisierung davon sein. Die zweite Art von Typsignaturen
sind Deklarationen. Diese haben die Form x :: τ, wobei x eine Variable und τ wieder ein
Typ ist. Hier wird der Variable x ein Typ zugewiesen, wobei dieser natürlich mit der
Definition von x harmonieren muss.
Explizite Typangaben mittels Typsignaturen sind aus folgenden Gründen mitunter
ratsam:
• Der Compiler kann verständlichere Fehlermeldungen generieren. Schlägt sich ein
Programmierfehler in einem falschen Typ nieder, welcher im Konflikt zu einer
Typsignatur steht, meldet der Compiler diesen Konflikt. Dagegen wird ohne explizite Typangabe einfach mit dem falschen Typ weiter gearbeitet. Das führt i.d.R.
zu einem Typfehler an einer Stelle, welche keinen direkten Bezug zu der eigentlichen Fehlerposition hat.
• Explizite Typangaben stellen eine Form von Dokumentation dar, die obendrein
vom Compiler auf Korrektheit überprüft wird.
• Manchmal würde für eine Variable ein allgemeinerer Typ abgeleitet werden, als
es dem Einsatzfeld dieser Variable entspricht. Dann kann man durch eine Typsignatur den Typ bewusst einschränken.
2.6. Aufgaben
Aufgabe 1. Im Prelude sind die folgenden Funktionen definiert:
id
:: val → val
const :: val → (dummy → val)
flip
:: (val1 → val2 → val) → (val2 → val1 → val)
($)
:: (val → val0 ) → val → val0
(◦)
:: (val0 → val00 ) → (val → val0 ) → (val → val00 )
(++) :: [el] → [el] → [el]
concat :: [[el]] → [el]
map
:: (el → el0 ) → [el] → [el0 ]
filter :: (el → Bool) → [el] → [el]
length :: [el] → Int
null
:: [el] → Bool
sum
:: [Int] → Int
product :: [Int] → Int
and
:: [Bool] → Bool
or
:: [Bool] → Bool
18
all
any
:: (el → Bool) → [el] → Bool
:: (el → Bool) → [el] → Bool
Gib zu jeder dieser Funktionen einen äquivalenten Ausdruck an, der keine let-Ausdrücke
enthält und nur folgende vordefinierten Funktionen benutzt:
¬ :: Bool → Bool
(≡) :: Int → Int → Bool
(+) :: Int → Int → Int
(:) :: el → [el] → [el]
foldr :: (el → accu → accu) → accu → [el] → accu
Die Spezifikation aller erwähnten Funktionen kann dem Haskell Report entnommen
werden.
Aufgabe 2. Die Ausdrücke
foldr (λel accu → accu ++ [el]) [ ]
und
λlist → foldr (λel fun → fun ◦ (el:)) id list [ ]
beschreiben beide die gleiche Funktion. Welche? Wie ist der Zeitaufwand in Abhängigkeit der Größe des Arguments?
Aufgabe 3. Gib zu folgenden Ausdrücken jeweils den allgemeinsten Typ an:
1. map (:) "Chamäleon"
2. foldr (const (++"Katze")) [ ]
3. (id◦)
4. map ◦ map
5. ( flip ◦ ( flip◦))
Aufgabe 4. Definiere eine Funktion reverseWords vom Typ [Char] → [Char], welche in
einer Zeichenkette alle Wörter umdreht. Als Wort soll dabei jede maximale Folge von
Buchstaben gelten. Es soll also z.B.
reverseWords "nicht-strikte, rein funktionale Programmiersprache"
das Resultat
"thcin-etkirts, nier elanoitknuf ehcarpsreimmargorP"
haben. Bei der Implementierung dürfen alle bisher eingeführten Sprachmittel sowie alle
auffindbaren Bibliotheken genutzt werden.
19
Aufgabe 5. Es soll ein Lösungsverfahren für das Spiel „Türme von Hanoi“ entwickelt
werden. Bei diesem Spiel hat man eine bestimmte Anzahl von gelochten Scheiben,
welche alle unterschiedliche Größe haben, sowie drei Säulen, auf die die gelochten
Scheiben gesteckt werden. Am Anfang befinden sich alle Scheiben mit von unten nach
oben abnehmender Größe auf Säule 1. Das Ziel des Spieles ist es, alle Scheiben auf
Säule 3 zu transportieren. Dabei darf immer nur jeweils eine Scheibe von einer Säule
entfernt und anschließend auf eine andere Säule gesteckt werden. Alle bereits auf der
Zielsäule vorhandenen Scheiben müssen dabei größer sein als die bewegte Scheibe.
Schreibe eine Funktion hanoi :: Int → [(Int, Int)], die bei Anwendung auf eine Anzahl
von Scheiben eine zugehörige Liste von Zügen ausgibt! Ein Zug soll dabei durch ein
Paar von Säulennummern dargestellt werden, wobei die erste Paarkomponente für die
Startsäule und die zweite für die Zielsäule der versetzten Scheibe stehen soll.
20
3. Das Typsystem
3.1. Algebraische Datentypen
3.1.1. Grundlegendes
Neue Typen können in Form algebraischer Datentypen eingeführt werden. Nehmen wir
an, wir wollen einen Typ für Postanschriften definieren. Es existieren Anschriften mit
Straße und Hausnummer sowie Anschriften mit Postfachangabe. Wir definieren einen
Hilfstyp, dessen Werte entweder eine Straße und eine Hausnummer oder ein Postfach
angeben:
data Location = StreetAndNo String String | POBox Int
Durch diese Deklaration wird folgendes definiert:
• ein Typkonstruktor Location, der den neuen Datentyp darstellt
• zwei Datenkonstruktoren StreetAndNo und POBox, welche die beiden Alternativen
charakterisieren
Die Typangaben hinter den Datenkonstruktoren geben die Typen einzelner Felder
an, aus denen sich ein entsprechender Location-Wert zusammensetzt. Im Falle von
StreetAndNo sind das Straße und Hausnummer, im Falle von POBox die Nummer des
Postfachs. Datenkonstruktoren sind spezielle Funktionen, welche aus einzelnen Feldern
einen Wert ihres algebraischen Datentyps konstruieren. Für die Datenkonstruktoren von
Location gilt also:
StreetAndNo :: String → String → Location
POBox
:: Int → Location
Bei der Konstruktion werden die einzelnen Felder also über ihre Position identifiziert.
Wie wir noch sehen werden, gilt gleiches für das Extrahieren von Feldern. Man kann
die Felder allerdings auch benennen. Eine entsprechende alternative Definition des
Location-Typs sieht folgendermaßen aus:
data Location = StreetAndNo {street :: String, no :: String} | POBox {poBoxNo :: Int}
Wir werden benannte Felder im Folgenden nicht verwenden. Daher wird auf den Umgang mit ihnen nicht weiter eingegangen.
Bezeichner von Datenkonstruktoren und Typkonstruktoren müssen mit einem Großbuchstaben beginnen. Tatsächlich sind auch Integer, Char usw. Typkonstruktoren. Datenkonstruktoren können auch als Operatoren eingeführt werden, welche dann natürlich
21
mit einem Doppelpunkt anfangen müssen. Für Typkonstruktoren ist die Operatorschreibweise mit Haskell 98 nicht möglich, mit dem Glasgow Haskell Compiler allerdings schon.
Algebraische Datentypen können auch parametrisiert sein. Als Beispiel betrachten
wir den im Prelude definierten Typkonstruktor Maybe. Maybe dient zum Darstellen
optionaler Werte und ist wie folgt definiert:
data Maybe val = Nothing | Just val
Für einen Typ val ist Maybe val der entsprechende Optionalitätstyp. Die Anwendung
eines Typkonstruktors auf einen Parameter wird also genauso geschrieben wie die
Anwendung einer Funktion auf ein Argument. Jetzt wird auch klar, warum wir von
Typ-Konstruktoren sprechen. Ein Typkonstruktor konstruiert Typen, wenn er auf Typparameter angewendet wird. Nullstellige Typkonstruktoren wie z.B. Location sind nur ein
Spezialfall.
3.1.2. Records und Aufzählungstypen
Hat ein algebraischer Datentyp nur einen Datenkonstruktor, entspricht er dem, was
in anderen Programmiersprachen Record oder Struktur genannt wird. In diesem Fall
bietet es sich an, den Datenkonstruktor genauso wie den Typkonstruktor zu bezeichnen. Das ist möglich, da Typkonstruktoren und Datenkonstruktoren in verschiedenen
Namensräumen residieren. Verwechslungen zwischen beiden sind nicht möglich.
Ein Beispiel für einen record-ähnlichen Typ ist der oben anvisierte Typ für Postanschriften:
data Address = Address String Location Int String
Die Adresse der BTU Cottbus könnte mit diesem als
Address "Brandenburgische Technische Universität Cottbus"
(StreetAndNo "Konrad-Wachsmann-Allee" "1")
03046
"Cottbus"
dargestellt werden.
Mit algebraischen Datentypen kann man auch Aufzählungstypen nachbilden. Man
führt einfach nur Datenkonstruktoren ohne Felder ein wie in der folgenden Definition
eines Wochentagstyps:
data DayOf Week = Sun | Mon | Tue | Wed | Thu | Fri | Sat
Auch der Typ Bool ist lediglich ein algebraischer Datentyp. Er ist im Prelude mittels
data Bool = False | True
definiert.
22
3.1.3. Rekursive Typen
Durch data-Deklarationen eingeführte Typkonstruktoren dürfen in den rechten Seiten beliebiger data-Deklarationen vorkommen. Dadurch kann man rekursive Typen
einschließlich wechselseitig rekursiver Typen formulieren. Als Beispiel nehmen wir die
Definition eines Typs, dessen Werte knotenmarkierte, geordnete Binärbäume darstellen:
data BinTree el = EmptyTree | NonEmptyTree el (BinTree el) (BinTree el)
Ein entsprechender Binärbaum ist laut dieser Definition entweder leer oder er ist ein
nicht-leerer Baum, bestehend aus einer Wurzelmarkierung sowie einem linken und
einem rechten Unterbaum. Man beachte zwei Dinge:
• Der leere Baum ist als spezieller Binärbaum zugelassen.
• Bei einem Einzelkind wird danach unterschieden, ob es linkes oder rechtes Kind
ist.
In der BinTree-Deklaration kommt die linke Seite BinTree el direkt als Feldtyp auf der
rechten Seite vor. Wir schauen uns nun ein Beispiel an, bei dem die linke Seite nicht
als Feldtyp, sondern als Parameter eines Feldtyps auftritt. Es ist die Deklaration eines
Typs für beliebige knotenmarkierte, geordnete Bäume, welche im Englischen auch als
rose trees bezeichnet werden:
data RoseTree el = RoseTree el [RoseTree el]
Die so eingeführten Bäume können allerdings nicht leer sein.
Schließlich behandeln wir noch ein Beispiel für eine data-Deklaration, in welcher der
definierte Typkonstruktor auf der rechten Seite mit verändertem Parameter auftritt. Unser Beispiel definiert einen Typ für vollständige, geordnete Binärbäume. Ein Binärbaum
ist vollständig, wenn alle Blätter die gleiche Tiefe und alle anderen Knoten genau zwei
Kinder haben. Unsere vollständigen Binärbäume haben allerdings nur an den Blättern
Markierungen und können nicht leer sein. Hier ist die Deklaration:
data PerfectBinTree el = Basic el | Nested (PerfectBinTree (el, el))
Das Interessante daran ist, dass die identische Tiefe der Blätter durch das Typsystem und
damit durch den Compiler garantiert werden kann. Die Sicherung dieser Eigenschaft
macht dieses Beispiel allerdings auch etwas kniffliger.
Für jeden Markierungstyp el hat jeder Wert von PerfectBinTree el die Struktur
Nested (. . . (Nested (Basic actualTree)) . . .),
da das einzige Argument von Nested wieder ein vollständiger Baum sein muss. Allerdings benutzt das Argument von Nested einen anderen, komplexeren, Elementtyp.
Während actualTree bei Bäumen ohne Nested-Anwendungen noch den Typ el hat, hat
actualTree bei genau einer Nested-Anwendungen den Typ (el, el), bei zwei Anwendungen ((el, el), (el, el)) usw. Die Werte dieser verschachtelten Paartypen können als Bäume
23
aufgefasst werden. Ein Paar charakterisiert einen Baum, dessen Wurzel zwei Unterbäume hat, die durch die beiden Paarkomponenten dargestellt werden. Ein Wert vom
Typ el stellt dagegen einen Baum dar, der aus einem einzigen Blatt besteht, welches mit
diesem Wert markiert ist. Durch actualTree wird also der eigentliche Baum beschrieben,
während die Anwendungen von Nested und Basic dem Aufbauen des geeigneten Typs
für actualTree gelten.
3.1.4. Musteranpassung
Das Analysieren von Werten algebraischer Datentypen geschieht mittels Musteranpassung (engl. pattern matching). Als Beispiel diene die Definition einer Funktion, welche
zu einem Wert des Typs Location die entsprechende Textzeile der Postanschrift liefert:
locationLine1 :: Location → String
locationLine1 loc = case loc of
StreetAndNo street no → street ++ ’ ’ : no
POBox no
→ "Postfach " ++ show no
Links der Pfeile befinden sich Muster. Diese sind Anwendungen von Datenkonstruktoren auf Variablen,1 welche die verschiedenen Alternativen von Location repräsentieren. Das Resultat des case-Ausdrucks hängt davon ab, ob der Wert von loc die Form
StreetAndNo street no oder POBox no hat. Die zu dem entsprechenden Muster gehörende
rechte Seite liefert das Ergebnis, wobei die Variablen des Musters an die entsprechenden
Felder von loc gebunden werden. Das Ergebnis des case-Ausdrucks mit der Bindung
loc = StreetAndNo "Konrad-Wachsmann-Allee" "1"
ist also
"Konrad-Wachsmann-Allee" ++ ’ ’ : "1".
Besteht wie oben die rechte Seite einer Funktionsdefinition aus einem case-Ausdruck,
welcher eine Fallunterscheidung über einem Funktionsargument durchführt, so kann
eine vereinfachende Syntax genutzt werden. Bei dieser wird für jede Alternative eine
eigene Definitionsgleichung angegeben, wobei auf der linken Seite für das Funktionsargument das entsprechende Muster eingesetzt wird. Damit kann die Berechnung der
besagten Zeile der Postanschrift auch so realisiert werden:
locationLine2 :: Location → String
locationLine2 (StreetAndNo street no) = street ++ ’ ’ : no
locationLine2 (POBox no)
= "Postfach " ++ show no
Das Arbeiten mit rekursiven und parametrisierten algebraischen Datentypen demonstriert folgende Definition einer Funktion zum Spiegeln von Binärbäumen:
1
Beachte, dass auch hier wieder die Operatorschreibweise genutzt werden kann!
24
mirror :: BinTree el → BinTree el
mirror EmptyTree
= EmptyTree
mirror (NonEmptyTree root left right) = NonEmptyTree root
(mirror left)
(mirror right)
Muster können verschachtelt werden. Auch Muster wie StreetAndNo street no sind
tatsächlich verschachtelte Muster. Der Grund ist, dass einzelne Variablen auch Muster
sind, nämlich solche, auf die jeder Wert passt und welche eine Variablenbindung bewirken. Somit wurde auch in der Definition von locationLine1 links vom Gleichheitszeichen
ein Muster für das Funktionsargument verwendet.
Neben Variablen und Anwendungen von Datenkonstruktoren auf Muster existieren
weitere Arten von Mustern:
• Das Jokermuster (engl. wildcard pattern), welches aus einem Unterstrich (_) besteht,
ist ein Muster, auf das jeder Wert passt. Im Gegensatz zu Variablenmustern wird
aber keine Variablenbindung erzeugt.
• Ein Aliasmuster (engl. as-pattern) hat die Form [email protected], wobei x eine Variable und p
ein Muster ist. Es hat die gleiche Wirkung wie das Muster p mit dem Unterschied,
dass bei erfolgreicher Musteranpassung zusätzlich die Variable x an den an das
Muster angepassten Ausdruck gebunden wird.
Mit verschachtelten Mustern und Jokermustern ist es jetzt natürlich leicht, Fallunterscheidungen zu konstruieren, in denen ein und derselbe Wert auf mehrere Muster passt.
Das ist durchaus zulässig. Es wird in diesem Fall einfach die erste passende Alternative
verwendet.
Die soeben besprochenen Features demonstrieren wir anhand der Definition einer
Funktion isHeap1 , welche prüft, ob ein gegebener Binärbaum ein Heap2 ist:
isHeap1 :: BinTree Integer → Bool
isHeap1 (NonEmptyTree root
[email protected](NonEmptyTree leftRoot _ _)
EmptyTree)
= root < leftRoot
∧ isHeap1 left
isHeap1 (NonEmptyTree root
EmptyTree
[email protected](NonEmptyTree rightRoot _ _)) = root < rightRoot
∧ isHeap1 right
isHeap1 (NonEmptyTree root
[email protected](NonEmptyTree leftRoot _ _)
[email protected](NonEmptyTree rightRoot _ _)) = root < leftRoot
∧ root < rightRoot
2
Ein Heap ist ein Binärbaum, in dem die Markierung jedes Knotens kleiner ist als die Markierungen
seiner Kinder.
25
∧ isHeap1 left
∧ isHeap1 right
= True
isHeap1 _
Erwähnt werden soll noch, dass in λ-Ausdrücken zwischen λ und → auch ein Muster
steht. Beispielsweise steht
λ(Address name loc postalCode city) → [name, locationLine loc, show postalCode ++ city]
für die Konvertierung einer Postanschrift in die Zeilen, die tatsächlich auf dem Brief
o.dgl. erscheinen sollen. In den bisher verwendeten λ-Ausdrücken haben wir also nur
den Spezialfall der Variablenmuster verwendet.
Weiterhin werden auch bei Listenkomprehensionen in Generatoren links vom ←
Muster verwendet. Die der Liste des jeweiligen Generatoren entnommenen Elemente werden einer Musteranpassung unterzogen. Passen sie auf das Muster, werden die
entsprechenden Variablenbindungen eingeführt. Andernfalls werden die Elemente verworfen. Als Beispiel betrachten wir eine alternative Implementierung des Heap-Tests:
isHeap2 :: BinTree Integer → Bool
isHeap2 EmptyTree
= True
isHeap2 (NonEmptyTree root left right) = subrootsAreOk
∧ isHeap2 left
∧ isHeap2 right where
subrootsAreOk = and [root < subroot | NonEmptyTree subroot _ _ ← [left, right]]
3.1.5. Standardtypen als algebraische Datentypen
Tupel- und Listentypen sind auch nur algebraische Datentypen, für die lediglich spezielle Notationen existieren. Wir zeigen dass zunächst für den Unit-Typ. Sieht man davon
ab, dass () kein gültiger Bezeichner ist, kann man den Unit-Typ mittels
data () = ()
definieren. Das macht klar, warum () sowohl als Typ als auch als Ausdruck verwendet
werden kann. Es zeigt außerdem, dass () auch ein Muster ist, auf welches der Wert ()
passt, denn () ist eine Datenkonstruktoranwendung auf null Argumente.
Ein Typ (τ1 , . . . , τn ) ist identisch mit (, . . . , ) τ1 . . . τn und ein Ausdruck (e1 , . . . , en ) ist
das Gleiche wie (, . . . , ) e1 . . . en . Die speziellen „Bezeichner“ (, , ), (, , , ) usw. stehen sowohl für Typ- als auch für Datenkonstruktoren und könnten, wenn sie den lexikalischen
Regeln für Bezeichner entsprechen würden, auf folgende Weise definiert werden:
data (, ) val1 val2
= (, ) val1 val2
data (, , ) val1 val2 val3 = (, , ) val1 val2 val3
..
.
26
Die spezielle Tupelsyntax gilt auch für Muster. Sind also p1 bis pn Muster, so ist (p1 , . . . , pn )
ein Muster, welches äquivalent zu (, . . . , ) p1 . . . pn ist.
Ist τ ein Typ, so ist [τ] nur eine alternative Notation für [ ] τ. Das heißt, dass [ ] in
Typen für den Listentypkonstruktor steht. Wäre [ ] als Bezeichner erlaubt, ließe sich der
Listentyp so definieren:
data [ ] el = [ ] | el : [el]
Man beachte, dass wir mit : einen Datenkonstruktor in Operatorform haben! Ein Listenausdruck [e1 , . . . , en ] ist äquivalent zu e1 : . . . : en : [ ]. Sind p1 bis pn Muster, so ist auch
[p1 , . . . , pn ] ein Muster und dieses ist äquivalent zu p1 : . . . : pn : [ ].
Der Operator ++ lässt sich nun aufbauend auf obiger Deklaration so definieren:
(++) :: [el] → [el] → [el]
[]
++ els2 = els2
(el1 : els1 ) ++ els2 = el1 : els1 ++ els2
Jetzt wird auch klar, wieso der Konkatenationsaufwand linear mit der Länge der ersten
Liste steigt. Die zweite Liste kann als Teil der Resultatliste wiederverwendet werden,
muss also nicht kopiert werden. Für die erste Liste ist das aber nicht möglich. Ist die
erste Liste e1 : . . . : en : [ ], so müsste man das abschließende [ ] durch die zweite Liste
ersetzen. Solche Änderungen an Datenstrukturen sind aber in einer rein funktionalen
Sprache nicht erlaubt.
3.2. Klassen
3.2.1. Grundlegendes
Betrachten wir einmal Haskells Gleichheitsoperator ≡ etwas genauer. Der Gleichheitsoperator kann mit unterschiedlichen Argumenttypen, wie z.B. Bool, Char und Integer,
umgehen. Er scheint also selbst einen polymorphen Typ zu besitzen. Allerdings benutzt
er für die verschiedenen Argumenttypen unterschiedliche Implementierungen. Außerdem ist er nicht für alle Typen definiert. Wie steht es z.B. mit Funktionen? Zwei Funktionen sollen sinnvollerweise dann als gleich gelten, wenn sie für gleiche Argumente
gleiche Resultate besitzen. Der Funktionsgleichheitstest ist damit ein unentscheidbares
Problem. Eine ≡-Implementierung für Funktionen kann es nicht geben.
Wir benötigen also eine Möglichkeit den ≡-Operator für ausgewählte Typen jeweils
speziell zu implementieren. Haskell gibt uns diese Möglichkeit in Form von Klassen3 .
Der Operator ≡ sowie sein Gegenstück . werden im Prelude folgendermaßen eingeführt:
3
Klassen in Haskell sind Typklassen und damit etwas ganz anderes als Klassen in objektorientierten
Programmiersprachen. Am ehesten gibt es Ähnlichkeiten zwischen Typklassen und Schnittstellen (engl.
interfaces) in Java.
27
class Eq val where
(≡) :: val → val → Bool
(.) :: val → val → Bool
val1 ≡ val2 = ¬ (val1 . val2 )
val1 . val2 = ¬ (val1 ≡ val2 )
Diese class-Deklaration führt eine Klasse Eq mit zwei Methoden (≡) und (.) ein.
Um die Methoden für einen konkreten Typ zu implementieren, macht man diesen
Typ zu einer Instanz4 dieser Klasse. Wir betrachten das am Beispiel des Typs Bool:
instance Eq Bool where
False ≡ False = True
False ≡ True = False
True ≡ False = False
True ≡ True = True
Für die Methode (≡) wird eine Implementierung angegeben. Da für die Typvariable
val der class-Deklaration der Typ Bool in der instance-Deklaration eingesetzt wurde,
muss (≡) im Rahmen der instance-Deklaration den Typ Bool → Bool → Bool besitzen.
Da die instance-Deklaration keine Implementierung von (.) enthält, wird die in der
class-Deklaration angegebene Standardimplementierung genommen.
3.2.2. Typen mit Kontext
Wir verwenden nun den Operator ≡ in einer Funktion, die prüft, ob alle Elemente einer
Liste identisch sind:
allEq [ ]
= True
allEq (el : els) = all (≡ el) els
Die Funktion allEq besitzt z.B. die Typen [Bool] → Bool und [Char] → Bool, weil Bool
und Char Instanzen von Eq sind. Was ist aber der allgemeinste Typ von allEq? Der Typ
[el] → Bool kann es nicht sein, weil das beliebige Elementtypen erlauben würde, also
auch solche, die keine Eq-Instanzen sind.
Der allgemeinste Typ ist von allEq ist (Eq el) ⇒ [el] → Bool. Der Teil links von ⇒ wird
dabei als Kontext bezeichnet. Er enthält die Aussage Eq el, welche besagt, dass el auf
Instanzen von Eq beschränkt ist. Ein Kontext kann mehrere solcher Aussagen enthalten, in welchem Falle sie durch Kommata getrennt werden. Unter Verwendung eines
Kontexts können wir auch den allgemeinsten Typ für die beiden Eq-Methoden angeben.
Dieser ist (Eq val) ⇒ val → val → Bool. Die in der class-Deklaration angegebenen Typen
müssen somit noch um die Aussage Eq val ergänzt werden, um den allgemeinsten Typ
der Methoden zu erhalten.
4
Man beachte, dass Instantiierung von Typklassen nichts mit Instantiierung in der objektorientierten Programmierung zu tun hat. Das Instantiieren entspricht am ehesten dem Implementieren einer Schnittstelle
durch eine Klasse in Java.
28
3.2.3. Instanzen mit Kontext
Wir wollen jetzt eine instance-Deklaration für den Gleichheitstest von Listen angeben:
instance (Eq el) ⇒ Eq [el] where
[]
≡ []
= True
el1 : els1 ≡ el2 : els2 = el1 ≡ el2 ∧ els1 ≡ els2
_
≡_
= False
Interessant ist hierbei die zweite Definitionsgleichung. Es müssen dort zwei Listenelemente miteinander verglichen werden. Dafür muss die Einschränkung Eq el gelten. Diese wird über den Kontext im Kopf der instance-Deklaration bewirkt. Die (≡)Implementierung für Listen hat damit den allgemeinsten Typ (Eq el) ⇒ [el] → [el] →
Bool.
3.2.4. Klassen mit Kontext
Sehen wir uns nun folgende Deklarationen an, welche in ähnlicher Weise im Prelude
existieren:
data Ordering = LT | EQ | GT
instance Eq Ordering where
LT ≡ LT = True
EQ ≡ EQ = True
GT ≡ GT = True
_ ≡ _ = False
class (Eq val) ⇒ Ord val where
compare :: val → val → Ordering
(6)
:: val → val → Bool
(<)
:: val → val → Bool
(>)
:: val → val → Bool
(>)
:: val → val → Bool
max
:: val → val → val
min
:: val → val → val
compare val1 val2 | val1 ≡ val2 = EQ
| val1 6 val2 = LT
| otherwise = GT
val1
val1
val1
val1
6 val2
< val2
> val2
> val2
max val1 val2
= compare val1
= compare val1
= compare val1
= compare val1
| val1 6 val2 = val2
29
val2
val2
val2
val2
. GT
≡ LT
. LT
≡ GT
min val1 val2
| otherwise = val1
| val1 6 val2 = val1
| otherwise = val2
Man beachte besonders den Kontext (Eq val) im Kopf der class-Deklaration! Er besagt,
dass ein Typ Instanz von Eq sein muss, wenn er Instanz von Ord sein soll. Die Aussage
Ord val beinhaltet damit die Aussage Eq val. Somit ist der Kontext (Eq val, Ord val) äquivalent zu (Ord val). Ohne diese Beziehung zwischen Eq und Ord könnte der Operator ≡
nicht in der Standardimplementierungen von compare verwendet werden.
3.3. Sorten
3.3.1. Motivation
Wie bekannt sein sollte, wendet die Funktion map :: (el → el0 ) → [el] → [el0 ] eine
gegebene Funktion elementweise auf eine gegebene Liste an. Eine solche elementweise
Anwendung kann aber auch für andere Datenstrukturen durchgeführt werden. Wir
sehen uns das für die Typen BinTree und RoseTree an:
mapBinTree :: (el → el0 ) → BinTree el → BinTree el0
mapBinTree _ EmptyTree
= EmptyTree
mapBinTree fun (NonEmptyTree root left right) = NonEmptyTree ( fun root)
(mapBinTree fun left)
(mapBinTree fun right)
mapRoseTree :: (el → el0 ) → RoseTree el → RoseTree el0
mapRoseTree fun (RoseTree root subtrees) = RoseTree ( fun root)
(map (mapRoseTree fun) subtrees)
Wir wollen nun die im Prinzip gleiche Funktionalität der verschiedenen map-Funktionen durch eine Methode einer bestimmten Klasse darstellen. Um diese Klasse zu
entwickeln, werfen wir noch einmal einen Blick auf die Klasse Eq. Ziel dieser Klasse
war es, die verschiedenen Gleichheitstest zu einen. Die einzelnen Gleichheitstest haben
die Typen Bool → Bool → Bool, Char → Char → Bool, Integer → Integer → Bool usw.
Der Unterschied liegt also in den Typen der Argumente. Es sind die Argumenttypen,
welche zu Instanzen der Klasse Eq werden. In der Typsignatur von (≡) werden sie durch
die im Kopf der Klassendeklaration angegebene Typvariable val ersetzt.
Nun betrachten wir die Typen der map-Funktionen. Es gibt zwei Stellen, an denen
sie sich unterscheiden, nämlich beim Typ des zweiten Arguments und bei dem des
Resultats. Unglücklicherweise sind diese beiden Typen nicht identisch. Wir können
also nicht beide durch eine einzige Typvariable ersetzen, welche dann im Kopf der
Klassendeklaration steht. Wir könnten eine deutliche Einschränkung der Flexibilität
in Kauf nehmen und die map-Funktionen so einschränken, dass die Elementtypen der
Quell- und Zielstruktur gleich sind. Dann hätten wir folgenden Ansatz:
30
class Collection coll where
mapCollection :: (el → el) → coll → coll
Aber damit ist die Beziehung zwischen dem Typ el und dem Kollektionstyp zerstört,
die darin bestand, dass der Kollektionstyp el als Elementtyp verwendet. Als Beispiel
schauen wir uns eine instance-Deklaration für Listen an:
instance Collection [el] where
mapCollection = [. . .]
Welchen Typ hat mapCollection für die entsprechende Instanz? Naiv würde man coll durch
[el] ersetzen und erhielte den korrekten Typ (el → el) → [el] → [el]. Jedoch bezeichnet el im Kopf der instance-Deklaration einen (potentiell) anderen Typen als el in der
Typsignatur von mapCollection .5 Damit wäre der Typ von mapCollection im Falle von Listen
(el1 → el1 ) → [el2 ] → [el2 ]. Es wäre also erlaubt, dass das Funktionsargument mit einem
Typ arbeitet, der vom tatsächlichen Elementtyp völlig verschieden ist.
Der eigentliche Unterschied zwischen den Typen der einzelnen map-Funktionen ist
der für die Kollektionstypen verwendete Typkonstruktor. Dieser ist entweder [ ] oder
BinTree oder RoseTree. Wir hätten eine Lösung für unser Problem, wenn wir nur diese
Konstruktoren ohne zugehörige Parameter zu Instanzen einer Klasse machen könnten.
Das geht aber in der Tat. Das Prelude stellt uns eine folgendermaßen definierte Klasse
namens Functor zur Verfügung:
class Functor func where
fmap :: (val → val0 ) → func val → func val0
Eine instance-Deklaration für [ ] wird bereits vom Prelude bereit gestellt. Deklarationen
für BinTree und RoseTree können wir uns selbst schreiben:
instance Functor BinTree where
fmap = mapBinTree
instance Functor RoseTree where
fmap = mapRoseTree
Wieso funktioniert nun dies alles? Die Antwort ist, dass auch [ ], BinTree und RoseTree
ohne Parameter als Typen betrachtet werden. Nun besitzen Typen aber normalerweise
Werte. Allerdings gibt es keine Werte vom Typ [ ], BinTree oder RoseTree. Es gibt also
Typen, die Werte besitzen und solche, die es nicht tun. Das führt uns zu Sorten.
5
Sowohl der Typ [el] im Kopf der instance-Deklaration als auch der für mapCollection angegebene Typ
enthält eine implizite Allquantifizierung von el. Damit ist el in beiden Fällen nur eine lokale Variable,
die unabhängig von anderen Typen belegt werden kann.
31
3.3.2. Grundlegendes
Sorten6 (engl. kinds) werden genutzt, um Typen zu klassifizieren. Sorten sind gewissermaßen die Typen der Typen. Haskells Typsystem umfasst also drei Ebenen. Auf der
unteren Ebene befinden sich Ausdrücke und Werte. Diese besitzen Typen, welche auf
der mittleren Ebene residieren. Jeder Typ wiederum hat eine Sorte, die zur oberen Ebene
gehört.
Eine Sorte ist entweder ∗ oder hat die Form κ → κ0 , wobei κ und κ0 wiederum Sorten
sind. Zur Sorte ∗ gehören alle Typen, von welchen es Werte gibt, also z.B. Char, [el] und
(Integer, Bool). Typen einer Sorte der Form κ → κ0 sind gewissermaßen Funktionen über
Typen, welche Typen der Sorte κ in Typen der Sorte κ0 überführen. Die Typen [ ], BinTree
und RoseTree besitzen also alle die Sorte ∗ → ∗. Die Instanzen der Klasse Functor sind
nun einfach Typen der Sorte ∗ → ∗, während Instanzen von Eq und Ord von Sorte ∗
sind. Der Compiler findet das selbständig mittels Sortenableitung, einem Analogon zur
Typableitung, heraus.7
3.3.3. Typen höherer Ordnung
Auch bei Typen wird Currying verwendet, sodass (, ) Sorte ∗ → ∗ → ∗ besitzt, was das
Gleiche wie ∗ → (∗ → ∗) ist. Der Typ (, ) ist also streng genommen bereits ein Typ höherer
Ordnung, da die Resultatsorte nicht ∗ ist. Aber auch in der Argumentposition können
andere Sorten als ∗ auftreten. Dazu betrachten wir ein Beispiel.
Wir definieren uns zunächst einen Datentyp für nicht-leere Listen:
data NonEmptyList el = NonEmptyList el (Maybe (NonEmptyList el))
Diesen machen wir zu einer Functor-Instanz:
instance Functor NonEmptyList where
fmap fun (NonEmptyList head maybeTail) = NonEmptyList head0 maybeTail0 where
head0
= fun head
maybeTail0
= fmap ( fmap fun) maybeTail
Diese instance-Deklaration nutzt die Tatsache, dass auch Maybe eine Functor-Instanz
ist. Die entsprechende instance-Deklaration sieht folgendermaßen aus:
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap fun (Just val) = Just ( fun val)
6
Bezüglich einer deutschen Übersetzung des englischen „kind“ gibt es momentan keinen Konsens. Es
gibt zu diesem Thema eine vom Autor dieser Zeilen angestoßene E-Mail-Diskussion [9] mit z.T. sehr
interessanten Beiträgen.
7
Im Unterschied zu Typen gibt es aber keine polymorphen Sorten. Wo die Sortenableitung normalerweise
eine Sortenvariable ermitteln würde, wird diese einfach zu ∗ spezialisiert.
32
Vergleichen wir nun die data-Deklaration von NonEmptyList und die zugehörige
instance-Deklaration für Functor mit den entsprechenden Deklarationen für RoseTree.
Die data-Deklaration von NonEmptyTree ist nahezu identisch mit der von RoseTree. Es
wird lediglich Maybe statt [ ] verwendet. Auch die fmap-Implementierungen sind nahezu
gleich. Die für NonEmptyList verwendet lediglich die Maybe- und die NonEmptyListImplementierung von fmap wo diejenige für RoseTree die Funktionen map und mapRoseTree
benutzt. Diese sind aber gerade äquivalent zu fmap für [ ] bzw. RoseTree.
Unser Vergleich führt uns zu einem allgemeineren Baumtyp, aus dem RoseTree und
NonEmptyList als Spezialfälle hervor gehen. Dieser Baumtyp ist parametrisiert über
einem Typ der Sorte ∗ → ∗. Im Falle von RoseTree wird dieser mit [ ] belegt und zeigt damit
an, dass ein Knoten beliebig viele Kinder besitzen kann. Im Falle von NonEmptyList ist
dieser Parameter Maybe, was anzeigt, dass eine nicht-leere Liste praktisch ein Baum ist,
in dem jeder Knoten ein oder kein Kind hat. Hier der entsprechende Code:
data Tree subtrees el = Tree el (subtrees (Tree subtrees el))
instance (Functor subtrees) ⇒ Functor (Tree subtrees) where
fmap fun (Tree root subtrees) = Tree ( fun root) ( fmap ( fmap fun) subtrees)
Der Typ Tree ist von der Sorte (∗ → ∗) → ∗ → ∗. Die partielle Typanwendung Tree [ ]
entspricht dem Typ RoseTree und Tree Maybe entspricht NonEmptyList.
3.4. Aufgaben
Aufgabe 6. Jede Liste lässt sich in der Form el1 : el2 : . . . : eln : [ ] darstellen.8 Für geeignete
Werte fun und init erhält man das Ergebnis von
foldr fun init (el1 : el2 : . . . : eln : [ ]),
indem man in el1 : el2 : . . . : eln : [ ] alle Vorkommen von : durch ‘fun‘ und das eine
Vorkommen von [ ] durch init ersetzt und das Ergebnis des entstandenen Ausdrucks
bildet.
Dieses Prinzip lässt sich auf andere Datentypen übertragen, z.B. auf BinTree. Schreibe
eine Funktion foldBinTree , welche das foldr-Pendant für BinTree darstellt! Implementiere
auf Basis dieser Funktion die Funktionen heightBinTree und leavesBinTree , welche Höhe bzw.
Blätter eines Binärbaumes ermitteln! Kann auch für RoseTree eine Faltungsfunktion
angegeben werden? Wenn ja, implementiere diese und schreibe auf deren Basis eine
Funktion toListRoseTree , welche zu jedem Baum dessen Präfixlinearisierung9 liefert!
Aufgabe 7. Implementiere die Funktionen heightPerfectBinTree und leavesPerfectBinTree , welche
zu einem vollständigen Binärbaum dessen Höhe bzw. Blätter liefern! Kann man auch
8
Das stimmt nicht ganz, denn es können mit Haskell auch unendliche Listen dargestellt werden, wie wir
in Kapitel ?? sehen werden. Allerdings ändert das nichts an der Idee dieser Aufgabe.
9
Die Präfixlinearisierung eines knotenmarkierten, geordneten Baumes ist eine Liste, die mit der Wurzelmarkierung des Baumes beginnt, an welche sich die Konkatenation der Präfixlinearisierungen der
Unterbäume anschließt.
33
zu PerfectBinTree eine allgemeine Funktion im Stil der Faltungsfunktionen foldr und
foldBinTree finden? Wenn ja, gib Implementierungen der Funktionen heightPerfectBinTree und
leavesPerfectBinTree an, die diese Funktion nutzen!
Aufgabe 8. Implementiere einen Typ für vollständige, geordnete Binärbäume, bei dem
ein Baum im Gegensatz zu PerfectBinTree leer sein kann und Markierungen für sämtliche
Knoten statt nur für die Blätter enthält!
Aufgabe 9. Implementiere eine Funktion mergeSort, welche Listen nach dem MergeSort-Verfahren sortiert! Welchen allgemeinsten Typ hat diese Funktion?
Aufgabe 10. Die Klasse Foldable aus dem Modul Data.Foldable ermöglicht die Verallgemeinerung von foldr auf andere Typen als [ ]. Die Funktion toList des gleichen Moduls
realisiert auf der Basis von foldr eine Konvertierung in Listen. Instantiiere Foldable für
BinTree und RoseTree, sodass toList jeweils die Präfixlinearisierung des gegebenen Baumes bildet. Gib außerdem eine instance-Deklaration für alle Typen Tree subtrees an, bei
denen subtrees Instanz von Foldable ist!
Aufgabe 11. Entwickle einen Typ SubtreesBinTree , für den Tree SubtreesBinTree dem Typ
BinTree entspricht. Mache diesen Typ zu einer Instanz von Functor und Foldable!
Aufgabe 12. Das Modul Control.Monad.Identity exportiert den Typ Identity. Welche Werte umfasst der Typ Tree Identity? Mache Identity zu einer Instanz von Foldable! Was für
eine Funktion stellt jetzt toList :: Tree Identity el → [el] dar?
Aufgabe 13. Wie in Aufgabe 10 erwähnt, ist Data.Foldable.toList eine Funktion, also keine
Methode der Klasse Foldable. Die Implementierung von toList ist also einheitlich für alle
Foldable-Instanzen. Jetzt stellen wir uns vor, toList wäre eine Foldable-Methode und die
ehemals einheitliche toList-Implementierung wäre lediglich die Standardimplementierung dieser Methode. Könnten nun alle anderen Foldable-Methoden unabhängig von der
konkreten Instanz auf toList zurück geführt werden, dann könnte man entsprechende
Standardimplementierungen für diese Methoden zur Verfügung stellen. Damit müsste
man für jede Instanz nur toList implementieren, was i.d.R. einfacher wäre, als z.B. foldr
zu implementieren. Ist dieses Zurück-Führen auf toList möglich?
34
4. Nicht-strikte Semantik und verzögerte
Auswertung
4.1. Vorüber gehende Notizen
Folgendes wurde vom Autor nur mal eben schnell hingeschrieben und wird noch in
Zukunft überarbeitet.
4.1.1. Untermengensummen
Wir implementieren das Untermengensummen-Problem, wobei wir eigentlich gar nicht
mit Mengen, sondern mit Multimengen arbeiten:
data SubsetWithSum = SubsetWithSum [Integer] Integer deriving (Show)
subsetsWithSums :: [Integer] → [SubsetWithSum]
subsetsWithSums [ ]
= [SubsetWithSum [ ] 0]
subsetsWithSums (el : els)
= subresult ++
[SubsetWithSum (el : chosen) (el + sum) |
SubsetWithSum chosen sum ← subresult] where
subresult = subsetsWithSums els
subsetSum :: [Integer] → Integer → [[Integer]]
subsetSum els wantedSum = [chosen |
SubsetWithSum chosen sum ← subsetsWithSums els,
sum ≡ wantedSum]
Wenn wir subsetSum [1 . . 100] 3 auswerten wollen, erhalten wir verständlicherweise
innerhalb unserer Lebenszeit kein Ergebnis. Erstaunlicherweise bekommen wir bei Eingabe von subsetSum [1 . . 100] 199 sofort die Ausgabe [[99, 100] und danach erst hängt
der Computer. Das hat mit verzögerter Auswertung zu tun.
4.1.2. Effiziente Berechnung von Fibonacci-Zahlen
Die in Unterabschnitt 2.4.3 vorgestellte Implementierung von fib führt zu exponentiellem Aufwand. Wir stellen hier eine effiziente Implementierung vor:
fibAccu :: Integer → Integer
fibAccu num | num < 0 = error "negative index into fibonacci sequence"
| num ≡ 0 = 0
35
| num > 0 = accumulate 0 1 num where
accumulate :: Integer → Integer → Integer → Integer
accumulate _
last 1
= last
accumulate secondLast last num = accumulate last (secondLast + last) (pred num)
Nun definieren wir die Liste aller Fibonacci-Zahlen, wobei für die Berechung jeder
neuen Zahl die Ergebnisse bisheriger Berechnungen genutzt werden:
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
4.2. Aufgaben
Aufgabe 14. Beim Acht-Damen-Problem geht es darum, auf einem Schachbrett acht
Damen so zu positionieren, dass keine die andere schlagen kann. Ein Schachbrett ist
8 × 8 Felder groß und eine Dame kann jede Figur schlagen, die sich auf der gleichen
Zeile, der gleichen Spalte oder ein und derselben Diagonalenparallelen befindet.1 Das
Acht-Damen-Problem kann verallgemeinert werden, indem die Positionierung von n
Damen auf einem (n × n)-Spielfeld mit n ∈ N behandelt wird.
Schreibe eine Funktion queens, welche zu einem gegebenen n die Liste aller Lösungen
des entsprechenden n-Damen-Problems liefert! Da bei jeder Lösung in jeder Spalte
genau eine Dame steht, soll eine Lösung kompakt als Liste [line1 , . . . , linen ] dargestellt
werden, wobei jedes linei die Zeile der in Spalte i stehenden Dame repräsentieren soll.
Nutzt deine Implementierung verzögerte Auswertung aus um Lösungen so früh wie
möglich zu liefern?
Aufgabe 15. Implementiere die Liste primes aller Primzahlen in aufsteigender Reihenfolge! Kann aufbauend auf dieser Liste die Liste aller Primzahlzwillinge2 definiert
werden?
Aufgabe 16. Für jedes c ∈ C sei die Funktion mc : N → C folgendermaßen definiert:
mc (0) = 0
mc (n + 1) = (mc (n))2 + c
Die Menge
M = {c ∈ C | ∀n ∈ N : |mc (n)| 6 2}
heißt Mandelbrotmenge. Um die Mandelbrotmenge näherungsweise zu visualisieren,
wählt man einen Genauigkeitsgrad g ∈ N und stellt die Menge
M g = c ∈ C ∀n ∈ 0, . . . , g − 1 : |mc (n)| 6 2
1
Wir vernachlässigen die Farben der Damen und damit auch die Tatsache, dass beim Schach eine Figur
eigentlich immer nur eine Figur anderer Farbe schlagen kann.
2
Ein Primzahlzwilling ist ein Paar zweier Primzahlen, bei dem die zweite Primzahl um zwei größer ist
als die erste.
36
dar. Es ist klar, dass die Mengen M g sich mit steigendem g der Mandelbrotmenge
o
Tn
M g g ∈ N = M.
annähern. Genauer gesagt, gilt M g ⊇ M g+1 für jedes g ∈ N und
Schreibe unter Zuhilfenahme des Typs Complex aus Data.Complex eine Funktion
isInMandelbrotSet :: Int → Complex Double → Bool,
welche zu einer Genauigkeit g und einer komplexen Zahl c ermittelt, ob c ∈ M g gilt!
Zum Visualisieren der Mandelbrotmenge kann folgender Code benutzt werden:
showMandelbrotSet realBounds imagBounds lineCnt colCnt stepCnt = show where
show = putStr $
unlines $
[[if isInMandelbrotSet stepCnt (real :+ imag) then ’#’ else ’ ’ |
col ← [0 . . pred colCnt],
let real = toCoord realBounds colCnt col] |
line ← [0 . . pred lineCnt],
let imag = toCoord imagBounds lineCnt line] where
toCoord (min, max) size pos = min +
(max − min) ∗ fromIntegral pos / fromIntegral size
Man gibt showMandelbrot (minReal, maxReal) (minImag, maxImag) lineCnt stepCnt an der
Eingabeaufforderung von GHCi ein und erhält eine Darstellung mittels #-Zeichen. Dabei
geben minReal, maxReal, minImag und maxImag den rechteckigen Ausschnitt der komplexen Ebene an, der betrachtet werden soll. Die Argumente lineCnt und colCnt liefern
die Zeilen- und Spaltenanzahl der Darstellung und stepCnt steht für die Genauigkeit.
Eine recht schöne Darstellung liefert showMandelbrotSet (−2, 0.5) (−1, 1) 36 80 1000.
37
A. Verwendete Sonderzeichen
In den Codebeispielen wurden manche in Haskell verwendete Symbolfolgen durch (oftmals an mathematische Darstellungen angelehnte) Sonderzeichen ersetzt. Tabelle A.1
zeigt die hier verwendeten Symbole zusammen mit ihren Haskell-Entsprechungen.
38
Sonderzeichen
→
←
⇒
¬
∧
∨
≡
.
6
>
◦
↑
⊥
λ
Haskell-Entsprechung
−>
<−
=>
not
&&
||
==
/=
<=
>=
.
^
undefined
\
Tabelle A.1.: Verwendete Sonderzeichen und ihre Haskell-Entsprechungen
39
B. Zitate über Haskell
Im Folgenden sind einige Zitate über Haskell aufgeführt, die bei Internet-Diskussionen
auftauchten. Die Originalzitate sind englisch und wurden vom Autor dieses Dokuments
ins Deutsche übersetzt.
• über die Schwierigkeit von C und Haskell: [15]
C ist nicht schwer; Programmieren in C ist schwer. Auf der anderen
Seite: Haskell ist schwer, aber Programmieren in Haskell ist einfach.
• auf die Frage, was die Vorteile von Haskell gegenüber „halbfunktionalen Sprachen
wie Perl, Common Lisp usw.“ seien: [16]
Für mich? Reinheit. [(engl. purity) Gemeint ist die rein funktionale Programmierung im Gegensatz zur „unreinen“.] Ich will damit sagen, dass
du zwar eine Menge der Vorteile funktionaler Programmierung in jeder
alten Sprache bekommen kannst (siehe C# 3.0), aber das Eine, was du
niemals bekommst, indem du lediglich Unterstützung für einen „funktionalen Stil“ in eine andere Sprache einbaust, ist Reinheit. Sobald die
Reinheit weg ist, ist sie weg! Sie kann nicht in einer vorhandenen Sprache nachgerüstet werden.
Reinheit ist großartig, denn sie macht es viel einfacher, Programme zu
schreiben, ohne dabei dumme Fehler zu machen. Wenn du Programme in Sprachen mit vielen Seiteneffekten schreibst, musst du quasi in
deinem Kopf ein „geistiges Protokoll“ mit allen möglichen Ausführungspfaden („In diesem Zweig ist x gleich y plus w und dieser Zeiger
hier ist null, in dem anderen Zweig ist x null und . . . “) mitführen. Was
mich anbelangt, so kann ich fast buchstäblich fühlen, wie „Gehirnressourcen“ frei gegeben werden, wenn ich Haskell verwende, was ich
wiederum nutzen kann, um Arbeit schneller zu erledigen. (Oder, vermutlich exakter: Ich kann fühlen, wie viel Gehirnleistung ich mit dem
Führen dieses „geistigen Protokolls“ verschwende, wenn ich Sprachen
wie C++ benutze.)
Reinheit ist auch sehr interessant, wenn du Programme parallelisieren
willst. (Eine reine Funktion kann in jedem Thread zu jeder Zeit ausgeführt werden und wird garantiert niemals die Berechung anderer Funktionen beeinflussen – in unreinen Sprachen [(engl. impure languages)] gilt
das überhaupt nicht.) Das ist wahrscheinlich die Killeranwendung für
funktionale Programmierung, meiner Meinung nach. Funktionale Programmierung ist cool aus mehreren Gründen, aber ich meine, „ist nicht
40
nahezu unbrauchbar in einem Multithreading-Rahmen“ ist das, was sie
am meisten von imperativen Programmiersprachen unterscheidet.
Haskell hat auch STM, was großartig für diese Low-Level-Shared-StateNebenläufigkeit ist, die du manchmal brauchst. (Keine Locks, Monitore
oder irgendetwas dieses nicht modularen, Wahnsinn hervor rufenden
Durcheinanders!)
• auf die Frage, ob Haskells Vorteile nicht „durch seine Komplexität (Monaden
usw.) und seine Strenge überwogen werden“: [14]
Du kennst Entwurfsmuster? Die sind komplex. Dutzende von RubeGoldberg-Maschinen, entworfen zum Umgehen von Unzulänglichkeiten von Sprachen, die wir vor 20 Jahren hätten aufgeben sollen.
Wenn du versuchst, das Entwurfsmusterbuch auf Haskell anzuwenden, löst sich die Hälfte der Entwurfsmuster in Luft auf, weil sie NichtProbleme lösen, die meisten der restlichen Muster werden viel einfacher und nur einige wenige kommmen hinzu. Ein besonders einfaches
neues Muster ist die Monade, welche die Viererbande nicht entdecken
konnte, weil ihnen einen Sprache fehlte, die mächtig genug ist, sie auszudrücken. (Monaden decken ganz einfach das Kompositumsmuster
ab und verallgemeinern und vereinfachen es dabei. Die Anwendung
von Monaden auf I/O ist unkompliziert und damit wird dann auch das
Kommandomuster subsumiert.)
Übrigens ist nichts streng an Haskell. Ich kann meinen Haskell-Code
viel schneller an neue Anforderungen anpassen, als es mit C oder Perl
möglich ist, und der Haskell-Code hat den zusätzlichen Vorteil, dass er
nach der Änderung immernoch funktioniert.
41
Literaturverzeichnis
[1] A, Julie D. (Hrsg.); B, Joe (Hrsg.): The Unicode Standard, Version 5.0.
Addison-Wesley Longman, November 2006. – URL http:// www.unicode.org/ versions/
Unicode5.0.0/ . – Zugriffsdatum: 14. Oktober 2008. – ISBN 978-0321480910
[2] B, John: Can Programming Be Liberated from the von Neumann Style? A
Functional Style and Its Algebra of Programs. In: Communications of the ACM 21
(1978), August, Nr. 8, S. 613–641. – URL http:// portal.acm.org/ citation.cfm?id=359579.
– Zugriffsdatum: 17. Oktober 2008. – ISSN 0001-0782
[3] B, Richard: Introduction to Functional Programming using Haskell. Second edition.
Prentice Hall, Oktober 1998 (Prentice Hall Series in Computer Science). – ISBN 9780134843469
[4] H, Michael u. a.: Curry: An Integrated Functional Logic Language, Version 0.8.2.
Mai 2006. – URL http:// www.informatik.uni-kiel.de/ ~curry/ papers/ report.pdf . – Zugriffsdatum: 15. Oktober 2008
[5] H, Ralf; L̈, Andres: Guide2lhs2TEX (for version 1.14). Oktober 2008. – URL http:
// people.cs.uu.nl/ andres/ lhs2tex/ Guide2-1.14.pdf . – Zugriffsdatum: 17. Oktober 2008
[6] H, Paul; H, John; P J, Simon; W, Philip: A History of
Haskell: Being Lazy With Class. In: Proceedings of the third ACM SIGPLAN conference on History of programming languages. New York: Association for Computing
Machinery, 2007, S. 12-1–12-55. – URL http:// portal.acm.org/ citation.cfm?id=1238856.
– Zugriffsdatum: 17. Oktober 2008. – ISBN 978-1-59593-766-X
[7] H, John: Why Functional Programming Matters. In: The Computer Journal
32 (1989), April, Nr. 2, S. 98–107. – URL http:// www.math.chalmers.se/ ~rjmh/ Papers/
whyfp.pdf . – Zugriffsdatum: 17. Oktober 2008. – ISSN 0010-4620
[8] H, Graham: Programming in Haskell. Cambridge University Press, Januar
2007. – ISBN 978-0521692694
[9] J, Wolfgang: translation of „kind“. Juni 2005. – URL http:// www.haskell.org/
pipermail/ haskell/ 2005-June/ 016008.html. – Zugriffsdatum: 13. November 2008. – EMail
[10] O, Chris: Purely Functional Data Structures. New Edition. Cambridge University Press, August 1999. – ISBN 978-0521663502
42
[11] O’S, Bryan; S, Donald B.; G, John: Real World Haskell. O’Reilly
Media, Dezember 2008. – URL http:// book.realworldhaskell.org/ read/ . – Zugriffsdatum: 16. Oktober 2008. – Das komplette Buch ist kostenlos online erhältlich. –
ISBN 978-0596514983
[12] P J, Simon (Hrsg.): Haskell 98 Language and Libraries: The Revised Report.
Cambridge University Press, April 2003. – URL http:// www.haskell.org/ definition/
haskell98-report.pdf . – Zugriffsdatum: 14. Oktober 2008. – ISBN 978-0521826143
[13] P J, Simon: A Taste of Haskell. Juli 2007. – URL http:// research.microsoft.
com/ ~simonpj/ papers/ haskell-tutorial/ index.htm. – Zugriffsdatum: 17. Oktober 2008. –
Vortragsvideo und Foliensatz
[14] S, Udo: Newbie: what are the advantages of Haskell? April 2007. – URL http:
// www.haskell.org/ pipermail/ haskell/ 2007-April/ 019425.html. – Zugriffsdatum: 15. Oktober 2008. – E-Mail
[15] 17: C vs. Haskell. 2008. – URL http:// www.reddit.com/ r/ programming/ comments/
66ags/ think_lisp_and_c_are_hard_heres_the_source_code/ c02ywve.
–
Zugriffsdatum: 15. Oktober 2008. – E-Mail
[16] S, Sebastian: Newbie: what are the advantages of Haskell? April 2007. – URL http:
// www.haskell.org/ pipermail/ haskell/ 2007-April/ 019392.html. – Zugriffsdatum: 15. Oktober 2008. – E-Mail
[17] T GHC T: The Glorious Glasgow Haskell Compilation System User’s Guide,
Version 6.8.3. Juni 2008. – URL http:// www.haskell.org/ ghc/ docs/ 6.8.3/ html/ users_guide/
index.html. – Zugriffsdatum: 16. Oktober 2008
[18] T H C: Haskell Hierarchical Libraries of GHC 6.8.3. Juni 2007.
– URL http:// www.haskell.org/ ghc/ docs/ 6.8.3/ html/ libraries/ index.html. – Zugriffsdatum: 17. Oktober 2008
[19] T, Peter: Grundlagen der funktionalen Programmierung. 1. Auflage.
B. G. Teubner, 1994 (Leitfäden der Informatik). – ISBN 978-3519021377
43
Index
0
rekursiver, 23–24
Allquantifizierung
explizite, 17
implizite, 17
Anwendung
einer Funktion, 10
partielle, 12
bei Typen, 33
arithmetische Folge, 9
Assembler, 3
Assoziativität
von →, 11
von Funktionsanwendungen, 11
von Infixoperatoren, 16
von λ-Ausdrücken, 11
Aufzählungstyp, 22
Ausdruck, 32
(Apostroph), 7
(), 8, 26
(, ), (, , ) usw., 26–27
∗, 12, 32
+, 12
++, 8, 27
−, 8, 13
:, 8, 27
;, 14
<, 29–30
>, 29–30
=, 13
[ ], 8, 27, 31
_ (Unterstrich), 7, 25
‘ (Backquote), 13
{, 14
|, 12
}, 14
→, 10, 26, 32, 39
←, 9, 39
⇒, 28, 39
¬, 8, 39
∧, 5, 8, 39
∨, 8, 39
≡, 27–30, 39
., 27–28, 39
6, 29–30, 39
>, 29–30, 39
◦, 39
↑, 9, 39
⊥, 15, 39
λ, 10, 26, 39
Backquote, siehe ‘ (Backquote)
Backtick, siehe ‘ (Backquote)
Backus, John Warner, 4
Baum
beliebiger, 23
binärer, 23–25
vollständiger, 23–24
β-Reduktion, 10
Bezeichner, 7
mit Index, 5
qualifizierter, 16
Bindung
einer Variable, siehe Variablenbindung
Binomialkoeffizient, 15–16
Bool, 8, 22
ADT, siehe algebraischer Datentyp
algebraischer Datentyp, 21–27
parametrisierter, 22
C (Programmiersprache), 3, 40, 41
44
C#, 3, 40
C++, 40
Caml, 4
case, 24
Char, 8, 21
Church, Alonzo, 3
class, 27–28
Clean, 4
Common Lisp, 40
compare, 29–30
Curry (Programmiersprache), 11
Curry, Haskell Brooks, 10
Currying, 10–12
bei Typen, 32
FP, 4
Functor, 31–32
Funktion, 9–12
höherer Ordnung, 10
mehrstellige, 10–12
Gang of Four, siehe Viererbande
Generator, 9, 26
GHC, siehe Glasgow Haskell Compiler
Glasgow Haskell Compiler, 5, 8, 17
Gravis, siehe ‘ (Backquote)
GT, 29–30
Guard, siehe Wächter
Haskell 98, 4
Heap, 25–26
data, 21
Datenkonstruktor, 21–22
Deklaration
einer Instanz, 28
einer Klasse, 27–28
einer Variable, 14
eines algebraischen Datentyps, 21
Fixity-, 16
globale, 16
in Listenkomprehension, 15
lokale, 13, 15
Design-Pattern, siehe Entwurfsmuster
Double, 7
if, 8
Import
in Modul, 16
import, 15–16
in, 13
infix, 16
infixl, 16
infixr, 16
Information-Hiding, 15
instance, 28
Instanz, 28
Int, 7
Integer, 7, 21
Einrückungstiefe, 13
else, 8
Entwurfsmuster, 41
EQ, 29–30
Eq, 27–30
Export
aus Modul, 16
Java, 3, 6, 8, 9, 17
Just, 22
Klasse, 27–30
generische (Java), 4, 17
Kleene, Stephen Cole, 3
Kommando (Entwurfsmuster), 41
Kompositum (Entwurfsmuster), 41
Kontext, 28
einer Instanz, 29
einer Klasse, 29–30
eines Typs, 28
Korrektheitsbeweis, 7
Fakultät, 15–16
False, 8, 22
Feld, 21
benanntes, 21
Fibonacci-Zahl, 14–15
Float, 7
fmap, 31
Forth, 3
λ-Ausdruck, 10, 26
45
für mehrstellige Funktion, 11
λ-Kalkül, 3
Layout, 13–14
Lazy Evaluation, siehe verzögerte Auswertung
Lazy ML, 4
LCF, 4
let, 13, 15
lhs2TEX, 4
LISP, 3
Liste, 8–9, 26–27
Listenkomprehension, 9, 12, 15, 26
Literal
für Zahl, 8
für Zeichen, 8
für Zeichenkette, 9
Locking, siehe Sperrsynchronisation
LT, 29–30
Objective Caml, 4
OCaml, siehe Objective Caml
Operator
für Datenkonstruktor, 21–22
für Typkonstruktor, 22
in Muster, 24
Infix-, 12–13, 16
Postfix-, 12
Präfix-, 12
Operatorschnitt, 13
Ord, 29–30
Ordering, 29–30
otherwise, 15
Parallelität, 6, 40
PASCAL, 3
Pattern-Matching, siehe Musteranpassung
Perl, 40, 41
Polymorphie
nicht bei Sorten, 32
parametrische, 4, 17
Postanschrift, 21–22, 24, 26
der BTU Cottbus, 22
PostScript, 3
Prelude, 16
Priorität
von Funktionsanwendungen, 16
von Infixoperatoren, 16
von λ-Ausdrücken, 11
Programmiersprache
constraintbasierte, 3
deklarative, 3
funktionale, 3, 4
imperative, 3, 4, 41
logische, 3
objektorientierte, 3
rein funktionale, 3, 4, 6, 40–41
stapelorientierte, 3
pythagoräisches Zahlentripel, 9
Python, 3
map, 30
max, 29–30
Maybe, 22, 32–33
Methode, 28
Milner, Robin, 4
min, 29–30
Miranda, 4
Mittel
arithmetisches, 11–13
geometrisches, 13
ML, 4
Modul, 15
module, 15–16
Monade, 41
Monitor (Nebenläufigkeit), 41
Monospace-Schrift, 4
Muster, 24
Alias-, 25
Joker-, 25
verschachteltes, 25
Musteranpassung, 24
Namensraum, 15, 22
Nebenläufigkeit, 7, 41
Nothing, 22
Qualifizierer, 9, 15
Record, 22
46
referentielle Transparenz, 6–7
Rekursion, 14
Rube-Goldberg-Maschine, 41
Viererbande, 41
void (Java-Typ), 8
von-Neumann-Rechner, 3
Schönfinkel, Moses Isajewitsch, 11
Schnitt, siehe Operatorschnitt
Seiteneffekt, 3, 5–6, 40
Semantik
nicht-strikte, 3, 4
SML, siehe Standard ML
Softwaretransaktionsspeicher, 7, 41
Sorte, 32–33
Sortenableitung, 32
Sperrsynchronisation, 41
Standard ML, 4
STM (Software Transactional Memory),
siehe Softwaretransaktionsspeicher
Struktur, siehe Record
Wächter
in Listenkomprehension, 9
in Variablendeklaration, 15
Was-passiert-dann-Maschine, siehe Rube-Goldberg-Maschine
where, 15–16
Wochentag, 22
Wurzel
einer quadratischen Gleichung, 14–
15
Zahl
ganze, 7
Gleitkomma-, 8
negative, 8
Zeichen, 8
Zeichenkette, 9
Zeilenumbruch, 13
Zustand
Abhängigkeit von, 3, 6, 40
Testen von Software, 7
then, 8
Thiemann, Peter, 11
True, 8, 22
Tupel, 8, 26–27
Turing Award, 4
Typ
allgemeinster, 17
höherer Ordnung, 32–33
parametrisierter, 16
Typableitung, 17
Typisierung
dynamische, 7
statische, 4, 5, 7, 17
Typklasse, siehe Klasse
Typkonstruktor, 21–22
Typsignatur, 18
Typurteil, 7
Typvariable, 17
Unicode, 8
Unit, siehe ()
Variablenbindung, 6, 13, 15, 25
verzögerte Auswertung, 6
47
Was this manual useful for you? yes no
Thank you for your participation!

* Your assessment is very important for improving the work of artificial intelligence, which forms the content of this project

Download PDF

advertisement