Informatik 2 - Stefan Klinger

Werbung
Informatik 2
Konzepte der Programmierung
(& Programmierkurs 2)
Stefan Klinger
LS Scholl, Datenbanken und Informationssysteme
Universität Konstanz
Sommer 2016
Folien basieren teilweise auf früheren Lehrveranstaltungen von M. Scholl und T. Grust.
0
Einleitung
0 · Einleitung
0.1
Das Modul Informatik 2 · 0.1
Das Modul Informatik 2
Zwei Lehrveranstaltungen:
Konzepte der Programmierung (KdP) Hier werden hauptsächlich die
theoretischen Konzepte vermittelt.
Programmierkurs 2 (PK2) Praktische Diskussion und Anwendung der
Theorie aus KdP.
⇒ Beide Veranstaltungen bilden eine Einheit!
I
Man kann nicht nur an PK2 xor KdP teilnehmen.
I
Massiver Kontakt mit Quellcode auch in der KdP Vorlesung.
I
PK2 greift auf Stoff aus der KdP Vorlesung zurück und vertieft diesen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
3
0 · Einleitung
Das Modul Informatik 2 · 0.1
Informatik 2 im ersten Semester?
Für Erstsemester (= Sommeranfänger) ...
I
ist diese Veranstaltung möglich,
I
empfohlen wird i.d.R. Theoretische Grundlagen der Informatik und
Informatik 2 im dritten Semester.
I
KdP und PK2 sind nicht so praxisorientiert wie sich das anhört.
I
Wer trotzdem möchte, sollte im Brückenkurs Mathematik von Dr. Kosub
gewesen sein.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
4
0 · Einleitung
Das Modul Informatik 2 · 0.1
Wieviel Arbeit ist Informatik 2 ?
Faustregel: Heimarbeit ≈ 2× Vorlesung
I
Konzepte der Programmierung (KdP): 4c
Einheit c für Credits.
I
Programmierkurs 2 (PK2): 5c
I
Nach ECTS1 gilt: 1c ≡ 30h
Einheit h für Stunden.
I
Dieses Semester: 14w
Einheit w für Wochen.
(4c + 5c) · 30 hc
h
≈ 19.29
14w
w
I
4 wh für die Vorlesung KdP (eigentlich nur 3h), plus
I
2 wh für die Vorlesung PK2 (eigentlich nur 1.5h), plus
I
2 wh für das Tutorium (eigentlich nur 1.5h), plus
I
11.29 wh zum Nachbearbeiten und zum Lösen der Übungsblätter.
1 http://de.wikipedia.org/wiki/European_Credit_Transfer_System
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
5
0 · Einleitung
Das Modul Informatik 2 · 0.1
Literatur
I
I
Umfassendes Folienscript. Zusammen mit PK2-Material vollkommen
ausreichend für die Klausur.
Bei weiterem Interesse:
• Richard Bird, Philip Wadler2 . Introduction to Functional Programming using
Haskell. 2. Ausgabe, 1998, Prentice Hall. ISBN 0-13-484346-0.
• J. Mitchell. Concepts in Programming Languages. 2002, Cambridge
University Press. ISBN 978-0-521-78098-8.
2 nur
in der ersten Auflage
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
6
0 · Einleitung
0.2
Koordinaten · 0.2
Koordinaten
Material https://svn.uni-konstanz.de/dbis/inf2_16s/pub
Folien, Übungsblätter, Code. Wird regelmäßig aktualisiert.
Vorlesungen immer 15:15–16:45
PK2 Montag, R513
“Hands on” Programmieren.
KdP Dienstag, A701 und Mittwoch, R513
Theoretische Konzepte und Einführung in Haskell.
⇒ Beide Veranstaltungen bilden eine Einheit!
Tutorien finden Donnerstags und Freitags statt (cf. Seite 10).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
7
0 · Einleitung
Koordinaten · 0.2
Subversion
I
Alle Materialien werden über Subversion3 verwaltet.
Siehe Schlüsselqualifikation, 1. Semester.
I
Aus dem Repository4 sind zwei Unterverzeichnisse für Sie relevant:
/pub Materialien aus der Vorlesung und Übungsblätter, wird von uns
aktualisiert, Sie haben nur Leserechte.
/group/foo Bearbeitung und Abgabe der Lösungen. Jede
Übungsgruppe (hier: foo) bekommt ein Unterverzeichnis. Dort die
Lösungen zu den wöchentlichen Übungen rechtzeitig eincheken!
• Authentifizierung über Uni-ID, also user & passwd wie für Uni-Mail.
• Den Namen der Gruppe erfahren Sie nach der Anmeldung für ein Tutorium.
⇒ Weitere Infos auf dem 1. Übungsblatt.
3 http://subversion.apache.org/
4 https://svn.uni-konstanz.de/dbis/inf2_16s
Stefan Klinger · DBIS
(Leserechte nur in genannten Unterverzeichnissen)
Informatik 2 · Sommer 2016
8
0 · Einleitung
Koordinaten · 0.2
Tutorien
Termine cf. Seite 10
Inhalt
I
Tutoren stellen Musterlösungen vor.
I
Fragen zur eigenen Lösung stellen.
I
Besprechen von Konzepten, die in der Vorlesung nicht ganz klar
geworden sind.
I
Elaborierte Fragen stellen.
I
evtl. Tips von den Tutoren zur Lösung der nächsten Aufgaben.
I
Die Initiative geht von Ihnen aus!
Obacht
I
Voraussetzung: Intensive Auseinandersetzung mit Stoff und Übungsblatt
vor dem Tutorium.
I
Die Übungen lassen sich nicht im Rahmen der Tutorien bearbeiten!
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
9
0 · Einleitung
Koordinaten · 0.2
Personal
Prof. Marc Scholl LS Datenbanken und Informationssysteme (DBIS)
web http://dbis.uni-konstanz.de/
office PZ811
Stefan Klinger Ich halte die Vorlesungen KdP und PK2
mail [email protected]
office PZ804
Die Tutoren
Stefan Erk (A) Do 13:30, E404
[email protected]
Denis Gietz (D) Fr 8:15, F428
[email protected]
Johannes Fuchs evtl. zwei Tutorien:
(B) Do 15:15, F428, und
(C) Do 17:00, F428
[email protected]
Robert Schmid (E) Fr 10:00, F429
[email protected]
Fabian Späh (F) Fr 11:45, G308
[email protected]
Der im LSF angegebene Termin G, Fr 15:15 wird nicht angeboten.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
10
0 · Einleitung
0.3
I
Prüfung · 0.3
Prüfung
Eine gemeinsame Klausur für PK2 und KdP.
• Nur bei Bestehen der Klausur werden die ECTS-Punkte für PK2 und KdP
gutgeschrieben.
I
Ein Übungsblatt pro Woche (für PK2 und KdP zusammen).
•
•
•
•
I
Ausgabe am Montag, Abgabe bis jeweils nächsten Montag, 15:00.
Blätter werden in 2er-Teams bearbeitet.
Klausur am Ende des Semesters bestimmt Note für KdP.
Klausurzulassung ⇒ 50% der Übungspunkte erreicht.
Ab diesem Semester ist die Prüfungsanmeldung (cf. Seite 12) zwingend
notwendig.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
11
0 · Einleitung
0.4
Anmeldung · 0.4
Anmeldung — Wichtig
← das ist nicht zum Spaß rot
Prüfungsanmeldung Sie müssen sich innerhalb des Anmeldezeitraums5
via StudIS6 verbindlich zur Prüfung anmelden. Und zwar für beide
Veranstaltungen, KdP und PK2.
cf. Info des Fachbereichs
Diese Woche Anmeldung per Mail, wie auf dem ersten Übungsblatt
beschrieben7 .
Frist: Mittwoch Abend, 18 Uhr
I
I
Bildung der 2er-Teams für die Übungen, und Verteilung auf die Tutorien.
Wenn sie sich frühzeitig anmelden, haben Vorrang bei der Terminwahl:
• Eltern mit StEP8 , und
• Fachfremde bei nachgewiesener Kollision mit einer anderen Pflichtvorlesung.
5 http://www.informatik.uni-konstanz.de/studieren/studium/pos-pruefungsinformationen/
pruefungsanmeldung/
6 https://studis.uni-konstanz.de/
7 https://svn.uni-konstanz.de/dbis/inf2_16s/pub/assignment01.pdf
8 http://www.familie.uni-konstanz.de/programme-fuer-eltern/studieren-mit-kind/
der-studierenden-elternpass/
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
12
0 · Einleitung
Anmeldung · 0.4
Noch Fragen zur Organisation?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
13
0 · Einleitung
0.5
Konzepte der Programmierung · 0.5
Konzepte der Programmierung
Je nach Zählung gibt es >50 Paradigmen (Konzepte), die sich nicht
gegenseitig ausschließen.
Eine übliche Einteilung
Imperative Sprachen
mit prominenten Vertretern:
I
• Strukturiert: Python, C, C++, Java
Beispiel cf. Seite 15
• Objektorientiert: Python, C++, Java
I
Deklarative Sprachen
• Funktionale Sprachen: Haskell, Erlang, JS
Beispiel cf. Seite 18
Haskell entstand, um state-of-the-art FPL-Forschung in einer Sprache zusammenzuführen.
Prominente Entwickler: Simon Peyton Jones, Philip Wadler, ...
• Logische Sprachen: Prolog, Datalog
Beispiel cf. Seite 19
Programmation en Logique. Initial: Alain Colmerauer, 1972. Viele Ableger.
• Datenbanksprachen wie SQL oder XQuery
cf. Modul Datenbanken!
Werfen wir einen kurzen Blick auf drei Vertreter...
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
14
0 · Einleitung
Konzepte der Programmierung · 0.5
Imperativ — Quicksort in Java
1
private static void quicksort(int[] a, int from, int to) {
2
if (to - from > 0) {
3
4
int pivot = to; int l = from, r = to - 1;
5
6
while (l < r) {
while ((l < r) && (a[l] <= a[pivot])) { l++; }
while ((l < r) && (a[r] >= a[pivot])) { r--; }
if (l != r) {
swap(a, l, r);
} else if (a[l] > a[pivot]) {
swap(a, l, pivot);
}
}
7
8
9
10
11
12
13
14
15
16
quicksort(a, from, l - 1);
quicksort(a, l + 1, to);
17
18
19
}
20
21
}
Code aus der KdI-Vorlesung von Dr. Meinl
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
15
0 · Einleitung
Frage
Konzepte der Programmierung · 0.5
Was passiert da?
1. Auswahl eines beliebigen Elements der Liste, sog. Pivotelement p.
2. Verschieben des Pivotlements in der Liste, so dass
• alle Elemente links von p kleiner als p sind, und
• alle Elemente rechts von p größer oder gleich p sind.
⇒ Damit ist das Pivotelement bereits an seiner endgültigen Position.
3. Rekursives Sortieren der linken und rechten Teilliste.
I
I
Als Pivotelement kann man z.B. das rechteste Element der Liste wählen.
Verschieben des Pivotelements:
1.
2.
3.
4.
5.
Suche von links nach einem Element xi mit xi > p,
suche von rechts nach einem Element xj mit xj < p,
und vertausche die beiden Elemente.
Solange wiederholen, bis sich i und j treffen.
Pivotelement mit Element an Position i vertauschen, falls p < xi .
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
16
0 · Einleitung
Konzepte der Programmierung · 0.5
Beispiel:
I
512 170
61
897 908
87
503 415
512 170
61
897 908
87
503 415
87
170
61
897 908 512 503 415
87
170
61
415 908 512 503 897
Jetzt noch rekursiv die beiden Teillisten links und rechts von 415
sortieren.
Grafik aus der KdI-Vorlesung von Dr. Meinl
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
17
0 · Einleitung
Konzepte der Programmierung · 0.5
Funktional — Quicksort in Haskell
1
quicksort [] = []
2
3
4
quicksort (x:xs)
= quicksort (filter (<x) xs)
++
[x]
++
quicksort (filter (>=x) xs)
Dabei ist
I
•
•
•
•
•
[] die leere Liste,
(x:xs) die Liste mit erstem Element x und Restliste xs,
[x] die Liste mit einem Element x,
++ die Listen-Konkatenation,
filter p filtert die Elemente aus einer Liste, die p erfüllen.
Benutzung:
I
1
2
*Main> quicksort [5,7,2,6,8,1,4,3,0,1]
[0,1,1,2,3,4,5,6,7,8]
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
18
0 · Einleitung
Konzepte der Programmierung · 0.5
Logisch — Pfadsuche in Prolog
Ein Prolog-Programm besteht aus Fakten und Regeln. Etwa:
Fakten: Eine Datenbank mit Flugverbindungen.
I
gla
1
2
3
4
5
leg(fdh,
leg(fra,
leg(sin,
leg(cgn,
leg(gla,
cgn).
sin).
syd).
sin).
sin).
leg(fdh,
leg(fra,
leg(sin,
leg(cgn,
fra).
cgn).
chc).
gla).
chc
cgn
sin
fdh
syd
fra
Regeln: Fliegen mit Zwischenstop.
I
6
7
I
route(A, B, [])
:- leg(A, B).
route(A, B, [X|XS]) :- leg(A, X), route(X, B, XS).
Gegen diese Wissensbasis können Anfragen formuliert werden:
Über welche Route Q komme
ich von Frankfurt nach
Singapore?
Stefan Klinger · DBIS
1
2
3
4
5
?- route(fra, sin, Q).
Q = [] ;
Q = [cgn] ;
Q = [cgn, gla] ;
false.
Informatik 2 · Sommer 2016
19
0 · Einleitung
I
Konzepte der Programmierung · 0.5
Diese Anfragen können auch mehrere freie Variablen aufweisen:
1
Wohin ab Friedrichshafen
mit genau einem
Zwischenstopp?
2
3
4
5
6
?- route(fdh, B,
B = sin, X = cgn
B = gla, X = cgn
B = sin, X = fra
B = cgn, X = fra
false.
[X]).
;
;
;
;
• Hier steht [X] für eine Liste mit nur einem Element X.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
20
0 · Einleitung
Konzepte der Programmierung · 0.5
Erste Beobachtungen
I
I
Die gezeigten Sprachen
unterscheiden sich stark: Sie
“sehen anders aus”.
Auch das Denkmodell ist jeweils
ein anderes: Sie verwenden nicht
nur andere Worte, sondern ganz
andere Konzepte.
I
In Prolog: wo ist der
Algorithmus. . . ?
I
Deklarative Sprachen bieten sehr
kompakte Notation.
I
(Geordnete) Listen als
“eingebaute” Datenstruktur.
I
Pattern Matching.
I
Alternative Definitionszweige.
Deklarative Programmiersprachen (im Ggs. zu imperativen):
Gemeinsam ist allen deklarativen Sprachen, dass die Lösung eines Problems
I
(eher) auf einer hohen Abstraktionsebene spezifiziert,
I
als auf einer niedrigen (Maschinen-) Ebene “ausprogrammiert” wird.
⇒ “Was und nicht wie.”
Stefan Klinger · DBIS
(Offensichtlich ist diese Unterscheidung etwas unscharf.)
Informatik 2 · Sommer 2016
21
0 · Einleitung
0.6
I
Inhalte der Vorlesung · 0.6
Inhalte der Vorlesung
Eine rein funktionale Sprache lernen: Haskell
Dadurch:
Einen Einblick in verschiedene Konzepte von Programmiersprachen
bekommen.
I
• Neue, Ihnen vermutlich unbekannte Konzepte werden eingeführt.
Typinferenz, Funktionen höherer Ordnung, “unendliche” Datenstrukturen ...
• Bekannte Konzepte stehen plötzlich nicht mehr zur Verfügung,
Zuweisung, Seiteneffekte, unsauberer Umgang mit Typen, ...
• oder sind ganz anders ausgeprägt.
Auswertestrategie, I/O, Polymorphie...
I
Wir werden uns meist auf das rein funktionale Paradigma
konzentrieren, mit dem Ziel die oben “unscharf” beschriebene
Unterscheidung klarer herauszuarbeiten.
I
Ein wenig die mathematischen Grundlagen von Programmiersprachen
beschnuppern.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
22
1
Syntax & Semantik
42 - (2+1) *7
1 · Syntax & Semantik
Was bedeutet
I
42 - (2+1) *7
— und wieso?
Der Mensch erkennt:
• Zahlen: 42, 7, 2, 1.
• Bekannte Operatoren: −, ·, +.
• Bekannte Rechenregeln: Punkt vor Strich, von links nach rechts, Klammerung.
I
Im Geist entwickeln wir die Struktur, und rechnen (reduzieren den
Baum) schrittweise entsprechend der Bedeutung:
−
−
·
42
+
2
7
1
−
·
_ 42
3
_
7
42
21
_ 21
Die Frage Wie “versteht” eine Maschine eine Programmiersprache?
Mittels welcher Mechanik können Programmiersprachen überhaupt etwas
ausdrücken?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
24
1 · Syntax & Semantik
1.1
I
I
Übersetzungsphasen · 1.1
Übersetzungsphasen
Ein Compiler (dt.: “Übersetzer”) ist ein Programm, das Sourcecode
(“Quellcode”, in einer Programmiersprache geschriebenes Programm) in
eine andere Sprache übersetzt.
Typische Phasen beim “Verstehen” eines Programmes:
1.
2.
3.
4.
5.
Lexikalische Analyse
Syntaktische Analyse
Semantische Analyse
Optimierungen
Auswerten / Erzeugen von Code
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
25
1 · Syntax & Semantik
Übersetzungsphasen · 1.1
Lexikalische Analyse
(aka. Lexing)
Zerlegen der Eingabe 42 - (2+1) *7 in die Worte (Token) der Sprache:
4 2
-
( 2 + 1 )
* 7
7→
42 - ( 2 + 1 ) * 7
I
Man sagt: Transformiert den Byte-Stream in den Token-Stream.
I
In dieser Phase wird die “Natur” der einzelnen Worte erkannt:
• Erkennt welche Zeichengruppen Literale9 bilden, z.B. Zahlen (3.14, -23),
Strings ("hello world"), boolesche Werte (true), ...
• Unterscheidet z.B. Variablen (x, foo) von Schlüsselworten (if, let,
return, ...):
I
Entfernt unnötige Leerzeichen, Zeilenumbrüche und Kommentare.
1
2
3
9 Worte
for (i = 0; i<23; i++) {
print(i); // Ausgabe
}
7→
for ( i = 0 ; i < 23 ;
i ++ ) { print ( i ) ; }
die für einen primitiven Wert stehen
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
26
1 · Syntax & Semantik
Übersetzungsphasen · 1.1
Syntaktische Analyse
(aka. Parsing)
I
Folgen die Worte der Eingabe einer bestimmten Struktur?
• Mit anderen Worten: Wird die Grammatik der Sprache erfüllt?
I
Diese Phase rekonstruiert die Struktur der Ausdrucks.
• Entfernt Interpunktion (Klammern), die Struktur ist jetzt explizit!
• Liefert den Abstract Syntax Tree (AST), der die Struktur des Ausdrucks
repräsentiert..
42 - ( 2 + 1 ) * 7
7→
42
*
7
+
2
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
1
27
1 · Syntax & Semantik
Übersetzungsphasen · 1.1
Weitere Schritte: Was passiert mit dem Programm?
I
Semantische Analyse, z.B.:
• Werden nur definierte Funktionen aufgerufen?
• Typprüfung (42+2 vs. 42+true),
• ...
I
Ab hier sind verschiedene Schritte möglich, z.B.:
• Code in einer anderen Sprache erzeugen:
I
I
I
Maschinencode der direkt auf einer CPU ausgeführt werden kann.
Eine wesentlich primitivere Sprache, die schlecht von Menschen, aber sehr
einfach von weiteren Phasen verstanden wird, z.B. Assembler oder Java
Bytecode. (Intermediate Code)
Eine andere Hochsprache für die effiziente Auswertemechanismen zur
Verfügung stehen (früher gab es einen Haskell → C Compiler).
• Statt dessen können die Anweisungen von einem Programm auch ausgeführt
(interpretiert) werden. Dann sagt man nicht “Compiler”, sondern
“Interpreter”.
Oft finden Optimierungen als Zwischenschritte statt (Ziel: Weniger
Resourcenverbrauch (Rechenleistung, Speicher) des Programmes).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
28
1 · Syntax & Semantik
Übersetzungsphasen · 1.1
Diese Vorlesung Wir kümmern uns nicht darum, wie man Compiler baut,
Code optimiert oder generiert.
→ Vorlesung Compilerbau.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
29
1 · Syntax & Semantik
Übersetzungsphasen · 1.1
Zurück zur Frage
Was bedeutet
42 - (2+1) *7
— und wieso?
(Wir haben bisher beschrieben was wir tun wollen, aber nicht wie das gehen soll.)
I
Wie kann man Syntax und Grammatik einer Programmiersprache
beschreiben?
I
Wie kann man aus dem “geschriebenen Wort” die Struktur des
Programmes rekonstruieren?
I
Wie kommt man von der Struktur zur Bedeutung?
Im Folgenden befassen wir uns mit der Beschreibung von Syntax und
Grammatik und dem Erkennen der Struktur.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
30
1 · Syntax & Semantik
1.2
Reguläre Ausdrücke · 1.2
Reguläre Ausdrücke
Definition RegEx A — Reguläre Ausdrücke über dem Alphabet A
Sei x ∈ A, und seien r , s ∈ RegEx A, dann sind ebenfalls in RegEx A:
I
x — Jeder einzelne Buchstabe ist auch ein Regulärer Ausdruck. Dieser
beschreibt genau die Zeichenkette mit diesem einen Buchstaben.
I
r ∗ — Wiederholung. Beschreibt genau die Zeichenketten, die sich durch
beliebig häufiges (0 ≤ n < ∞) Hintereinanderschreiben jeweils von r
beschriebener Zeichenketten bilden lassen.
I
rs — Konkatenation. Beschreibt genau die Zeichenketten, die sich
durch Hintereinanderschreiben einer von r , und einer von s beschriebenen
Zeichenkette (in dieser Reihenfolge) bilden lassen.
I
r |s — Alternative. Beschreibt genau die Zeichenketten, die durch r oder
s beschrieben werden.
Notation Die Operatoren sind mit absteigender Präzedenz gelistet, wir
verwenden Klammern zum Gruppieren. Sei die leere Konkatenation.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
31
1 · Syntax & Semantik
Reguläre Ausdrücke · 1.2
Einfache Beispiele
I
2015|6|7 — Beschreibt die Zeichenfolgen 2015, 6, und 7.
I
201(5|6|7) — Beschreibt die Zeichenfolgen 2015, 2016, und 2017.
I
fo∗ — Beschreibt foo, aber nicht fofofo.
I
Dezimale Darstellungen der ganzen Zahlen, ohne führende Null, über
einem Alphabet A ⊇ {0, ..., 9, -}:
0 (-|) (1|2|3|4|5|6|7|8|9) (0|1|2|3|4|5|6|7|8|9)∗
Notation Oft findet man abkürzende Schreibweisen (r ∈ RegEx A):
I
r ? = r | — Optional.
I
r + = rr ∗ — Mindestens eine Wiederholung.
I
[a − z] = a|b|...|z — genau eines der Zeichen a, b, ..., z ∈ A.
Oft mehrere Zeichen und/oder Bereiche:
[a − d k − m p q] = a|b|c|d k|l|mp|q
Dann lautet das Beispiel für ganze Zahlen:
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
0 -? [1 − 9] [0 − 9]∗
32
1 · Syntax & Semantik
Reguläre Ausdrücke · 1.2
Beispiel: Arithmetische Ausdrücke
(ohne Klammern)
Arithmetische Ausdrücke mit den üblichen Operatoren und ohne
Klammern könnte man so beschreiben:
∗
(0-? [1 − 9][0 − 9]∗ ) (+|-|*|/) (0-? [1 − 9][0 − 9]∗ )
∗
also etwa: Zahl (+|-|*|/) Zahl
Problem Tatsächlich kann man prinzipiell keinen10 RegEx angeben, der
irgendeine Sprache beschreibt, die auf korrekte Klammerung angewiesen ist.
⇒ Es gibt keinen RegEx, der genau die Arithmetischen Ausdrücke mit
korrekter Klammerung beschreibt.
10 Beweis
mit dem Pumping Lemma aus der theoretischen Informatik.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
33
1 · Syntax & Semantik
Grammatik · 1.3
Grammatik
1.3
(gibt uns die Möglichkeit zu klammern)
Definition Grammatik
Weiterführendes Wissen cf. Seite 52
Eine Grammatik über einem Alphabet A besteht aus
I
einer Menge N von Nonterminalen11 , N ∩ A = ∅,
(im Gegensatz dazu werden die Elemente von A auch Terminale genannt.)
I
einer Menge von Produktionen der Form
n→r
wobei n ∈ N , und r ∈ RegEx(A ∪ N ), sowie
I
einem als Startpunkt ausgezeichneten Nonterminal (hier mit ? markiert).
Beispiel Grammatik für Boolesche Ausdrücke (BoolEx), mit Klammern.
Alphabet A = {T, F, &, |, !, (, )}, Nonterminale N = {Op, BoolEx}:
Op → & | |
? BoolEx → T | F | ( BoolEx Op BoolEx ) | ! BoolEx
11 die
man sich ganz grob als “Abkürzungen” für reguläre Ausdrücke vorstellen kann.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
34
1 · Syntax & Semantik
Grammatik · 1.3
Eine Folge von Zeichen aus dem Alphabet A erfüllt eine Grammatik, wenn
man sie durch Anwendung der Produktionsregeln vom Startsymbol
ableiten kann:
I
→
→
→
→
→
→
→
BoolEx
Zur besseren Lesbarkeit schreibt man
die Alternativen oft untereinander:
Op → &
| |
( BoolEx Op BoolEx )
(T Op BoolEx )
(T& BoolEx )
(T&( BoolEx Op BoolEx ))
? BoolEx →
|
|
|
T
F
( BoolEx Op BoolEx )
! BoolEx
(T&(T Op BoolEx ))
(T&(T| BoolEx ))
(T&(T|F))
Stefan Klinger · DBIS
I
Die Zeichenfolge (T&(T|F)) erfüllt
also offenbar die Grammatik.
I
Unsere Grammatik erlaubt keine
Leerzeichen (sogar: 6∈ A).
Informatik 2 · Sommer 2016
35
1 · Syntax & Semantik
Grammatik · 1.3
Ableitung liefert Struktur
I
Bei der Ableitung vom Startsymbol entsteht der Parse Tree (links).
&
BoolEx
T
|
BoolEx
T
(
I
BoolEx
Op
T
&
(
BoolEx
Op
BoolEx
T
|
F
)
F
)
Der Parse-Tree enthält bereits die Struktur des Ausdrucks (rechts):
• Der |-Operator gehört zu dem BoolEx welcher rechtes Argument des
BoolEx mit dem &-Operator ist.
I
Der Strukturbaum rechts heißt Abstract Syntax Tree, AST.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
36
1 · Syntax & Semantik
Grammatik · 1.3
Leerzeichen
Eine Grammatik aufzuschreiben die alle Varianten von erlaubten
Leerzeichen in der Eingabe berücksichtigt, ist manchmal recht aufwändig.
Oft wendet man ein zweistufiges Verfahren an:
1. Lexikalische Analyse übersetzt Folge von Bytes (incl. Leerzeichen &
Kommentare) in Folge von Token (ohne diese).
2. Syntaktische Analyse: Die Grammatik wird dann nicht mehr über dem
Alphabet der Bytes, sondern dem Alphabet der Token definiert:
A = Z ∪ {+, -, *, /, (, )}
? ArithEx → Z
| ( ArithEx Op ArithEx )
Op → + | - | * | /
I
I
Leerzeichen spielen bei dieser Betrachtung keine Rolle mehr!
Etwas salopp (schlampig) wird hier die Menge Z auch als Nonterminal
aufgefasst, ihre Elemente als Terminale.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
37
1 · Syntax & Semantik
→
→
→
→∗
→∗
→∗
Grammatik · 1.3
ArithEx
(ArithEx Op ArithEx)
(ArithEx Op (ArithEx Op ArithEx))
(ArithEx Op ((ArithEx Op ArithEx) Op ArithEx))
(ArithEx - ((ArithEx + ArithEx) * ArithEx))
(Z - ((Z + Z) * Z))
(42 - ((2 + 1) * 7))
Verbleibendes Problem Unangenehm viele Klammern, ein Paar für jeden
Operator! Man sagt, die Ausdrücke sind vollständig geklammert.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
38
1 · Syntax & Semantik
1.4
Klammern und Konvention · 1.4
Klammern und Konvention
Ein Hilfsmittel ausserhalb der Grammatik:
I
Konvention von Präzedenz und Assoziativität der Operatoren, und
I
die Erlaubnis Klammern wegzulassen wenn ihre Position durch
Konvention feststeht.
42 - 23 + 2 * 7
— Schreibweise nicht durch Grammatik gedeckt!
Konventionen wie Punkt-vor-Strich und links-Assoziativität der
Operatoren machen daraus:
((42 - 23) + (2 * 7))
Die Klammerung in 42 - 23 + 2 * 7 ist aufgrund der vereinbarten Konvention
implizit gegeben (Konvention impliziert Klammerung, im Gegensatz zur
expliziten, d.h. ausdrücklich angegebenen Klammerung).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
39
1 · Syntax & Semantik
Klammern und Konvention · 1.4
Operatortabelle
Diese Konventionen gibt man oft in einer Operatortabelle an:
Präzedenz
8
7
6
I
I
Operatoren
^
*, /
+, -
Die Präzedenz (aka. Priorität) wird oft in Form einer Zahl angegeben.
Die Assoziativität gibt an, ob Operatoren der gleichen Priorität nach
links oder nach rechts geklammert werden.
Man kann z.B. den
Haskell-Interpreter danach fragen:
I
Assoziativität
rechts
links
links
1
2
Prelude> :info + — Info über + abfragen
infixl 6 + — linksassoziativ, Priorität 6
Explizites Klammern ist dann nur noch nötig, wenn die gewünschte
Struktur nicht von der Konvention erzeugt wird:
((1 - (2 + 3)) + (4 * 5))
(implizite Klammern hier in Hellgrau)
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
40
1 · Syntax & Semantik
Klammern und Konvention · 1.4
Strukturbeschreibungen
I
Manchmal sieht man Grammatiken wie die folgende:
Op → + | - | * | /
? Expr → Z
| Expr Op Expr
I
Diese Grammatik alleine reicht allerdings nicht aus um die Struktur eines
Ausdrucks zu erkennen: Es gibt Ausdrücke mit verschiedenen
Ableitungen (Parse-Trees), die Grammatik ist nicht eindeutig!
• Aufgabe: Zeige für die Tokenfolge
10 - 6 - 2
zwei verschiedene
Ableitungen vom Startsymbol Expr.
I
Trozdem kann man mit solchen Spezifikationen arbeiten (oft leichter,
weil übersichtlicher), man braucht allerdings Klammern und eine
Operatortabelle wie oben. Die Klammern sind dann Teil der
Metasprache (cf. Seite 45) weil sie nicht durch die Grammatik
spezifiziert sind.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
41
1 · Syntax & Semantik
Klammern und Konvention · 1.4
Man kann Grammatiken konstruieren, die auch ohne Konventionen nur
die notwendigen Klammern erzwingen:
? Sum → Sum ( + | - ) Product
| Product
Product → Product ( * | / ) Singleton
| Singleton
Singleton → Z
| ( Sum )
I
Ein Parser-Generator (aka. Compiler-Compiler) kann so eine
Spezifikation verstehen und automatisch einen Parser daraus generieren.
• Für uns Menschen ist das eher schwer verständlich! 1213
• Wir verwenden lieber “übersichtlichere” Grammatiken mit Konventionen.
• Noch häufiger lernen wir Programmiersprachen anhand von Beispielen. Nur
für die “interessanten Randfälle” schauen wir in der Sprachdefinition nach!
12 https://www.haskell.org/onlinereport/haskell2010/haskellch10.html
13 http://docs.oracle.com/javase/specs/jls/se8/html/jls-2.html
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
42
1 · Syntax & Semantik
Klammern und Konvention · 1.4
Grammatik gut, alles gut?
I
Wir haben gesehen, dass eine einfache Grammatik nicht alle
Informationen über einen Ausdruck bereitstellen kann die wir für die
Auswertung gerne hätten.
I
Eine Grammatik für Arithmetische Ausdrücke, die nur die nötigen
Klammern enthält, ist schon arg umständlich, cf. Seite 42.
I
Im Allgemeinen können anhand der Grammatik nicht alle gewünschten
Einschränkungen überprüft werden:
2/(42 − 2 ∗ 21)
— Division durch Null!
Später werden wir ein Typsystem als weiteren Mechanismus kennen lernen,
der die Menge der akzeptablen Programme noch genauer festlegt.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
43
1 · Syntax & Semantik
1.5
Semantik — die Lehre von der Bedeutung · 1.5
Semantik — die Lehre von der Bedeutung
Bleibt eine Frage zu klären:
−
Was bedeutet eigentlich
·
42
+
2
— und wieso?
7
1
I
Wir haben die Eingabe14 42 - ( 2 + 1 ) * 7 schrittweise auf den
AST zurückgeführt, und seine Bedeutung einfach angenommen.
I
Tatsächlich haben wir nur die Bedeutung der Objektsprache auf einer
Metaebene15 erklärt, mit Hilfe einer Metasprache (gemalte
Bäumchen).
I
Wie kommen wir vom Gemälde zur Bedeutung?
14 Hier schon als Token-Stream aufgefasst.
15 μετά, griechisch, etwa “hinter” oder “über”.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
44
1 · Syntax & Semantik
Semantik — die Lehre von der Bedeutung · 1.5
Die Metaebene
Frage
Wie können wir überhaupt über Sprachen sprechen?
I
Die Objektsprache ist der Gegenstand (das Objekt) unserer Diskussion.
I
In der Metasprache sprechen wir über diesen Gegenstand, also über die
Objektsprache.
Beispiele für Sprachebenen (Notation:
Metasprache
Objektsprache ).
Deutsch
Englisch
“Long story short.” ist eine englische Redewendung.
Deutsch
Deutsch
Der Zungenbrecher “Blaukraut bleibt Blaukraut und
Brautkleid breibt Blautkreid.” ist auch schwer zu tippen.
Deutsch
ArithEx
42-(2+1)*7 ist das Siebenfache der Summe von zwei und
eins, abgezogen von zweiundvierzig.
Mathematik
BoolEx
Stefan Klinger · DBIS
(T&(T|F)) =
ˆ True ∧ (True ∨ False)
Informatik 2 · Sommer 2016
45
1 · Syntax & Semantik
Semantik — die Lehre von der Bedeutung · 1.5
Meta- und Meta-Metaebene
I
Die gleiche Sprache kann auf Objekt- und Metaebene verwendet
werden (cf. Blaukraut-Beispiel)
I
Die Unterscheidung zwischen Objekt- und Metaebene ist nicht immer
ganz einfach:
• Konstanz hat 80’000 Einwohner.
• Konstanz hat 8 Buchstaben.
I
— spricht über eine Stadt
— spricht über ein Wort
Oft (immer) treten mehrere Ebenen (Meta-Metaebene, ...) auf:
• Im vorigen Punkt wird (Meta2 -Ebene) ein Satz analysiert, der wiederum
(Metaebene) die Anzahl der Buchstaben eines Wortes (Objekt) beschreibt.
• Das war eine Aussage (Meta3 ) über diese Folie...
I
Wir werden die Frage nach der “wirklichen Bedeutung” also nicht
abschließend klären.
(Fragen Sie einen Philosophen Ihres Vertrauens.)
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
46
1 · Syntax & Semantik
Semantik — die Lehre von der Bedeutung · 1.5
Semantische Klammern
I
Zur Abgrenzung der verschiedenen Ebenen werden Ausdrücke der
Objektsprache oft in semantische Klammern gesetzt.
J(T&(T|F))K ≡ True ∧ (True ∨ False)
I
Man kann sich JeK als Strukturbaum des Ausdrucks e vorstellen, über
dessen Bedeutung hier eine Aussage getroffen wird.
• Die genaue Bedeutung der Semantischen Klammern variiert jedoch je nach
Kontext und Autor, und wird nur bei Bedarf jeweils exakt definiert.
• Tatsächlich werden auch wir J·K in unterschiedlichen Zusammenhängen
unterschiedlich verwenden...
Problem Leider können wir noch nicht die Bedeutung aller möglichen
BoolEx beschreiben, denn es gibt unendlich viele davon, die können wir
nicht aufzählen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
47
1 · Syntax & Semantik
Semantik — die Lehre von der Bedeutung · 1.5
Metavariablen
I
So wie man mit J·K Terme der Objektsprache in Terme der Metasprache
einbetten kann, möchte man auch oft Variablen der Metasprache in
die Objektsprache einbetten.
Beispiel Die Semantik von BoolEx: Seien b1 , b2 ∈ BoolEx.
JTK ≡ True
JFK ≡ False
J(b1 & b2 )K ≡ Jb1 K ∧ Jb2 K
J(b1 | b2 )K ≡ Jb1 K ∨ Jb2 K
J!b1 K ≡ ¬ Jb1 K
I
Hier sind b1 und b2 Metavariablen: Das Token b1 gehört nicht zur
Objektsprache BoolEx, sondern zur Metaebene, und repräsentiert einen
(festen, aber beliebigen) Ausdruck in BoolEx.
I
z.B. muss man sich zunächst über die Bedeutung der mit b1 und b2
benannten Ausdrücke klar weden, bevor man daraus (mittels ∧) die
Bedeutung von b1 & b2 bestimmen kann.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
48
1 · Syntax & Semantik
I
Semantik — die Lehre von der Bedeutung · 1.5
Mit diesen Regeln kann man die
Bedeutung eines Ausdrucks bestimmen.
Nochmal die Regeln von der vorigen Folie:
JTK
JFK
J(b1 & b2 )K
J(b1 | b2 )K
J!b1 K
I
≡
≡
≡
≡
≡
True
False
Jb1 K ∧ Jb2 K
Jb1 K ∨ Jb2 K
¬ Jb1 K
Im 1. Schritt steht z.B. die Metavariable
b2 für den Teilausdruck (T|F).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
≡
≡
≡
≡
J(T&(T|F))K
Regel für &
JTK ∧ J(T|F)K
Regel für |
JTK ∧ (JTK ∨ JFK)
Regel für T
True ∧ (True ∨ JFK)
Regel für F
True ∧ (True ∨ False)
49
1 · Syntax & Semantik
I
Semantik — die Lehre von der Bedeutung · 1.5
Sehr oft ist “klar” was zur Meta-, und was zur Objektsprache gehört.
• Semantische Klammern werden dann meist weggelassen.
• Man kann je Ebene eine andere Schriftart, oder einen anderen Zeichenvorrat
verwenden.
I
Auch werden manchmal Symbole (z.B. Zahlen) mit Ihrer Bedeutung in
mehreren Ebenen verwendet, ohne sie separat zu kennzeichnen.
I
Auch in der Mathematik kann es helfen einen Schritt zurückzutreten
und sich zu fragen
• worüber man eigentlich argumentiert (“Was ist das Objekt?”),
• und mit welchen Mitteln man das tut (“Wie funktioniert die Metasprache?”).
I
Im Lauf der Zeit werden Sie ganz selbstverständlich und automatisch
mit mehreren Metaebenen gleichzeitig jonglieren...
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
50
1 · Syntax & Semantik
Semantik — die Lehre von der Bedeutung · 1.5
Ausblick: Operationale und Denotationelle Semantik
I
Die bisherige Methode Semantik zu beschreiben ist Teil der sogenannten
Denotationellen Semantik.
• Ende der 1960er von Christopher Strachey und Dana Scott zur Beschreibung
der Semantik von imperativen Sprachen entwickelt.
• Die semantischen Klammern J·K heißen auch Strachey brackets.
I
Eine andere Art Semantik zu beschreiben, ist die Operationale
Semantik: Die Metasprache beschreibt dabei die Rechenregeln der
Objektsprache, ohne Bezug zu einer “Bedeutung” auf einer Metaebene.
Beispiel Die Operationale Semantik von BoolEx: Sei b ∈ BoolEx.
T &b _ b
F &b _ F
T |b _ T
F |b _ b
!T _ F
!F _ T
Wie genau man damit arbeiten kann, werden wir aber erst im Kapitel über
den λ-Kalkül lernen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
51
1 · Syntax & Semantik
1.6
I
Weiterführendes Wissen · 1.6
Weiterführendes Wissen
In diesem Kapitel wurde eine spezielle Art von Grammatiken verwendet,
nämlich die sog. Kontextfreien Grammatiken (CFG).
• Es gibt unterschiedlich mächtige (ausdrucksstarke) Arten von
Grammatiken.
• Die Chomsky Hierarchie liefert eine entsprechende Klassifizierung.
I
Unsere Notation für CFGn ist an die Erweiterte Backus-Naur-Form
(EBNF) angelehnt.
?X → aY
• Eigentlich ist in Produktionen kein Regulärer Ausdruck
erlaubt, sondern nur eine Folge von (Non-)Terminalen.
• Rechts eine traditionelle Darstellung für ? X → a (b | c)∗ .
• Die verwendete EBNF erlaubt es, Grammatiken deutlich
kompakter und übersichtlicher aufzuschreiben, ist aber
nicht mächtiger.
I
Y → ZY
Y → Z → b
Z → c
Algorithmen zum Lexing und Parsing: Andrew W. Appel. Modern
Compiler Implementation in C. 1997, Cambridge University Press.
ISBN 0-521-58653-4.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
52
1 · Syntax & Semantik
Weiterführendes Wissen · 1.6
Der Kreis schließt sich
I
Dieses Kapitel spricht (Metasprache) über diverse Objektsprachen, z.B.
Reguläre Ausdrücke (cf. Seite 31), und Grammatiken (cf. Seite 34).
• Diese wurden wiederum als Metasprachen verwendet, um z.B. ArithEx und
BoolEx zu beschreiben.
• Leicht kann man eine Grammatik für Reguläre Ausdrücke angeben.
I
Manchmal haben wir Formeln verwendet statt sie zu beschreiben.
• So haben wir etwa Mengenarithmetik (∪, ∈ und {·}) verwendet.
• Die Struktur verstehen Sie anhand von Konventionen und Klammern.
• Die Bedeutung verstehen Sie aus der Bedeutung der Teilausdrücke.
I
In der Definition von Regulären Ausdrücken (cf. Seite 31) haben wir die
Metavariablen x, r und s verwendet. Der Absatz “Notation” dort ist
eine Anwendung des Abschnitts Klammern und Konvention, cf. Seite 39.
I
Für eine Struktur sind verschiedene Syntaxen denkbar: Wir haben z.B.
die besonders kompakte EBNF für Grammatiken eingeführt (cf. Seite 52).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
53
2
Funktionale Programmierung
Aunt Agatha: Well, James, what programming language are you studying in this term
at university?
James: Haskell.
Agatha: Is that a procedural language, like Pascal?
James: No.
Agatha: Is it object-oriented, like Java or C++?
James: No.
Agatha: What then?
James: Haskell is a fully higher-order purely functional language with
non-strict semantics and polymorphic static typing.
Agatha: Oh. hPausei More tea, James?
—Originally due to Richard Bird.
2 · Funktionale Programmierung
Programmieren (nur) mit Funktionen
I
(Fast) jede Programmiersprache kennt Funktionen (und manchmal
Prozeduren), die man mit geeigneten Parametern aufrufen kann, um
irgendetwas zu berechnen.
I
Funktionale Programmierung besteht nur aus der Definition und dem
Anwenden von Funktionen.
Beispiel Funktionen in der Mathematik, am Bsp. der Fakultätsfunktion
Definition: sei fact eine Funktion fact : N → N mit
fact 0 = 1 und fact n = n · fact (n − 1).
Anwendung: fact 4 = 24, fact 6 = 720.
I
Aus Programmiersprachensicht handelt es sich bei “fact : N → N” um
eine Typdeklaration – fact ist ein Objekt eines Funktionstyps (N → N).
I
Eine Besonderheit von funktionaler Programmierung ist, dass Funktionen
“first class objects” sind.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
55
2 · Funktionale Programmierung
“First Class Objects”—Higher-Order Functions
Was meinen wir mit “Funktionen sind first class objects?”
First class objects können z.B.
I
an Variablen gebunden werden,
I
als Argument an Funktionen übergeben werden,
I
als Ergebnis von Funktionen zurückgegeben werden,
I
in Datenstrukturen aufgenommen werden.
Intuition: So wie in der Arithmetik die Operatoren +, ×, etc. auf Zahlen
operieren, so operieren ◦ (Komposition), $ (Applikation), etc. auf
Funktionen. Der Trick: ◦ und $ sind selbst ebenfalls Funktionen.
Echter Mehrwert im Vgl. zur Arithmetik: viel stärkere Orthogonalität der
Sprache!
Funktionen, die Funktionen als Argumente/Resultate haben, nennt man
auch Funktionen höherer Ordnung (higher-order functions).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
56
2 · Funktionale Programmierung
Programmieren mit Termersetzung
In Haskell, und auch sonst meist,
schreibt man für “Objekt a ist
vom Typ b” i.d.R. a :: b,
ansonsten liest sich Haskell hier
wie Mathematik. Klammern um
Funktionsargumente lassen wir
weg, wenn das eindeutig ist.
1
2
3
fact :: Integer -> Integer
fact 0 = 1
fact n = n * fact (n-1)
Die Auswertung einer
Funktionsanwendung kann mit
einfacher Textersetzung
geschehen:
fact 3
_
3 ∗ fact (3 − 1)
_
3 ∗ fact 2
_
3 ∗ 2 ∗ fact (2 − 1)
_
3 ∗ 2 ∗ fact 1
_
3 ∗ 2 ∗ 1 ∗ fact (1 − 1)
_
3 ∗ 2 ∗ 1 ∗ fact 0
_
3∗2∗1∗1
_∗
6
Damit das wirklich so einfach funktioniert, müssen eine Reihe von
Voraussetzungen erfüllt sein. Die wichtigste ist die sog. Referenzielle
Transparenz (cf. Seite 182).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
57
2 · Funktionale Programmierung
Auswertung mit einfacher Textersetzung?
Bei genauerer Betrachtung ergibt sich schnell eine Reihe von Fragen, z.B.
I
In der Mathematik ist 2 · f n ≡ f n + f n —Hier auch?
I
Auch wenn f Seiteneffekte hätte?
I
Wie wird der nächste zu ersetzende Term bestimmt?
I
Spielt diese Auswahl überhaupt eine Rolle?
I
Gibt es unendliche Ersetzungsfolgen?
I
...
⇒ Zur Beantwortung solcher und ähnlicher Fragen dient u.a. der λ-Kalkül.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
58
2 · Funktionale Programmierung
The Taste of Functional Programming (FP)
I
A programming language is a medium for expressing ideas (not to get a
computer perform operations). Thus programs must be written for people
to read, and only incidentally for machines to execute.
I
Using FP, we restrict or limit not what we program, but only the
notation for our program descriptions.
I
Large programs grow from small ones – idioms. Develop an arsenal of
idioms of whose correctness we are convinced (or whose correctness we
have proven). Combining idioms is crucial.
I
It is better to have 100 functions operate on one data structure than 10
functions on 10 data structures.
— Alan J. Perlis
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
59
2 · Funktionale Programmierung
FP in the real world
I
Die Versionsverwaltung darcs ist in Haskell geschrieben.
http://darcs.net/
I
xmonad ist ein dynamisch teilender X11 Window Manager.
http://xmonad.org/
I
Pugs, die erste Implementation von Perl 6, wurde in Haskell
geschrieben. https://github.com/perl6/Pugs.hs
I
Andere moderne Programmiersprachen bieten oft funktionale Aspekte:
Erlang http://www.erlang.org/
OCaml http://caml.inria.fr/ocaml/
Scala http://www.scala-lang.org/
Python http://www.python.org/
Java Script Douglas Crockford. JavaScript: The Good Parts.
http://javascript.crockford.com/.
...
Mehr auf Philip Wadler’s Hompage...
http://homepages.inf.ed.ac.uk/wadler/realworld/
I
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
60
2 · Funktionale Programmierung
2.1
Funktionale vs. Imperative Programmierung · 2.1
Funktionale vs. Imperative Programmierung
Programme einer funktionalen Programmiersprache (functional
programming language, FPL) bestehen ausschließlich aus
Funktionsdefinitionen und Funktionsaufrufen.
Die Bausteine der Funktionsdefinitionen sind dabei
I
der Aufruf weiterer vom Programmierer definierter Funktionen und
I
der Aufruf elementarer Funktionen (und Operatoren), die schon in der
FPL definiert sind.
Die Anwendung (application) einer Funktion f auf ein Argument e ist das
zentrale Konzept in FPLs und wird daher standardmäßig einfach durch
Nebeneinanderschreiben (Juxtaposition) notiert:
f e
Funktionale Programme werden ausschließlich durch das Zusammensetzen
von Funktionen konstruiert.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
61
2 · Funktionale Programmierung
Funktionale vs. Imperative Programmierung · 2.1
Funktionale PL
Programmkonstruktion:
Applikation und Komposition
Operational:
Funktionsaufruf, Ersetzung von
Ausdrücken
Formale Semantik:
λ-Kalkül (cf. später)
Imperative PL
Programmkonstruktion:
Sequenzen von Anweisungen
Operational:
Zustandsänderungen (Seiteneffekte)
Formale Semantik:
schwierig (z.B. denotationell)
~
I
FPLs bieten konsequenterweise folgende Konzepte nicht:
Sequenzoperatoren für Anweisungen (‘;’ in Pascal oder C)
• Programme werden durch Funktionskomposition zusammengesetzt,
• eine explizite Reihung von Anweisungen existiert nicht.
I
Zustand
• FPLs sind “zustandslos” und bieten daher keine änderbaren Variablen.
I
Zuweisungen (‘:=’ in Pascal, ‘=’ in C/Java)
• Berechnungen in FPLs geschehen allein durch Auswertung von Funktionen,
nicht durch Manipulation des Maschinenzustandes bzw. -speichers.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
62
2 · Funktionale Programmierung
Funktionale vs. Imperative Programmierung · 2.1
Beispiel Eine Funktion, die testet, ob eine Zahl n eine Primzahl ist.
○
1 Ist die Menge der Teiler (factors) von n leer, so ist n prim.
○
2 Die Teiler von n sind alle Zahlen x von 2 bis n − 1, die n ohne Rest teilen.
Diese Beschreibung der Eigenschaften einer Primzahl könnte man in
gewohnter mathematischer Notation z.B. so aufschreiben:
I
isPrime n ⇔ factors n = ∅ ,
wobei factors n = {x | x ∈ [2, n − 1]N ∧ n mod x = 0}.
Oder direkt als funktionales Programm (hier: Haskell)
I
1
2
3
4
isPrime :: Integer -> Bool
isPrime n = factors n == []
where
factors n = [ x | x <- [2..n-1], n ‘mod‘ x == 0 ]
--
○
1
--
○
2
• Das Programm liest sich mehr wie die deklarative Spezifikation der
Eigenschaften einer Primzahl als eine explizite Vorschrift, den Primzahltest
auszuführen.
• Bspw. ist eine parallele Ausführung von factors nicht ausgeschlossen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
63
2 · Funktionale Programmierung
Funktionale vs. Imperative Programmierung · 2.1
Imperative Programmiersprachen sind dagegen eng mit dem
zugrundeliegenden von Neumann’schen Maschinenmodell verknüpft,
indem sie die Maschinenarchitektur sehr direkt abstrahieren:
I
der Programmzähler (PC) der CPU arbeitet Anweisung nach
Anweisung sequentiell ab.
• Der Programmierer hat seine Anweisungen also explizit aufzureihen und
Wiederholungen/Sprünge zu codieren.
I
der Speicher der Maschine dient zur Zustandsprotokollierung
• Der Zustand eines Algorithmus muss durch Variablenzuweisung bzw.
-auslesen explizit kontrolliert werden.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
64
2 · Funktionale Programmierung
Funktionale vs. Imperative Programmierung · 2.1
I
Zusätzlich zur Lösung seines Problemes hat der Programmierer einer
imperativen PL die Aufgabe, obige Punkte korrekt zu spezifizieren.
I
Imperative Programme
• sind oft länger als ihre FPL-Äquivalente,
I Aktualisierung und Kontrolle des Zustands sind explizit zu codieren.
• sind oft schwieriger zu verstehen,
I Eigentliche Problemlösung und Kontrolle der von Neumann-Maschine werden
vermischt.
• sind nur mittels komplexer Methoden auf Korrektheit zu überprüfen.
I Bedeutung jedes Programmteils immer von Zustand des gesamten Speichers und
Änderungen auf diesem abhängig.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
65
2 · Funktionale Programmierung
Funktionale vs. Imperative Programmierung · 2.1
Beispiel Primzahltest in PASCAL
1
function isPrime (n : integer) : boolean;
2
3
4
var m : integer;
found_factor : boolean;
5
6
7
8
begin
m := 2;
found_factor := false;
9
10
11
12
13
while (m <= n-1) and (not found_factor) do
if (n mod m) = 0
then found_factor := true
else m := m + 1;
14
15
16
isPrime := not found_factor
end; { isPrime }
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
66
2 · Funktionale Programmierung
Funktionale vs. Imperative Programmierung · 2.1
Beobachtungen: imperative Programmierung
I
Das Programm kontrolliert die Maschine durch explizite
Schleifenanweisungen (while, for), bedingte Anweisungen
(if · then · else) und Sequenzierung (‘;’) von Anweisungen. Die
Auswertungsfolge ist explizit festgelegt.
I
Das ist das Hauptmerkmal des imperativen Stils.
I
Die eigentliche Berechnung des Ergebnisses erfolgt “als Seiteneffekt” auf
den Zustandsvariablen (m, found_factor).
Die Variablen dienen gleichzeitig
I
• zur Kontrolle der Maschine (m, found_factor) und
• zur Protokollierung des (Zwischen-) Ergebnisses des eigentlichen Problems
(found_factor).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
67
2 · Funktionale Programmierung
Funktionale vs. Imperative Programmierung · 2.1
“Geringes” Abstraktionsniveau imperativer Programmierung
Andere Konzepte imperativer PLs bieten noch weitergehenden direkten
Zugriff auf die Maschine:
I Arrays und Indexzugriff (A[i])
• Modelliert direkt den linearen Speicher der Maschine sowie indizierende
Adressierungsmodi der CPU.
I
Pointer und Dereferenzierung
• Modellieren 1:1 die indirekten Adressierungsmodi der CPU.
I
explizite (De-)Allokation von Speicher (malloc, free, new)
• Der Speicher wird eigenverantwortlich als Resource verwaltet.
I
Sprunganweisungen (goto)
• Direkte Manipulation des PC.
I
...
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
68
2 · Funktionale Programmierung
2.2
Ausführung funktionaler Programme · 2.2
Ausführung funktionaler Programme
Funktionale Programme berechnen ihre Ergebnisse allein durch die
Ersetzung von Funktionsaufrufen durch Funktionsergebnisse (s. oben).
Dieser Ersetzungsvorgang ist so zentral, dass wir dafür das Zeichen “_”
(reduces to) reservieren.
Beispiel
length [0, 2*2, fact 100]
_
3
1. Die Reihenfolge der Ersetzungen wird durch die Programme nicht
spezifiziert, insbesondere können mehrere Ersetzungen parallel erfolgen,
2. und ein Funktionsaufruf kann jederzeit durch sein Ergebnis ersetzt
werden, ohne die Bedeutung des Programmes zu ändern (referenzielle
Transparenz).
Frage: Gilt 2. nicht auch für imperative PLs?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
69
2 · Funktionale Programmierung
Ausführung funktionaler Programme · 2.2
Konstruktion von Funktionen
Beispiel Die Mathematik definiert eine Funktion f als eine Menge von
geordneten Paaren (x, y ). Ob diese Menge explizit (durch eine Maschine)
konstruierbar ist, ist hierbei nicht relevant.
Die Funktion f mit
(
1 wenn x irrational ist,
f x=
0 sonst.
ist auf einem Rechner aufgrund der endlichen und daher ungenauen
Repräsentation von irrationalen Zahlen nicht implementierbar.
Im Gegensatz zur Mathematik benötigen FPLs einen konstruktiven
Funktionsbegriff.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
70
2 · Funktionale Programmierung
Ausführung funktionaler Programme · 2.2
λ-Notation
Der λ-Kalkül (s. nächstes Kapitel) stellt eine Notation zur Verfügung, mit
der Funktionsobjekte dargestellt werden können.
I
“Funktion f : N → N mit f n = n2 ” lesen wir als
• Deklaration einer Variablen f vom Typ N → N und
• Definition des Funktionsrumpfes f n = n2 .
Also ist genau genommen f der Name der Funktion(-svariablen), nicht
die Funktion selbst. Meist definieren wir in der Mathematik Funktionen
nur zusammen mit einem Namen.
I
Jetzt wollen wir “anonyme Funktionen”, oder “die Funktion selbst”
auch irgendwie aufschreiben können, so dass sie “first class objects”
werden.
I
Dazu liefert uns der λ-Kalkül das Handwerkszeug.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
71
2 · Funktionale Programmierung
Ausführung funktionaler Programme · 2.2
λ-Abstraktion
Funktionen werden durch die λ-Abstraktion definiert. Beispiel:
λx . (+ x x)
|{z} | {z }
formaler Parameter x
Funktionsrumpf
Applikation wird dann über Termersetzung (_) formalisiert. Beispiel:
(λ x.(+ x x)) 3
_
(+ 3 3)
_
6
I
Durch λ gebundene Vorkommen von x im Rumpf werden durch aktuellen
Parameter 3 ersetzt.
I
Ersetzungsregeln dieser Art bilden allein das operationale Modell aller
FPLs.
Obacht Die λ-Notation kennt keine (binären) Operatoren, nur Funktionen
(in Präfix-Notation), daher “+ x x” statt “x + x”.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
72
2 · Funktionale Programmierung
Ausführung funktionaler Programme · 2.2
Graphische Darstellung
I
I
Die FPL-Gemeinde hat bis heute ausgefeilte Techniken entwickelt, um die
Operation _ effizient zu unterstützen.
Moderne FPL-Compiler erzeugen fast ausschließlich eine interne
Graph-Repräsentation des Programmes, die mittels (paralleler)
Graph-Reduktion die Termersetzung via _ nachbildet (sog.
“G-Machines”).
Beispiel Sei f = λx. (∗ (+ x 1) (− x 1)).
Dann wird der Ausdruck f 4 wie folgt reduziert:
∗
∗
f
4
4
Stefan Klinger · DBIS
−
+
_
1
4
_
5
3
_ 15
1
Informatik 2 · Sommer 2016
73
2 · Funktionale Programmierung
Ausführung funktionaler Programme · 2.2
I
Da die Applikation für FPLs so zentral ist, wird eine
Graph-Repräsentation gewählt bei der als innere Knoten (fast) nur die
Applikation @ vorkommt.
I
Wir werden diese Form später (cf. Seite 89) vertiefen, und ausschließlich
verwenden.
@
Beispiel f x wird dann zu
f
Stefan Klinger · DBIS
x
Informatik 2 · Sommer 2016
74
2 · Funktionale Programmierung
Ausführung funktionaler Programme · 2.2
Beobachtungen
I
Ausführung eines funktionalen Programmes = Auswertung eines
Ausdrucks.
I
Die Auswertung geschieht durch simple Reduktionsschritte
(Graph-Reduktion).
I
Reduktionen können in beliebiger Reihenfolge, auch parallel, ausgeführt
werden. (Reduktionen sind unabhängig voneinander, Seiteneffekte
existieren nicht)
I
Die Auswertung ist komplett, wenn keine Reduktion mehr möglich ist
(Normalform erreicht).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
75
2 · Funktionale Programmierung
Ausführung funktionaler Programme · 2.2
Performance-Überlegungen
I
Exotische Architekturen, die die Reduktion _ auf Maschinen-Ebene
unterstützten (z.B. LISP-Maschinen von Symbolics) konnten sich nicht
durchsetzen.
I
Compiler für imperative PLs erzeugen derzeit meist effizienteren
Maschinen-Code:
• die maschinennahen Konzepte der imperativen PLs sind direkter auf die
klassischen von Neumann-Maschinen abbildbar.
• Für parallele Maschinen-Architekturen ist dies jedoch nicht unbedingt der Fall.
I
Performance ist nicht alles:
• Lesbarkeit: ggf. spezielle Syntax für eine Domäne: XQuery.
• Beweisbarkeit: Wie einfach ist es formal über Programme zu argumentieren?
• Prototyping: Schnell ein abstraktes Modell ausprogrammieren.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
76
2 · Funktionale Programmierung
Ausführung funktionaler Programme · 2.2
Beispiel Imperative Vektortransformation
(hier in PASCAL notiert) überspezifiziert die
Lösung durch explizite Iteration (bzgl. i).
1
2
for i := 1 to n do
A[i] := transform(A[i]);
I
Ein vektorisierender PASCAL-Compiler hat nun die schwierige (oft
unmögliche) Aufgabe, zu beweisen, dass die Funktion transform keine
Seiteneffekte besitzt, um die Transformation parallelisieren zu können.
Die inherente Parallelität wird durch die Iteration verdeckt und muss
nachträglich wiederentdeckt werden.
I
Das äquivalente funktionale Programm map transform A spezifiziert
keine explizite Iteration und lässt dem Compiler alle
Optimierungsmöglichkeiten.
I
“Parallele imperative Sprachen” mit
entsprechenden Spracherweiterungen
erlauben dem Programmierer die explizite
Angabe von Parallelisierungsmöglichkeiten.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
1
2
for i := 1 to n do in parallel
A[i] := transform(A[i]);
77
2 · Funktionale Programmierung
Ausführung funktionaler Programme · 2.2
Strikte vs. nicht-strikte Auswertung
@
Wird im Graphen
f
erst der rechte oder der linke Zweig reduziert?
x
rechts Strikte Auswertung. Die weitaus meisten PLs reduzieren das
Argument x bevor die Definition von f verwendet und weiter reduziert
wird.
links Nicht-strikte Auswertung. Die Expansion der Definition von f bevor
das Argument x ausgewertet wird kann unnötige Berechnungen
ersparen: Ein Argument wird erst dann ausgewertet, wenn eine
Funktion tatsächlich auf das Argument zugreifen muss.
Beispiel Nicht-strikte Funktionen:
kx y
pos f x
= x(
0
=
f x
(genaue Definition später)
falls x < 0
sonst
Hingegen ist der +-Operator strikt in beiden Argumenten.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
78
2 · Funktionale Programmierung
Ausführung funktionaler Programme · 2.2
Nicht-strikte Auswertung eröffnet neue Wege zur Strukturierung von
Programmen.
I
Programme können auf potentiell unendlich großen Datenstrukturen
(etwa Listen) operieren. Nicht-strikte Auswertung inspiziert die
unendliche Struktur nur soweit wie dies zur Berechnung des
Ergebnisses notwendig ist (also nur einen endlichen Teil).
Beispiel Das “Sieb des Eratosthenes” kann einfach als Filter auf einem
potentiell unendlichen Strom von natürlichen Zahlen ab 2 (Haskell: [2..])
definiert werden:
1
2
primes :: [Integer]
primes = sieve [2..]
3
4
sieve (x:xs) = x : sieve [ y | y <- xs, y ‘mod‘ x > 0 ]
Solange nur jeweils eine endliche Anzahl von Primzahlen inspiziert wird
(etwa durch den Aufruf take n primes) terminiert das Programm:
1
2
> take 10 primes
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
79
2 · Funktionale Programmierung
Ausführung funktionaler Programme · 2.2
I
Programme können durch Funktionskomposition klar strukturiert und
aus einfachen Funktionselementen zusammengesetzt werden.
I
Bei nicht-strikter Auswertung werden in der Komposition
g (f x)
g und f synchronisiert ausgeführt: f wird nur dann aufgerufen, wenn g
dies verlangt; f konsumiert sein Argument x nur soweit, wie dies zur
Beantwortung von g s Anfrage notwendig ist.
Beispiel Ein Programm zur iterativen Wurzelberechnung kann aus
einfachen Bauteilen zusammengesetzt werden:
√
Erinnerung: Iterative Berechnung von x:
r0 =
x
2
rn+1 = rn +
Stefan Klinger · DBIS
x − rn2
2 · rn
Informatik 2 · Sommer 2016
80
2 · Funktionale Programmierung
Ausführung funktionaler Programme · 2.2
I
Die Iterationsvorschrift wird durch die Funktion rn1 implementiert:
x − rn2
1 rn1 rn = rn + (x - rn*rn) / (2*rn)
rn+1 = rn +
2 · rn
I
Die eigentliche Iteration realisieren wir durch die Standardfunktion
iterate, die mit zwei Argumenten f und r die unendliche Liste
[ r , f r , f (f r ) , f (f (f r )) , f (f (f (f r ))) , ...]
generiert. Startwert r0 = x/2. Beispiel mit x = 17:
1
2
*Main> iterate rn1 (x/2)
[8.5,5.25,4.244047619047619,4.124828858612169,4.123105985575862^CInterrupted.
Es fehlt nur noch eine Funktion die entscheidet, ob die Iteration weit
genug fortgeschritten ist (die Differenz zweier aufeinanderfolgender
Listenelemente ist < ε):
I
1
2
1
2
within eps (x1:x2:xs) | abs (x1-x2) < eps = x2
| otherwise
= within eps (x2:xs)
*Main> within 0.0001 (iterate rn1 (x/2))
4.123105625617677
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
81
2 · Funktionale Programmierung
I
1
2
3
4
Ausführung funktionaler Programme · 2.2
Komposition fügt die Teile zu einer iterativen Wurzelberechnungsfunktion
isqrt zusammen:
isqrt eps x = within eps (iterate rn1 r0)
where
r0 = x/2
rn1 rn = rn + (x - rn*rn) / (2*rn)
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
1
2
3
4
*Main> isqrt 0.001 17
4.123105625617677
*Main> isqrt 0.0000001 17
4.123105625617661
82
3
Der λ-Kalkül
3 · Der λ-Kalkül
3.1
Überblick · 3.1
Überblick
Alonzo Churchs λ-Kalkül (ca. 1940) ist der formale Kern jeder
funktionalen Programmiersprache.
Der λ-Kalkül
I
ist eine einfache Sprache mit nur wenigen syntaktischen Konstrukten
und simpler Semantik.
⇒ Eine Implementation des λ-Kalküls ist leicht zu erhalten und damit eine gute
Basis für die Realisierung von FPLs.
I
ist eine mächtige Sprache. Jede berechenbare Funktion kann im
λ-Kalkül dargestellt werden.
⇒ Alle Konzepte, auch die moderner Sprachen, lassen sich auf den λ-Kalkül
abbilden.
⇒ Im Prinzip könnten wir einen Compiler für eine FPL erhalten, indem wir sie
auf den λ-Kalkül abbilden und dessen Implementation nutzen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
84
3 · Der λ-Kalkül
Überblick · 3.1
FPL ←→ λ-Kalkül
Funktionales Programm ≡ Ausdruck des λ-Kalküls
Ausführung des Programms ≡ Auswertung durch Reduktion
Auswertung Primitiver Algorithmus:
1. Wähle nächsten zu reduzierenden Teilausdruck e (Redex, reducible
expression).
2. Ersetze Redex e durch Reduktionsergebnis e 0 .
1.
3. Stop, wenn Normalform erreicht. Sonst zurück zu ○
2 ab jetzt: e _ e 0
Notation Schreibe für Schritt ○
0
(sprich: e wird zu e reduziert, e reduces to e 0 )
1 und ○
3 ) untersuchen
Auswertungsverfahren (insbesondere die Schritte ○
wir später genauer.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
85
3 · Der λ-Kalkül
Überblick · 3.1
Beispiel Auswertung von (+ (× 5 6) (× 8 3)) durch Reduktion:
(+ (× 5 6) (× 8 3))
Zwei Redexe: (× 5 6) und (× 8 3). Wähle ersten (beliebig).
_
Reduktion: (× 5 6) _ 30.
(+ 30 (× 8 3))
Normalform? Nein, ≥ 1 Redex verbleibt. Wähle einzigen
_
Redex (× 8 3). Reduktion: (× 8 3) _ 24.
(+ 30 24)
Normalform? Nein, ≥ 1 Redex verbleibt. Wähle einzigen
_
Redex (+ 30 24). Reduktion:
54
(+ 30 24) _ 54.
Normalform? Ja, kein Redex verbleibt. Stop.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
86
3 · Der λ-Kalkül
3.2
Currying · 3.2
Currying
Im λ-Kalkül genügt es Funktionen mit nur einem Argument zu
betrachten, ebenso in den meisten FPLs.
Beispiel Zweistellige Funktionen:
I
Wir schreiben in der Mathematik üblicherweise etwa
f :: N × N → N
mit
f (a, b) = a + b ,
und lesen dies als: “die Funktion f hat zwei Argumente”.
(oder auch: ein zusammengesetztes Argument, nämlich ein Tupel zweier Zahlen)
I
Im FPL-Kontext schreiben wir eher
f :: N → N → N
mit
f ab=+ab ,
was bedeutet: f ist eine Funktion mit einem Argument (vom Typ N), die
eine (anonyme) Funktion zurückgibt (vom Typ N → N).
Wendet man diese auf eine zweite Zahl an, so kommt wieder eine Zahl heraus.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
87
3 · Der λ-Kalkül
Currying · 3.2
Genauer müssten wir den Typ der Funktion f also so notieren:
f :: N → (N → N)
und damit die Funktionsanwendung als:
I
(f a) b.
Als Konvention vereinbart man
• Links-Assoziativität der Funktionsanwendung und
• Rechts-Assoziativität des Pfeils,
und lässt die Klammern in beiden Fällen weg (sofern nicht im Kontext
notwendig).
I
Diese Technik nennt man Currying16 .
I
Sie ermöglicht auch die partielle Anwendung (partial application). Im
Beispiel ist auch “f a” ein wohldefinierter Ausdruck, bei dem die
Funktion f nur partiell angewendet wurde.
16 Obwohl
das Verfahren von Moses Schönfinkel erfunden und von Gottlob Frege vorausgedacht
wurde, ist es nach Haskell Brooks Curry benannt, der das Verfahren letztlich umfangreich theoretisch ausgearbeitet hat.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
88
3 · Der λ-Kalkül
Currying · 3.2
Graph-Repräsentation
I
Mit Currying macht auch die bereits angedeutete Repräsentation mit der
Applikation (@) als innere Knoten Sinn (cf. Seite 74).
(später werden wir noch ‘λ‘ als inneren Knoten hinzufügen)
Beispiel λ-Ausdrücke und ihr AST17
I
Jf xK =
@
@
I
f
x
Jf a bK =
f
@
I
Jf x (g y z)K =
@
f
b
a
@
x
z
@
g
17 Abstract
@
y
Syntax Tree, cf. Seite 27
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
89
3 · Der λ-Kalkül
Currying · 3.2
Funktionen höherer Ordnung
Currying ist ein Beispiel für die Verwendung von Funktionen höherer
Ordnung: Funktionen können Funktionen als Argumente und/oder
Resultate haben.
Beispiel map — wende eine Funktion auf alle Elemente einer Menge18 an.
map :: (A → B) → P A → P B
map f S = { f x | x ∈ S }
Anwendung:
I
map malZwei {1, 2, 3, 4, 5} _ {2, 4, 6, 8, 10}
Die Eleganz funktionaler Programme wird u.a. durch “higher order
functions” erreicht.
18 Dabei
bezeichnet P A die Potenzmenge von A, also die Menge aller Teilmengen von A.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
90
3 · Der λ-Kalkül
3.3
Syntax des λ-Kalküls · 3.3
Syntax des λ-Kalküls
I
Funktionsanwendung: Juxtaposition von Funktion und Argument,
notiert in Präfix-Form:
f x
I
Curried functions: Mittels Currying wird jede Funktion als Funktion
eines einzigen Arguments dargestellt. Beispiel:
(+ x) y
I
Vereinbarung: Funktionsanwendung ist links-assoziativ. Schreibe daher
auch kürzer:
+x y
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
91
3 · Der λ-Kalkül
Syntax des λ-Kalküls · 3.3
Konstanten und vordefinierte Funktionen
I
Der Kern des λ-Kalküls bietet keine Konstanten (wie 42, "foo", True
oder primitive Funktionen wie +, ×, if).
I
In dieser Vorlesung verwenden wir meist einen um Primitive und
entsprechende Reduktionsregeln erweiterten λ-Kalkül.
Beispiel Reduktion einiger vordefinierter Funktionen
(später: δ-Reduktion _):
δ
+x
×x
if True e
if False e
and False
and True
y
y
f
f
e
e
_
_
_
_
_
_
x y
x y
e
f
False
e
(Operationen in seien direkt auf der Zielmaschine ausführbar, wenn die
Argumente x und y zuvor bis zur Normalform reduziert wurden)
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
92
3 · Der λ-Kalkül
Syntax des λ-Kalküls · 3.3
λ-Abstraktion
Mit der sog. λ-Abstraktion werden im λ-Kalkül neue Funktionen definiert:
λx. e
In dieser λ-Abstraktion ist x der formale Parameter, der im
Funktionskörper e zur Definition der Funktion benutzt werden kann
(sprich: x ist in e gebunden). Der Punkt ‘.’ trennt x und e.
Beispiel Funktion, die ihr Argument inkrementiert:
λx. (+ x 1)
Maximumsfunktion:
λx. (λy . (if (< x y ) y x))
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
93
3 · Der λ-Kalkül
Syntax des λ-Kalküls · 3.3
Die Grammatik des λ-Kalküls
Definition Grammatik des um Konstanten erweiterten λ-Kalküls
Expr →
|
|
|
Const
Var
(Expr Expr)
(λ Var . Expr)
Konstanten, z.B. ‘c‘, 42, +
Variablen, z.B. x, f, bar
Applikation (Juxtaposition)
λ-Abstraktion
Notation (cf. Seite 39)
I Treten keine Mehrdeutigkeiten auf können Klammern (·) weggelassen
werden.
I Applikation ist links-assoziativ.
I Der Wirkungsbereich der λ-Abstraktion erstreckt sich bis zum Ende des
längsten gültigen Terms.
inneres λ
z
}|
{
f ( λx. λy . + 2 (× x y ) ) 42
|
{z
}
äußeres λ
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
94
3 · Der λ-Kalkül
Syntax des λ-Kalküls · 3.3
Beispiele
6≡
I
f (g x)
I
λx. (f x)
I
((λx. ((+ x) 1)) 4)
Stefan Klinger · DBIS
f g x
≡
≡
λx. f x
≡
(f g ) x
6≡
(λx. f ) x
(λx. + x 1) 4
Informatik 2 · Sommer 2016
95
3 · Der λ-Kalkül
3.4
Operationale Semantik des λ-Kalküls · 3.4
Operationale Semantik des λ-Kalküls
Um den λ-Ausdruck
(λx. + x y ) 4
auszuwerten
I
wird der nicht bekannte “globale” Wert der Variablen y benötigt (wir
brauchen einen Kontext der y liefert), andererseits
I
ist der Wert des Parameters x im Funktionskörper (+ x y ) durch das
Argument 4 festgelegt.
Man sagt: Innerhalb der λ-Abstraktion
I
ist der Parameter x durch das λ gebunden, während
I
die Variable y frei ist.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
96
3 · Der λ-Kalkül
Operationale Semantik des λ-Kalküls · 3.4
Definition Freie und gebundene Vorkommen von Variablen
Seien x, y , v Variablen; c Konstanten; e, e 0 beliebige λ-Ausdrücke19 .
Freies Vorkommen einer Variablen x:
x ist frei in v
x ist frei in (e e 0 )
x ist frei in (λy .e)
⇐⇒
⇐⇒
⇐⇒
x =v
x ist frei in e oder x ist frei in e 0
x 6= y und x ist frei in e
In einem Ausdruck der aus einer einzigen Konstanten c besteht, ist x
niemals frei.
Gebundenes Vorkommen einer Variablen x:
x ist gebunden in (e e 0 )
x ist gebunden in (λy .e)
⇐⇒
⇐⇒
x ist gebunden in e oder in e 0
x = y oder x ist gebunden in e
In einem Ausdruck der aus einer einzigen Variablen v bzw. Konstanten c
besteht, ist x nie gebunden.
19 Obacht:
Das sind sind Metavariablen, z.B. steht x für irgend eine Variable, cf. Seite 48.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
97
3 · Der λ-Kalkül
Operationale Semantik des λ-Kalküls · 3.4
Generell hängt der Wert eines Ausdrucks nur von seinen freien Variablen ab.
Beispiel In der λ-Abstraktion
λx. + ((λy . × y z) 7) x
sind die Variablen x und y gebunden, z jedoch frei.
~
Vorsicht! Bindung/Freiheit muss für jedes einzelne Auftreten eines
Namens entschieden werden. Ein Variablenname kann innerhalb eines
Ausdrucks gebunden und frei auftreten.
Beispiel Hier kommt x sowohl gebunden, als auch frei vor:
+ x ((λx. +
frei
x
gebunden
1) 4)
Gleich werden wir sehen dass es sich um verschiedene Variablen handelt,
die zufällig den gleichen Namen haben (cf. Seite 102).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
98
3 · Der λ-Kalkül
Operationale Semantik des λ-Kalküls · 3.4
β-Reduktion
Die β-Reduktion definiert, wie eine λ-Abstraktion auf ein Argument
angewandt wird.
Definition β-Reduktion _
Vorläufig, cf. Seite 109
β
Seien x Variable; e, m λ-Ausdrücke. Die Funktionsanwendung
(λx. e) m
wird reduziert zu
I
einer Kopie des Funktionsrumpfes e,
I
in der die (dann) freien Vorkommen von x durch m ersetzt wurden.
Beispiel
(λx. + x 1) 4
Stefan Klinger · DBIS
_
β
+41
Informatik 2 · Sommer 2016
_
δ
5
99
3 · Der λ-Kalkül
Operationale Semantik des λ-Kalküls · 3.4
Beispiele
I
Der formale Parameter kann mehrfach im Funktionsrumpf auftreten:
(λx. + x x) 5
I
_
β
_
δ
10
Der formale Parameter muss nicht im Funktionsrumpf auftreten:
(λx. 3) 5
I
+55
_
β
3
In einem Funktionsrumpf kann eine weitere λ-Abstraktion enthalten sein
(Currying cf. Seite 87):
(λx. (λy . × y x)) 4 5
_
β (λy . × y 4) 5
_
β × 5 4
_
δ 20
Notation Schreiben abkürzend λx y . e statt λx. λy . e .
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
100
3 · Der λ-Kalkül
Beispiel Funktionen können
problemlos als Argumente
übergeben werden:
Operationale Semantik des λ-Kalküls · 3.4
(λf . f 3) (λx. + x 1)
_
(λx. + x 1) 3
_
+31
_
4
β
β
δ
Wichtig Bei β-Reduktion werden genau die in der Kopie des
Funktionsrumpfes freien Vorkommen des formalen Parameters ersetzt:
(λx. λx. + (− x 1) x 3) 9 Das unterstrichene Vorkommen von
_
x ist durch die innere λ-Abstraktion
β (λx.+ (− x 1)) 9 3
gebunden und wird daher bei der
_
β + (− 9 1) 3
ersten β-Reduktion nicht ersetzt.
_∗
δ
11
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
101
3 · Der λ-Kalkül
Operationale Semantik des λ-Kalküls · 3.4
α-Konversion
Erinnerung Generell hängt der Wert eines Ausdrucks nur von seinen
freien Variablen ab.
—cf. Seite 98
Im Umkehrschluss heißt das:
Die Bedeutung eines λ-Ausdrucks ändert sich nicht, wenn wir gebundene
Variablen konsistent umbenennen, d.h. wenn wir alle durch das gleiche λ
gebundenen Vorkommen durch den gleichen neuen Namen ersetzen:
(λx. × x 2)
]
α
(λy . × y 2)
I
Man sagt auch: Diese Ausdrücke sind gleich bis auf Umbenennen, oder
gleich modulo α-Konversion.
I
Manchmal ist diese α-Konversion unerläßlich, um Namenskollisionen
und damit fehlerhafte Reduktionen (sog. “name capture”) zu vermeiden.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
102
3 · Der λ-Kalkül
Operationale Semantik des λ-Kalküls · 3.4
Name Capture
Beispiel Betrachten wir den λ-Ausdruck
λy . (λx y . + x y ) y 3 5
und zwei verschiedene Reihenfolgen bei der Auswertung:
λy . (λx y . + x y ) y 3 5
λy . (λx y . + x y ) y 3 5
zuerst äußeren Redex
_
β
_
β
(λx y . + x y ) 5 3
_
β
_
β
_
δ
zuerst inneren Redex
λy . (λy . + y y ) 3 5
(λy . + 5 y ) 3
_
(λy . + y y ) 3
+53
_
+33
8
_
6
Frage
β
β
δ
— Falsch!
Was ist schief gegangen?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
103
3 · Der λ-Kalkül
Operationale Semantik des λ-Kalküls · 3.4
λy . (λx y . + x y ) y 3 5
_
β
λy . (λy . + y y ) 3 5
Lösung
I
Das zunächst durch das äußere λ
gebundene y ist nun falsch durch
das innere λ gebunden (captured).
Umbenennung durch α-Konversion hilft hier:
λy . (λx y . + x y ) y 3 5
Ersetze λy durch λz
λy . (λx z. + x z) y 3 5
_
β
λy . (λz. + y z) 3 5
I
Ersetzen die durch das innere λ
gebundenen y konsistent durch
neue Variable z.
I
Das y wird jetzt von λz nicht
mehr eingefangen.
]
α
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
104
3 · Der λ-Kalkül
Operationale Semantik des λ-Kalküls · 3.4
Noch ein Beispiel Unter dem Namen twice sei folgende Funktion
definiert :
twice = λf x. f (f x)
Wir verfolgen jetzt die Reduktion des Ausdrucks twice twice mittels
β-Reduktion.
=
_
β
_
β
dann:
twice twice
(λf x. f (f x)) twice
=
λx. twice (twice x)
_
β
λx. twice (twice
| {z x})
○
2
|
{z
○
1
}
1 und ○
2.
Es entstehen die Redexe ○
2 beliebig,
Wir wählen ○
Stefan Klinger · DBIS
○
2
z }| {
λx. twice (twice x)
λx. twice ((λf x. f (f x)) x)
λx. twice (λx. x (x x))
Falsch! Die x sind nun durch die
innere λ-Abstraktion gebunden
(captured). Umbenennung mittels
α-Konversion hilft hier...
Informatik 2 · Sommer 2016
105
3 · Der λ-Kalkül
Operationale Semantik des λ-Kalküls · 3.4
○
2
Es ist nötig die Variable der inneren
Bindung umzubenennen, bevor die
Ersetzung stattfindet.
=
λx. twice ((λf x. f (f x)) x)
]
λx. twice ((λf y . f (f y )) x)
_
λx. twice (λy . x (x y ))
α
β
~
Obacht
z }| {
λx. twice (twice x)
An dieser Stelle erkennt man auch, dass es sich bei den x in
λx. twice ((λf x. f (f x)) x)
um verschiedene Variablen handelt, die den gleichen Namen tragen.
I
Sie unterscheiden sich durch das λ welches sie bindet.
I
Jede Variable ist entweder frei oder durch genau ein λ gebunden.
I
α-Konversion einer Variablen kann dies sichtbar machen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
106
3 · Der λ-Kalkül
Operationale Semantik des λ-Kalküls · 3.4
Die Einsetzung ist so zentral, dass wir ihr eine eigene Notation spendieren:
Definition e[x
m]
sprich: “m für x in e”, oder “e mit x ersetzt durch m”
Seien x, v Variablen; c Konstante; m, e, e1 , e2 beliebige λ-Ausdrücke.
c[x
v [x
(e1 e2 )[x
(λv . e)[x
Stefan Klinger · DBIS
m] = c
(
m
m] =
v
wenn v = x
sonst
m] = e1 [x m] e2 [x m]


λv . e
wenn v = x




λv . e[x m]
wenn v 6= x, und
m] =
v nicht frei in m ist



(λz. e[v z])[x m] sonst, z neuer Variablenname


(das ist α-Konversion)
Informatik 2 · Sommer 2016
107
3 · Der λ-Kalkül
Operationale Semantik des λ-Kalküls · 3.4
~
Vorsicht Einsetzen ist eine Operation der Meta-Ebene.
I
I
Die Notation e[x m] beschreibt eine syntaktische Veränderung, die
wir am λ-Ausdruck e vornehmen.
Diese Operation ist nicht Bestandteil eines λ-Ausdrucks!
• Es ist eine Operation auf einem λ-Ausdruck.
• Die Grammatik des λ-Kalküls (cf. Seite 94) kennt den Ersetzungsoperator
·[· ·] überhaupt nicht.
Notation Der Ersetzungsoperator ·[· ·] bezieht sich immer auf den
kürzesten voranstehenden gültigen λ-Ausdruck:
a (a b)[a
Stefan Klinger · DBIS
x] = a (x b)
6= x (x b)
Informatik 2 · Sommer 2016
108
3 · Der λ-Kalkül
Operationale Semantik des λ-Kalküls · 3.4
Zusammenfassung
Damit sind alle Regeln zum Umgang mit dem λ-Kalkül vorhanden:
Definition Operationale Semantik des λ-Kalküls
Seien x, y Variablen; m, e beliebige λ-Ausdrücke; ∗ primitive Operation.
α-Konversion
λx. e
] λy . e[x
α
β-Reduktion
(λx. e) m
_ e[x
δ-Reduktion
∗e
_ e
β
y]
wenn y nicht frei in e
m]
δ
wenn e in Normalform20
Dieses kompakte formale System ist ausreichend, um als Zielsprache für alle
funktionalen Programmiersprachen zu dienen.
I
Tatsächlich ist Haskell ein syntaktisch stark angereicherter λ-Kalkül.
I
Manche Sprachelemente von Haskell werden wir auf den λ-Kalkül
zurückführen.
20 d.h.
e enthält keinen Redex; implementiert ∗ auf der Zielmaschine.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
109
3 · Der λ-Kalkül
3.5
Anmerkungen · 3.5
Anmerkungen
Namen als Abkürzungen für Terme
Betrachten wir nochmal die Definition von Seite 105:
twice = λf x. f (f x)
I
Diese Zeile ist nicht im λ-Kalkül geschrieben, denn der kennt keine
Zuweisung, kein =-Zeichen.
I
twice ist eine Metavariable, die uns als Abkürzung für den Term
λf x. f (f x) dient.
I
Obacht: Wenn man twice verwendet, dann tut man so als wären
Klammern drum herum:
twice x
≡ (λf x. f (f x)) x
6
≡
λf x. f (f x) x
twice x meint also die Anwendung des ganzen Ausdrucks twice auf den
Ausdruck x, nicht die syntaktische Konkatenation der beiden.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
110
3 · Der λ-Kalkül
Anmerkungen · 3.5
Äquivalenz
I
Wann sind λ-Ausdrücke gleich?
Man hätte auch schreiben (und denken) können:
twice = λharry foo. harry (harry foo)
I
Diese Erkenntnis wird rigoros angewandt: λ-Ausdrücke die bis auf
α-Konversion gleich sind, werden semantisch nicht unterschieden!
• De Bruijn indices sind eine Syntax für λ-Ausdrücke, welche keine α-Konversion benötigt.
• Der SK -Kalkül verwendet gar keine Variablen, ist aber gleich mächtig wie der λ-Kalkül.
I
Etwas weiter gefasst ist die Äquivalenz:
Definition Äquivalenz von λ-Ausdrücken
Zwei λ-Ausdrücke e1 , e2 heißen äquivalent, gdw. sie zur gleichen
Normalform reduziert werden können, d.h.:
e1 ≡ e2
⇔
∃m. e1 _∗ m ∧ e2 _∗ m
wobei _∗ hier durchaus für unterschiedlich viele Schritte stehen kann.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
111
3 · Der λ-Kalkül
3.6
I
Exkurs: Variablenbindung anderswo · 3.6
Exkurs: Variablenbindung anderswo
Das Konzept der Variablenbindung begegnet uns auch in der Mathematik
und in anderen Programmiersprachen.
∀x. 2 · x > t
n
X
2·i −1
i=1
1
2
3
for (int i = 0; i < k; i++) {
print(i);
}
Frage Was sind hier die freien Variablen? Welche sind gebunden? Wo
findet die Bindung statt?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
112
3 · Der λ-Kalkül
Exkurs: Variablenbindung anderswo · 3.6
α-Konversion?
I
Auch hier ist der gewählte Name eigentlich nicht relevant (cf. Seite 102).
∀x. 2 · x > t
n
X
2·i −1
≡
∀y . 2 · y > t
n
X
≡
i=1
1
2
3
for (int i = 0; i < k; i++) {
print(i);
}
Stefan Klinger · DBIS
2·j −1
j=1
1
≡
2
3
for (int j = 0; j < k; j++) {
print(j);
}
Informatik 2 · Sommer 2016
113
3 · Der λ-Kalkül
Exkurs: Variablenbindung anderswo · 3.6
Scoping
Bei vielen Programmiersprachen können wir die Verwendung
verschiedener Variablen mit dem gleichen Namen beobachten:
I
1
2
3
4
5
int i = 42;
for (int i = 0; i < 10; i++) {
print(i);
// gibt 0–9 aus
}
print(i);
// gibt 42 aus
Die innere Variable i überdeckt die
äußere. Innerhalb der Schleife kann
auf die 42 nicht zugegriffen werden.
I
Der Bereich in dem eine Variable syntaktisch verwendet werden kann,
heißt Sichtbarkeitsbereich, oder Scope.
I
Ob, und wo eine Variable sichtbar ist, hängt von der jeweiligen
Programmiersprache ab.
• Obiger Code wäre in C erlaubt, hingegen
• verbietet Java diese Überdeckung (aka. Shadowing).
⇒ Scoping-Regeln der Sprache lesen!
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
114
3 · Der λ-Kalkül
I
Exkurs: Variablenbindung anderswo · 3.6
Was in der Programmierung als zumindest fragwürdiger Stil gesehen
werden kann, ist in der Mathematik unüblich, wenn nicht verpönt:
!
10
i
X
X
∀x. ∃y . Px,y ∧ ∀x. Qx,y
oder
5·
3·i
i=1
i=1
(Manche sagen: Das macht keinen Sinn! — Kann aber beim Einsetzen passieren)
I
Üblich ist aber die Wiederverwendung der Zählvariablen in
nebeneinander stehenden Termen:
n
n
X
X
(2 · i − 1) +
5·i
i=1
i=0
Tatsächlich sind das verschiedene Variablen die beide i heißen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
115
3 · Der λ-Kalkül
3.7
Ausdruckskraft des λ-Kalküls · 3.7
Ausdruckskraft des λ-Kalküls
Frage: Gibt es Dinge die wir nur in anderen Sprachen berechnen können?
Was kann man wirklich mit dem λ-Kalkül ausdrücken?
Antwort: Alles was man überhaupt mit irgendeiner Programmiersprache
ausdrücken kann, kann man auch im λ-Kalkül ausdrücken.
I
Solche formalen Systeme (und damit den λ-Kalkül) nennt man
Turing-vollständig.
cf. Seite 190, Exkurs zum Thema Berechenbarkeit.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
116
3 · Der λ-Kalkül
Ausdruckskraft des λ-Kalküls · 3.7
Datenstrukturen vs. Operationen
I
I
Wir haben schon betont, dass der (reine) λ-Kalkül keine primitiven
Datentypen (z.B. Integer, Boolean) kennt und auch keine
“eingebauten” Operationen darauf.
Dennoch kann man diese mit dem λ-Kalkül nachbilden. Ebenso wie man
konstruierte Datentypen mit Funktionen “nachbauen” kann.
Beispiel Darstellung von Paaren im λ-Kalkül: Wir beschreiben zunächst
die Schnittstelle des abstrakten Typs Pair.
pair :: α → β → Pair α β
fst :: Pair α β → α
snd :: Pair α β → β
(Konstruktor)
(Accessor)
(Accessor)
und seine Eigenschaften
fst (pair a b) ≡ a
Stefan Klinger · DBIS
snd (pair a b) ≡ b
Informatik 2 · Sommer 2016
117
3 · Der λ-Kalkül
Ausdruckskraft des λ-Kalküls · 3.7
(nochmal die Eigenschaften)
fst (pair a b) ≡ a
Beweis für fst (pair a b) ≡ a:
snd (pair a b) ≡ b
... bis hierhin sieht das aus wie eine
algebraische Spezifikation eines
abstrakten Datentyps.
=
_
β
=
Jetzt realisieren wir das im λ-Kalkül:
(λs. s a b) (λx y . x)
_
(λx y . x) a b
_
(λy . a) b
_
a
β
...bleibt nur noch zu beweisen, dass
die geforderten Eigenschaften erfüllt
sind.
Stefan Klinger · DBIS
(λx y s. s x y ) a b (λx y . x)
(λy s. s a y ) b (λx y . x)
β
snd = λp. p (λx y . y )
pair a b (λx y . x)
_
β
fst = λp. p (λx y . x)
(λp. p (λx y . x)) (pair a b)
_
β
pair = λx y s. s x y
fst (pair a b)
β
Informatik 2 · Sommer 2016
118
3 · Der λ-Kalkül
Ausdruckskraft des λ-Kalküls · 3.7
I
Ähnlich kann man das auch mit primitiven (z.B. Boolean und Integer)
oder anderen konstruierten Datentypen (z.B. Listen) machen.
I
Diese Technik der Repräsentation von Daten und Operatoren im λ-Kalkül
nennt man Church Codierung (siehe dazu auch später: “Algebraische
Datentypen” sowie einige Übungsaufgaben).
Dualität von Daten und Operationen
... ein Beispiel dafür, dass die Unterscheidung zwischen Daten und
Operationen keine scharfe, zwingende ist.
Man kann auch umgekehrt ein Programm (z.B. einen λ-Ausdruck) als
Datenobjekt betrachten und mit einem (anderen oder gar dem gleichen)
Programm bearbeiten ...
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
119
4
Haskell – Typen, Werte und einfache
Definitionen
4 · Haskell – Typen, Werte und einfache Definitionen
4.1
Typen · 4.1
Typen
Intuitiv unterteilt man die Objekte, die man mit einer Programmiersprache
manipulieren will, in disjunkte Mengen, etwa: Zeichen, ganze Zahlen,
Listen, Bäume und Funktionen:
I
Objekte verschiedener Mengen haben unterschiedliche Eigenschaften,
(Zeichen und auch ganze Zahlen sind bspw. anzuordnen, Funktionen nicht)
I
für die Objekte verschiedener Mengen sind unterschiedliche
Operationen sinnvoll.
(eine Funktion kann angewandt werden, eine ganze Zahl kann mit 0 verglichen werden,
aber auf einen Wahrheitswert kann man nicht addieren, etc.)
Viele Programmiersprachen (wie auch Haskell) formalisieren diese
Intuition mittels eines Typsystems.
Typen im λ-Kalkül?
I
Weder der einfache, noch der erweiterte λ-Kalkül haben ein Typsystem.
I
Später werden wir Typsystem für den λ-Kalkül betrachten.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
121
4 · Haskell – Typen, Werte und einfache Definitionen
Typen · 4.1
Ein Typ definiert
1. eine Menge von gleichartigen Objekten (Wertevorrat, “Domain”) und
2. Operationen, die auf diese Objekte anwendbar sind (Interface).
Einige Basis-Typen:
Objektmenge
Ganze Zahlen
Zeichen
Wahrheitswerte
Fließkommazahlen
Typkonstruktoren
Operationen (Auswahl)
+, max, <, >, ==
max, <, >, ==
&&, ==, not
*, /, round
konstruieren aus beliebigen Typen α, β neue Typen:
Objektmenge
Funktionen von α nach β
Listen von α-Objekten
Paare von α, β-Objekten
Stefan Klinger · DBIS
Typname
Integer
Char
Bool
Double
Typkonstruktor
α→β
[α]
(α,β)
Informatik 2 · Sommer 2016
Operationen (Auswahl)
$, map
head, reverse, length
fst, snd
122
4 · Haskell – Typen, Werte und einfache Definitionen
I
I
Die Notation x :: α (x hat den Typ α) wird vom Haskell-Compiler
eingesetzt, um anzuzeigen, dass das Objekt x den Typ α besitzt.
Umgekehrt können wir so dem Compiler anzeigen, dass x eine Instanz
des Typs α sein soll.
Beispiel
I
I
Typen · 4.1
2
’X’
0.05
round
[2,3]
head
(’a’,(2,True))
snd
::
::
::
::
::
::
::
::
Integer
Char
Double
Double -> Integer
[Integer]
[α] -> α
(Char,(Integer,Bool))
(α,β) -> β
Manche Typen (z.B. von snd) enthalten Typvariablen α, β, ....
Das entspricht der Beobachtung, dass snd das zweite Element eines
Paares bestimmen kann, ohne Details der gepaarten Objekte zu
kennen oder Operationen auf diese anzuwenden.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
123
4 · Haskell – Typen, Werte und einfache Definitionen
4.2
Interpretation komplexer Typen · 4.2
Interpretation komplexer Typen
Beispiel Typ der Prelude-Funktion unzip :: [(α, β)] → ([α], [β])
unzip
unzip
unzip
unzip
unzip
::
...
:: [...]
:: [(α,β)]
:: [(α, β)]
:: [(α, β)]
→
→
→
→
→
...
...
...
(...,...)
([α], [β])
unzip ist eine Funktion...
...die eine Liste...
...von Paaren als Argument hat, ...
...und ein Paar...
...von Listen als Ergebnis liefert, ...
...und dabei ist der Elementtyp α der ersten Liste der gleiche wie der Typ
der ersten Komponente der Argumentlistenpaare und derjenige der zweiten
Liste (also β) der gleiche wie der der zweiten Komponente der
Argumentlistenpaare.
unzip [(x1 , y1 ), ..., (xn , yn )]
Stefan Klinger · DBIS
_
([x1 , ..., xn ], [y1 , ..., yn ])
Informatik 2 · Sommer 2016
124
4 · Haskell – Typen, Werte und einfache Definitionen
4.3
Currying und der Typkonstruktor “→” · 4.3
Currying und der Typkonstruktor “→”
Erinnerung Mittels Currying kann eine Funktion mehrerer Argumente
sukzessive auf ihre Argumente angewandt werden (cf. Seite 87).
I
Auch haskell verwendet Currying, und damit
I
spielt Currying auch bei der Typisierung von Funktionen eine Rolle.
Beispiel Typ der Funktion (des Operators) +, bei Anwendung auf zwei
Argumente vom Typ Integer, also x :: Integer, y :: Integer:
x +y
≡
((+ x) y )
1. Der Teilausdruck (+ x) besitzt den Typ Integer → Integer,
2. damit hat + also den Typ Integer → (Integer → Integer).
Vereinbarung: → ist rechts-assoziativ. Schreibe daher kürzer
(+) :: Integer → Integer → Integer
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
125
4 · Haskell – Typen, Werte und einfache Definitionen
Currying und der Typkonstruktor “→” · 4.3
Haskell besitzt einen Mechanismus zur Typinferenz, der für (fast) jedes
Objekt x den zugehörigen Typ α automatisch bestimmt. Haskell ist
streng typisiert, d.h. eine Operation kann niemals auf Objekte angewandt
werden, für die sie nicht definiert wurde.
statisch typisiert, d.h. schon zur Übersetzungszeit und nicht erst während
des Programmlaufs wird sichergestellt, dass Programme keine Typfehler
enthalten.
⇒ Der Interpreter oder Compiler weist inkorrekt typisierte Ausdrücke sofort
zurück.
Beispiel Typische Typfehlermeldung:
1
2
3
4
Prelude> fst [2,3]
<interactive>:2:5:
Couldn’t match expected type ‘(α, β)’ with actual type ‘[Integer]’
— ...
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
126
4 · Haskell – Typen, Werte und einfache Definitionen
4.4
Deklaration & Definition · 4.4
Deklaration & Definition
Haskell-Programm (Skript) = Deklarationen + Definitionen
Beispiel Fakultätsfunktion fact
1
2
3
4
fact :: Integer -> Integer
fact n = if n == 0
then 1
else n * fact (n-1)
I
Deklaration fact :: Integer -> Integer
fact ist eine Funktion, die einen Wert des Typs Integer (ganze Zahl)
auf einen Wert des Typs Integer abbildet.
I
Definition fact n = ...
(Rekursive) Regeln für die Berechnung der Fakultätsfunktion.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
127
4 · Haskell – Typen, Werte und einfache Definitionen
4.5
I
Basis-Typen · 4.5
Basis-Typen
Haskell stellt diverse Basis-Typen zur Verfügung. Die Notation für
Konstanten dieser Typen ähnelt anderen Programmiersprachen.
Ganze Zahlen: Integer
I
I
Der Typ Integer enthält die ganzen Zahlen, der Wertebereich ist
unbeschränkt. Haskell kennt auch den Typ Int, fixed precision
integers, mit Wertebereich [−229 , 229 − 1]
Eine nichtleere Sequenz von Ziffern 0...9 stellt ein Integer-Literal dar.
(kann aber auch als anderer numerischer Typ aufgefasst werden, cf. PK2, oder später)
I
I
Allgemein werden negative Zahlen durch die Anwendung der Funktion
negate oder des Prefix-Operators - gebildet.
Achtung: Operator - wird auch zur Subtraktion benutzt, wie etwa in
f -123
I
6≡
f (-123)
Beispiele: 0, 42,
1405006117752879898543142606244511569936384000000000
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
128
4 · Haskell – Typen, Werte und einfache Definitionen
Basis-Typen · 4.5
Konstanten des Typs Char (Zeichen)
I
Zeichenkonstanten werden durch Apostrophe ’ · ’ (ASCII 39) eingefasst.
I
Nichtdruckbare und Sonderzeichen werden mit Hilfe des bspw. auch in C
verwendeten \ (escape, backslash) eingegeben. Nach \ kann ein
ASCII-Mnemonic (etwa NUL, BEL, FF, ...) oder ein dezimaler (oder
hexadezimaler nach \x bzw. oktaler nach \o) Wert stehen, der den
ASCII-Code des Zeichens festlegt.
I
Zusätzlich werden die folgenden Abkürzungen erkannt:
\a
\n
\v
\’
I
(alarm)
(newline)
(vertical feed)
(apostroph)
\b
\r
\\
\&
(backspace)
(carriage return)
(backslash)
(NULL)
\f (formfeed)
\t (Tab)
\" (dbl quote)
Beispiele: ’P’, ’s’, ’\n’, ’\BEL’, ’\x7F’, ’\’’, ’\\’
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
129
4 · Haskell – Typen, Werte und einfache Definitionen
Basis-Typen · 4.5
Konstanten des Typs Float (Fließkommazahlen)
I
Fließkommakonstanten enthalten stets einen Dezimalpunkt. Vor und
hinter diesem steht mindestens eine Ziffer 0...9.
I
Die Konstante kann optional von e bzw. E und einem ganzzahligen
Exponenten (zur Basis 10) gefolgt werden.
I
Beispiele: 3.14159, 10.0e-4, 0.001, 123.45E6
Konstanten des Typs Bool (Wahrheitswerte)
I
Bool ist ein Summentyp (Aufzählungstyp, enumerated type) und besitzt
lediglich die beiden Konstanten21 True und False.
21 Später:
Konstruktoren.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
130
4 · Haskell – Typen, Werte und einfache Definitionen
4.6
Funktionen · 4.6
Funktionen
Funktionen in funktionalen Programmiersprachen sind tatsächlich im
mathematischen Sinne zu verstehen. Ein Wert f mit
f :: α -> β
bildet bei Anwendung Objekte des Typs α auf Objekte des Typs β ab und
es gilt22
x =y ⇒ fx =fy
Diese einfache aber fundamentale mathematische Eigenschaft von
Funktionen zu bewahren, ist die Charakteristik funktionaler
Programmiersprachen.
22 Referenzielle
Stefan Klinger · DBIS
Transparenz, cf. später
Informatik 2 · Sommer 2016
131
4 · Haskell – Typen, Werte und einfache Definitionen
I
I
I
Funktionen · 4.6
Variablennamen, und damit auch die Namen von Funktionen23
beginnen mit Kleinbuchstaben a...z gefolgt von a...z, A...Z, 0...9, _ und ’.
Als RegEx:
[a − z][a − z A − Z 0 − 9 _’]∗
Beispiele: foo, c_3_p_o, f’
Haskell ist case-sensitive, i.e., foobar 6≡ fooBar.
Die Funktionsapplikation ist der einzige Weg in Haskell komplexere
Ausdrücke zu bilden. Applikation wird syntaktisch durch Juxtaposition
(Nebeneinanderschreiben) ausgedrückt:
Beispiel: Anwendung von Funktion f auf die Argumente x und y:
f x y
I
Die Juxtaposition hat höhere Priorität als Infix-Operatoren:
f x + y
I
≡
(f x) + y
Klammern ( · ) können zur Gruppierung eingesetzt werden.
23 bis
auf Operatoren, cf. Seite 133
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
132
4 · Haskell – Typen, Werte und einfache Definitionen
Funktionen · 4.6
Operatoren
I
Operatoren sind nichts anderes als Funktionen mit besonderen
syntaktischen Eigenschaften (andere Zeichen, meist infix notiert).
I
Haskell erlaubt die Einführung neuer Infix-Operatoren. Operatoren
erfüllen den RegEx
+
!#$%&*+/<=>?@^|~:.
I
Übliche Infix-Operatoren (+, *, ==, <, ...) sind bereits vordefiniert.
I
Operatoren, die mit : beginnen, spielen eine Sonderrolle (cf. Seite 272,
Algebraische Datentypen).
I
Die Token .., :, ::, =, \, |, <-, ->, @, ~, =>, -- sind reserviert, ebenso
der einzige unäre Präfix-Operator - (Minus).
Beispiel Definition von ~~ als “fast gleich”:
1
2
epsilon :: Float
epsilon = 1.0e-4
1
2
3
4
5
3
(~~) :: Float -> Float -> Bool
x ~~ y = abs (x-y) < epsilon
Stefan Klinger · DBIS
4
5
> pi ~~ 3.141
False
> pi ~~ 3.1415
True
>
Informatik 2 · Sommer 2016
133
4 · Haskell – Typen, Werte und einfache Definitionen
Funktionen · 4.6
Operatoren sind Funktionen
Infix-Operator → Prefix-Applikation. Jeder Infix-Operator kann in der
Notation () auch als Prefix-Operator geschrieben werden (cf. Seite 139):
1 + 3 ≡ (+) 1 3
True && False ≡ (&&) True False
Funktion → Infix-Applikation. Umgekehrt kann man jede binäre
Funktion f (Funktion zweier Argumente) mittels der Schreibweise ‘f‘
(ASCII 96) als Infix-Operator verwenden:
max 2 5
I
≡
2 ‘max‘ 5
Die so notierten Infix-Operatoren werden durch die Sprache als
links-assoziativ und mit höchster Operatorpriorität (Level 9) interpretiert:
5 ‘max‘ 3 + 4
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
_
9
134
4 · Haskell – Typen, Werte und einfache Definitionen
Funktionen · 4.6
Bemerkung: Information über die Assoziativität und Priorität eines
Operators durch den Interpreter:
1
2
3
4
5
6
> :i +
class (Eq a, Show a) => Num a where
(+) :: a -> a -> a
...
-- Defined in GHC.Num
infixl 6 +
Die letzte Zeile verrät uns:
I
+ ist linksassoziativ,
I
und hat priorität 6.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
135
4 · Haskell – Typen, Werte und einfache Definitionen
Funktionen · 4.6
Currying
I
Prinzipiell hat jede in Haskell definierte Funktion nur einen Parameter.
I
Funktionen mehrerer Parameter werden durch Currying realisiert
(cf. oben).
I
Der Typ einer Funktion mehrerer Parameter, etwa max : N × N → N wird
dargestellt als
max :: Integer -> Integer -> Integer
I
Damit max eine Funktion eines Integer-Parameters, die bei Anwendung
einen Wert (hier: wieder eine Funktion) des Typs Integer -> Integer
liefert. Dieser kann dann auf ein weiteres Integer-Argument angewandt
werden, um letzlich das Ergebnis des Typs Integer zu bestimmen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
136
4 · Haskell – Typen, Werte und einfache Definitionen
Funktionen · 4.6
Currying im λ-Kalkül:
I
Haskell-Funktionsdefinitionen sind tatsächlich lediglich syntaktischer
Zucker für die schon bekannten λ-Abstraktionen:
f x = e
g x y = e
I
≡ f = λx. e
≡ g = λx y. e
Damit lässt sich Currying durch mehrfache β-Reduktion erklären.
Beispiel Maximumsfunktion:
max 2 5
≡
1
2
max :: Integer -> Integer -> Integer
max x y = if x<y then y else x
(λx y . if (x < y ) y x) 2 5
_
(λy . if (2 < y ) y 2) 5
_
if (2 < 5) 5 2
δ
5
β
max = λx y . if (x < y ) y x
β
_∗
Stefan Klinger · DBIS
Definition von max
Informatik 2 · Sommer 2016
137
4 · Haskell – Typen, Werte und einfache Definitionen
Funktionen · 4.6
Partielle Anwendung
Currying erlaubt die partielle Anwendung von Funktionen. Der Wert des
Ausdrucks (+) 1 hat den Typ Integer -> Integer und ist die
Funktion, die 1 zu ihrem Argument addiert.
Beispiel Nachfolgerfunktion inc:
1
2
inc :: Integer -> Integer
inc = (+) 1
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
138
4 · Haskell – Typen, Werte und einfache Definitionen
Funktionen · 4.6
Sections
I
Currying ist auch auf binäre Operatoren anwendbar, da Operatoren ja
lediglich binäre Funktionen in Infix-Schreibweise (mit festgelegter
Assoziativität) sind. Man erhält dann die sogenannten Sections.
I
Für jeden Infix-Operator gilt (die Klammern ( · ) gehören zur Syntax!):
(x ) ≡ λy. x y
( y) ≡ λx. x y
() ≡ λx y. x y
Beispiel Sections erlauben viele elegante Notationen:
1
2
3
4
inc = (1+)
halve = (/2)
add = (+)
positive = (‘max‘ 0)
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
139
4 · Haskell – Typen, Werte und einfache Definitionen
Funktionen · 4.6
λ-Abstraktionen (anonyme Funktionen)
I
Haskell erlaubt λ-Abstraktionen als Ausdrücke und somit anonyme
Funktionen, i.e., Funktionen ohne Namen.
I
Die Notation ähnelt dem λ-Kalkül:
λx. e
λx. λy . e
λx y . e
Damit wird der Ausdruck “(\x -> 2*x) 3” zu 6 ausgewertet und die
vorige Definition von max kann alternativ wie folgt geschrieben werden:
I
1
2
I
≡ \x -> e
≡ \x -> \y -> e
≡ \x y -> e
max :: Integer -> Integer -> Integer
max = \x y -> if x<y then y else x
Auch hier erstreckt sich der Wirkungsbereich des λ bis zum Ende des
längsten gültigen Terms (cf. Seite 94), d.h.,
\x -> (f x)
Stefan Klinger · DBIS
≡
\x -> f x
Informatik 2 · Sommer 2016
6≡
(\x -> f) x
140
4 · Haskell – Typen, Werte und einfache Definitionen
4.7
Listen · 4.7
Listen
Listen sind die primäre Datenstruktur in funktionalen Programmiersprachen.
I
Haskell unterstützt die Konstruktion und Verarbeitung homogener
Listen beliebigen Typs: Listen von Integer, Listen von Listen, Listen
von Funktionen, ...
I
Der Typ von Listen, die Elemente des Typs α enthalten, wird mit [α]
bezeichnet (gesprochen list of α). Listen sind also immer homogen, d.h.
alle Elemente sind vom gleichen Typ.
I
Die Struktur von Listen ist rekursiv:
• Eine Liste ist entweder leer, notiert als [], genannt nil,
• oder ein konstruierter Wert aus Listenkopf x (head) und Restliste xs (tail),
notiert als x:xs. Der Operator (:) heißt cons24
24 Für
list construction.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
141
4 · Haskell – Typen, Werte und einfache Definitionen
Listen · 4.7
Listen-Konstruktion
I
I
[] und (:) sind Beispiele für ”data constructors”, Funktionen, die
Werte eines bestimmten Typs konstruieren. Wir werden dafür noch
zahlreiche Beispiele kennenlernen.
Jede Liste kann mittels [] und (:) konstruiert werden:
• Liste, die 1 bis 3 enthält: 1:(2:(3:[]))
• Der cons-Operator ist rechts-assoziativ, also äquivalent: 1:2:3:[]
I
Syntaktische Abkürzung: [e1 ,e2 ,...,en ]
Beispiele
[]
’z’:[]
[[1],[2,3],[]]
(False:[]):[]
[(<),(<=),(>),(>=)]
[[]]
::
::
::
::
::
::
≡
e1 :e2 :...:en :[]
[α]
[Char]
[[Integer]]
[[Bool]]
[α -> α -> Bool]
[[α]]
Natürlich hat auch cons einen Typ: (:) :: α → [α] → [α].
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
142
4 · Haskell – Typen, Werte und einfache Definitionen
Listen · 4.7
Arithmetische Sequenzen
[x..y ]
[x1 ,x2 ..y ]
≡ wenn x<=y dann [x,x+1,x+2,...,y ] sonst []
≡ Liste der Werte x1 bis y mit Schrittweite x2 -x1
~
Für Sequenzen vom Typ [Float] und [Double] ist die
1
Abbruchbedingung y + x2 −x
2 .
Beispiel
I
Der Ausdruck [2 .. 6] wird zu [2, 3, 4, 5, 6] ausgewertet,
I
[9, 7 .. 2] ergibt [9, 7, 5, 3].
I
Aber: [0.1, 0.3 .. 0.6] _ [0.1, 0.3, 0.5, 0.7], weil
0.6 + 0.3−0.1
≥ 0.7.
2
I
Die Abbruchbedingung kann weggelassen werden, um “unendliche”
Listen zu erzeugen: [1, 5 ..] _ [1,5,9,13,17,21,...
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
143
4 · Haskell – Typen, Werte und einfache Definitionen
Listen · 4.7
Listen-Dekomposition
I
Mittels der vordef. Funktionen head und tail kann eine nicht-leere Liste
x:xs wieder in ihren Kopf und Restliste zerlegt werden:
head
tail
head
tail
I
(x:xs)
(x:xs)
[]
[]
_
_
_
_
x
xs
*** Exception: Prelude.head: empty list
*** Exception: Prelude.tail: empty list
Die Funktion null :: [α] -> Bool bestimmt ob eine Liste leer ist.
null []
_ True
null [1,2,3] _ False
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
144
4 · Haskell – Typen, Werte und einfache Definitionen
Listen · 4.7
Konstanten des Typs String (Zeichenketten)
I
Zeichenketten werden in Haskell durch den Typ [Char] repräsentiert,
eine Zeichenkette ist also eine Liste von Zeichen.
I
Funktionen auf Listen können damit auch auf Strings operieren.
I
Haskell kennt String als Synonym für den Typ [Char] (realisiert
durch die Deklaration type String = [Char] (→ später)).
I
Strings werden in doppelten Anführungszeichen " · " notiert.
Beispiel
Stefan Klinger · DBIS
""
"AbC"
’z’:[] ≡
[’C’,’u’,’r’,’r’,’y’] ≡
head "Curry" _
tail "Curry" _
tail (tail "OK\n") _
Informatik 2 · Sommer 2016
"z"
"Curry"
’C’
"urry"
"\n"
145
4 · Haskell – Typen, Werte und einfache Definitionen
4.8
I
I
Tupel · 4.8
Tupel
Tupel erlauben die Gruppierung von Werten unterschiedlicher Typen
(im Gegensatz zu Listen, welche immer homogen sind).
Ein Tupel (c1 ,c2 ,...,cn ) besteht aus einer fixen Anzahl von
Komponenten ci :: αi .
Der Typ dieses Tupels wird notiert als (α1 ,α2 ,...,αn ).
Beispiele
(1, ’a’)
("foo", True, 2)
([(*1), (+1)], [1..10])
((1,’a’),True)
I
::
::
::
::
(Integer, Char)
([Char], Bool, Integer)
([Integer -> Integer], [Integer])
((Integer, Char), Bool)
Die Position einer Komponente in einem Tupel ist signifikant. Es gilt
(c1 ,c2 ,...,cn ) = (d1 ,d2 ,...,dm )
genau dann, wenn n = m und ∀i; 1 ≤ i ≤ n. ci = di .
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
146
4 · Haskell – Typen, Werte und einfache Definitionen
Tupel · 4.8
Zugriff auf Tupel-Komponenten
I
Der Zugriff auf die einzelnen Komponenten eines Tupels geschieht durch
Pattern Matching, cf. Seite 150.
Beispiel Zugriffsfunktionen für die Komponenten eines 2-Tupels und
Tupel als Funktionsergebnis:
1
2
fst :: (α, β) -> α
fst (x,y) = x
3
4
5
snd :: (α, β) -> β
snd (x,y) = y
6
7
8
mult :: Integer -> (Integer, Integer -> Integer)
mult x = (x, (*x))
9
10
11
> snd (mult 3) 5
15
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
147
5
Funktionsdefinitionen
5 · Funktionsdefinitionen
Typischerweise analysieren Funktionen ihre Argumente, um
Fallunterscheidungen für die Berechnung des Funktionsergebnisses zu
treffen. Je nach Beschaffenheit des Argumentes wird ein bestimmter
Berechnungszweig gewählt.
Beispiel Summiere die Elemente einer Liste l:
1
sum :: [Integer] -> Integer
2
3
4
5
sum xs = if xs == []
then 0
else head xs + sum (tail xs)
---
○
1
○
2
sum hat den Berechnungszweig mittels if · then · else im Fall der leeren
1 bzw. nichtleeren Liste ○
2 auszuwählen und im letzeren Fall explizit
Liste ○
auf Kopf und Restliste von l zuzugreifen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
149
5 · Funktionsdefinitionen
5.1
I
I
Pattern Matching und case · 5.1
Pattern Matching und case
Pattern Matching erlaubt, für jeden Berechnungszweig einer Funktion f
Muster pi1 , ..., pik für die erwartete Struktur der Argumente anzugeben.
Wenn die Argumente von f den Mustern pi1 , ..., pik entsprechen, wird der
entsprechende Berechnungszweig ei ausgewählt:
f :: α1 -> ...
f p11 ... p1k =
f p21 ... p2k =
..
.
f pn1 ... pnk =
-> αk -> β
e1
e2
en
Die ei müssen dabei einen gemeinsamen (allgemeinsten) Typ β besitzen.
Semantik Beim Aufruf f x1 ... xk ...
werden die xj gegen die Muster pij (i = 1...n, von oben nach unten)
“gematcht” und der erste Berechnungszweig gewählt, bei dem ein
vollständiger Match vorliegt.
I Wenn kein Match erzielt wird, bricht das Programm ab.
I
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
150
5 · Funktionsdefinitionen
Pattern Matching und case · 5.1
Definition Erlaubte Formen für ein Pattern pij
I
Variable v — Der Match gelingt immer; v wird an das aktuelle
Argument xj gebunden und ist in ei verfügbar; Pattern müssen linear
sein, d.h. eine Variable v darf nur einmal in einem Pattern auftauchen.
I
Konstante c — Der Match gelingt nur mit einem Argument xj welches
zu c reduziert, d.h. xj _ c.
I
Wildcard ‘_’ — Der Match gelingt immer, es wird aber keine
Variablenbindung hergestellt. (don’t care)
I
Tupel-Pattern (p1 ,p2 ,...,pm ) — Der Match gelingt mit einem
m-Tupel, dessen Komponenten mit den Pattern p1 , ..., pm matchen.
I
List-Pattern [] und (p:ps) — Während [] nur auf die leere Liste
matcht, gelingt der Match mit (p:ps) für jede nichtleere Liste, deren
Kopf das Pattern p und deren Rest das Pattern ps matcht.
~
Diese Definition ist aufgrund der beiden letzten Fälle rekursiv.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
151
5 · Funktionsdefinitionen
Pattern Matching und case · 5.1
Beispiel Funktion sum mittels Pattern Matching:
1
sum :: [Integer] -> Integer
2
3
4
sum []
= 0
sum (x:xs) = x + sum xs
Sowohl die Fallunterscheidung als auch der Zugriff auf head und tail des
Arguments geschehen nun elegant durch Pattern Matching.
Beispiel Funktion zur Bestimmung der ersten n Elemente einer Liste:
1
take :: Int -> [α] -> [α]
2
3
4
5
take 0 _
= []
take _ []
= []
take n (x:xs) = x : take (n-1) xs
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
152
5 · Funktionsdefinitionen
Pattern Matching und case · 5.1
Beispiel Berechne xn (kombiniert Wildcard und Tupel-Pattern):
1
power :: (Float, Integer) -> Float
2
3
4
power (_, 0) = 1.0
power (x, n) = x * power (x,n-1)
Beachte: power ist trotz der Übergabe von x und n eine Funktion eines
(tupelwertigen) Parameters.
(Nur zur Demonstration von Tupel-Pattern)
~
Vorsicht Eine potentielle Fehlerquelle sind Definitionen wie diese:
1
foo :: (Integer,Integer) -> Integer
2
3
4
foo (x, y) = x + y
foo (0, _) = 0
---
○
1
○
2
2 wird auch bei einem Aufruf foo (0,5) nicht ausgewertet
Der Zweig ○
1 überdeckt das Pattern in Zweig ○
2 ).
(das Pattern in Zweig ○
Allgemein gilt: Spezialfälle vor den allgemeineren Fällen anordnen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
153
5 · Funktionsdefinitionen
Pattern Matching und case · 5.1
Layered Patterns
I
I
Auf die Komponenten eines Wertes e kann mittels Pattern Matching
zugegriffen werden. Oft ist in Funktionsdefinitionen aber gleichzeitig auch
der Wert von e selbst interessant.
Sei v eine Variable, p ein Pattern. Das Layered Pattern (as-Pattern)
v @p
matcht gegen e, wenn p gegen e matcht. Zusätzlich wird v an den Wert
e gebunden.
Beispiel Variante der Funktion within (cf. Seite 80). Schneide Liste von
Näherungswerten ab, sobald sich die Werte weniger als eps unterscheiden.
1
2
3
4
5
6
within’
within’
within’
within’
:: Float -> [Float] -> [Float]
_
[]
= []
_
[y]
= [y]
eps (y:rest@(x:_)) = if abs (x-y) < eps
then [y,x]
else y : within’ eps rest
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
154
5 · Funktionsdefinitionen
Pattern Matching und case · 5.1
Nützlichkeit von Layered Patterns
Aufgabe: Mische 2 bezüglich lt geordnete Listen (z.B. in der merge-Phase
von Mergesort):
merge (<) [1,3 .. 10] [2,4 .. 10]
_
[1,2,3, ..., 10]
Beispiel Formulierung ohne as-Patterns
1
merge :: (α -> α -> Bool) -> [α] -> [α] -> [α]
2
3
4
5
6
7
merge lt []
ys
= ys
merge lt xs
[]
= xs
merge lt (x:xs) (y:ys) = if x ‘lt‘ y
then x : merge lt xs (y:ys)
else y : merge lt (x:xs) ys
Die Listenargumente werden erst mittels Pattern Matching analysiert, um
danach evtl. wieder via (:) identisch zusammengesetzt zu werden.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
155
5 · Funktionsdefinitionen
Pattern Matching und case · 5.1
Beispiel Jetzt Formulierung mit as-Patterns:
1
merge :: (α -> α -> Bool) -> [α] -> [α] -> [α]
2
3
4
5
6
7
merge lt []
ys
= ys
merge lt xs
[]
= xs
merge lt l1@(x:xs) l2@(y:ys) = if x ‘lt‘ y
then x : merge lt xs l2
else y : merge lt l1 ys
I
Hier ist die Listenrekonstruktion nicht notwendig.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
156
5 · Funktionsdefinitionen
Pattern Matching und case · 5.1
case-Ausdrücke
I
Pattern Matching ist so zentral, dass es nicht nur in Zweigen einer
Funktionsdefinition, sondern auch als eigenständige Form zur
Verfügung steht.
case e of p1
p2
-> e1
-> e2
..
.
pn
-> en
I
Der Wert des Ausdrucks e wird nacheinander gegen die Pattern pi
(i = 1...n) gematcht.
I
Falls der Match e auf pk gelingt, so ist der Wert des Gesamtausdrucks ek .
Die Variablenbindungen aus pk stehen in ek zur Verfügung.
I
Trifft kein Muster zu, so bricht das Programm ab. (Der Wert des
Ausdrucks ist dann ⊥ (bottom), s. später).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
157
5 · Funktionsdefinitionen
Pattern Matching und case · 5.1
Beispiel “Zipper” für zwei Listen.
1
zip’ :: [a] -> [b] -> [(a, b)]
2
3
4
5
zip’ [] _
= []
zip’ _ []
= []
zip’ (x:xs) (y:ys) = (x, y) : zip xs ys
Die Fallunterscheidung könnte man genauso gut25 auch auf der rechten
Seite der Funktionsdefinition treffen:
1
zip :: [α] -> [β] -> [(α,β)]
2
3
4
5
6
zip xs ys = case (xs, ys) of
([], _)
-> []
(_, [])
-> []
(x:xs, y:ys) -> (x, y) : zip xs ys
Übrigens In Haskell gehören case-Ausdrücke zum innersten
Sprachkern. Viele andere Sprachkonstrukte (insb. alle, die auf Pattern
Matching bauen) werden intern auf case zurückgeführt.
25 was
ist lesbarer? praktischer?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
158
5 · Funktionsdefinitionen
Pattern Matching und case · 5.1
Sprachkern?
I
Viele Programmiersprachen bieten verschiedene syntaktische Formen
um das Gleiche auszudrücken.
I
Beim Bau eines Compilers könnte man für jede dieser Formen eigene
Übersetzungsregeln definieren.
! Mehr Arbeitsaufwand (Übersetzungsregeln sind oft aufwändig).
! Drücken die unterschiedlichen Formen wirklich das Gleiche aus? Beweisen!
I
Üblicherweise wird eine essentielle Kernsprache (aka. core language)
definiert, die nur die nötigsten Konstrukte der Sprache enthält.
Alle anderen Konstrukte (aka. syntactic sugar) können auf die Kernsprache
zurückgeführt werden.
Ein Compiler für die Kernsprache ist leichter zu konstruieren.
Oft können Spracherweiterungen bequem als syntaktischer Zucker realisiert
werden ⇒ Kein Eingriff in den Sprachkern nötig.
Der Übergang zur Kernsprache ist typischerweise eine recht frühe
Übersetzungsphase, cf. Seite 25.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
159
5 · Funktionsdefinitionen
Pattern Matching und case · 5.1
case gehört zum Haskell Sprachkern — Beispiele
I
Bedingte Ausdrücke werden mittels case implementiert
if e1 then e2 else e3
≡
case e1 of True -> e2
False -> e3
• Damit wird die Forderung nach einem gemeinsamen allgemeinsten Typ α von
e2 und e3 deutlich.
I
Funktionsdefinitionen mit Pattern Matching werden intern in
case-Ausdrücke über Tupeln übersetzt:
f p1 ... pk = e
≡
f v1 ... vk = case (v1 ,...,vk ) of
(p1 ,...,pk ) -> e
• Dabei sind v1 , ..., vk neue Variablen.
• So hat der Compiler lediglich die etwas einfachere Aufgabe,
Funktionsdefinitionen ohne Pattern Matching zu übersetzen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
160
5 · Funktionsdefinitionen
5.2
I
I
Guards · 5.2
Guards
Oft ist die Analyse der Struktur der Argumente einer Funktion nicht
ausreichend, um den korrekten Berechnungszweig auszuwählen.
Guards bieten die Möglichkeit, zusätzlich beliebige Tests auszuführen,
wenn Zweige gewählt werden:
f :: α1 -> ... -> αk
f p11 ... p1k | g11 =
| g12 =
| ... =
..
.
f pn1 ... pnk | gn1 =
| gn2 =
| ...
=
I
I
-> β
e11
e12
...
en1
en2
...
Die Guards gij sind Ausdrücke des Typs Bool.
In den Guards gij (j ≥ 1) sind die durch die Pattern pi1 ...pik
gebundenen Variablen nutzbar.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
161
5 · Funktionsdefinitionen
Guards · 5.2
Semantik von Guards
..
.
f pi1 ...pik | gi1
| gi2
| ...
..
.
I
= ei1
= ei2
= ...
Falls die Pattern pi1 , ..., pik matchen, werden die Guards gij der Reihe
nach von oben nach unten (j = 1, j = 2, . . .) getestet.
• Der erste Guard gij der dabei zu True ausgewertet wird bestimmt eij als
Ergebnis von f.
• Wird keiner der Guards gik erfüllt, wird nach dem nächsten matchenden
Pattern gesucht.
• Der spezielle Guard otherwise evaluiert immer zu True, nützlich als
Default-Alternative.
I
Guards können oft explizite Abfragen mittels if · then · else ersetzen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
162
5 · Funktionsdefinitionen
Guards · 5.2
Beispiel Selektiere die Elemente einer Liste, die die Bedingung p erfüllen.
1
2
3
4
filter :: (α -> Bool) -> [α] -> [α]
filter p [] = []
filter p (x:xs) | p x = x : filter p xs
| otherwise = filter p xs
Beispiel Noch eine Variante von within:
1
2
3
4
5
6
within :: Float -> [Float] -> [Float]
within _ [] = []
within _ [y] = [y]
within eps (y:rest@(x:_))
| abs (x-y) < eps = [y,x]
| otherwise
= y : within eps rest
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
163
5 · Funktionsdefinitionen
Guards · 5.2
Beispiel Lösche adjazente Duplikate aus einer Liste.
1
remdups :: [Integer] -> [Integer]
2
3
4
5
remdups (x:xs@(y:_)) | x == y
= remdups xs
| otherwise = x : remdups xs
remdups xs = xs
Frage: Könnte der letzte Zweig auch remdups [] = [] geschrieben
werden?
Beispiel Ist ein Element e in einer absteigend geordneten Liste vorhanden?
1
elem’ :: Integer -> [Integer] -> Bool
2
3
4
5
6
elem’ _ []
elem’ e (x:xs) | e > x
| e == x
| e < x
Stefan Klinger · DBIS
=
=
=
=
False
False
True
elem’ e xs
Informatik 2 · Sommer 2016
164
5 · Funktionsdefinitionen
I
Guards · 5.2
Guards können auch in case-Ausdrücken verwendet werden. Die Syntax
wird analog zu der von Funktionsdefinitionen erweitert:
case e of p1 | g11
| g12
pn | gn1
| gn2
->
->
..
.
->
->
..
.
e11
e12
en1
en2
Beispiel Entferne die ersten n Elemente einer Liste.
1
drop :: Integer -> [α] -> [α]
2
3
4
5
6
drop n xs = case xs of
[]
-> []
(x:xs) | n > 0 -> drop (n-1) xs
| n == 0 -> x:xs
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
165
5 · Funktionsdefinitionen
5.3
Lokale Definitionen · 5.3
Lokale Definitionen
Es kann oft nützlich sein, lediglich lokal sichtbare Namen in Ausdrücken
zu verwenden. Dies dient
I
der Beschränkung der Sichtbarkeit von Namen (Verbergen von
Implementationdetails),
I
dem “Herausfaktorisieren” öfter auftretender identischer
Teilausdrücke aus einem Ausdruck (kann die Effizienz der Auswertung
steigern).
Vgl. lokale Definitionen/Deklarationen in blockstrukturierten (imperativen)
Sprachen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
166
5 · Funktionsdefinitionen
Lokale Definitionen · 5.3
let-Ausdrücke
Wenn e, e1 , . . . , en Haskell-Ausdrücke
sind, und p1 , . . . , pn Patterns, so ist auch
der let-Ausdruck rechts ein gültiger
Haskell-Ausdruck.
I
I
let p1 = e1
p2 = e2
..
.
pn = en
in e
Die Reihenfolge der Definitionen pi = ei ist unerheblich, sie dürfen
wechselseitig rekursiv sein.
In e erscheinen die durch den Pattern-Match von ei gegen pi definierten
Namen an ihre Werte gebunden und sind außerhalb des Scopes von let
unbekannt.
Semantik Falls die ei nicht rekursiv sind (!) hat der obige let-Ausdruck
den Wert
(λ p1 p2 ... pn . e) e1 e2 ... en
I
I
Die Auswertung der ei geschieht also lazy: (ei wird nur dann tatsächlich
ausgewertet, wenn dies zur Auswertung von e erforderlich ist).
Wir werden im Kapitel Typinferenz eine weitere Besonderheit sehen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
167
5 · Funktionsdefinitionen
Lokale Definitionen · 5.3
Beispiel
I
1
Wir vervollständigen die Implementation von Mergesort (cf. Seite 155):
mergesort :: (α -> α -> Bool) -> [α] -> [α]
2
3
4
5
6
mergesort lt [] = []
mergesort lt [x] = [x]
mergesort lt xs = let (l1,l2) = split xs
in merge lt (mergesort lt l1) (mergesort lt l2)
I
Es verbleibt die Definition der für Mergesort typischen Divide-Phase
mittels split, die eine Liste xs in zwei Listen ungefähr gleicher Länge
teilt (das Listenpaar wird in einem 2-Tupel zurückgegeben).
I
Frage: Wie ist eine Liste xs unbekannter Länge in zwei ca. gleich lange
Teillisten l1 , l2 zu teilen?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
168
5 · Funktionsdefinitionen
1
split, Version ○
1
split
Lokale Definitionen · 5.3
Teile in der Mitte (mit Funktion length).
:: [α] -> ([α], [α])
2
3
split xs = nsplit (length xs ‘div‘ 2) xs
4
5
6
nsplit :: Int -> [α] -> ([α], [α])
7
8
nsplit n xs = (take n xs, drop n xs)
Besser: Verstecke Implementationsdetail nsplit in einem let-Ausdruck
1
split :: [α] -> ([α], [α])
2
3
4
split xs = let nsplit n xs = (take n xs, drop n xs)
in nsplit (length xs ‘div‘ 2) xs
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
169
5 · Funktionsdefinitionen
Lokale Definitionen · 5.3
1 : xs wird zweimal
Offensichtlicher Nachteil der split Version ○
durchlaufen.
2 Durchlaufe xs nur einmal, füge dabei abwechselnd ein
split, Version ○
Element in l1 oder l2 ein.
1
split2 :: [α] -> ([α], [α])
2
3
4
5
6
split2 []
= ([], [])
split2 [x]
= ([x], [])
split2 (x:x’:xs) = let (l1, l2) = split2 xs
in (x:l1, x’:l2)
Das Pattern (l1, l2) wird verwendet um das Ergebnis des rekursiven
Aufrufs von split2 aufzutrennen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
170
5 · Funktionsdefinitionen
Lokale Definitionen · 5.3
where-Klauseln
I
Lokale Definitionen lassen sich
alternativ mit einer where-Klausel
einführen.
I
Sie erweitert die Syntax von
Funktionsdefinitionen und
case-Ausdrücke ein weiteres Mal.
I
Die dij sind jeweils in den Guards gik
und Ausdrücken eik (k = 1...) des
gesamten Definitionszweiges i
sichtbar. Dies lässt sich mit
let-Ausdrücken nicht formulieren.
I
Für die lokalen Definitionen dij gelten
die zuvor bei let erklärten
Vereinbarungen.
Stefan Klinger · DBIS
f :: α1 -> ... -> αk ->
f p11 ... p1k | g11
=
| g12
=
|...
=
where d11
d12
..
.
β
e11
e12
...
f pn1 ...pnk | gn1
=
| gn2
=
|...
=
where dn1
dn2
..
.
en1
en2
...
Informatik 2 · Sommer 2016
171
5 · Funktionsdefinitionen
I
1
2
3
4
Lokale Definitionen · 5.3
where-Klauseln sind in allen Guards und rechten Seiten eines Zweiges
sichtbar...
f x y | y > z
| y == z
| otherwise
where z = x*x
= ... z ...
= ... z ...
= ... z ...
Beispiel Euklids Algorithmus zur Bestimmung des größten gemeinsamen
Teilers (ggT):
1
ggT :: Integer -> Integer -> Integer
2
3
4
5
6
ggT x y = ggT’ (abs x) (abs y)
where
ggT’ x 0 = x
ggT’ x y = ggT’ y (x ‘mod‘ y)
~
Mit let werden Ausdrücke konstruiert, dagegen gehört where zur
Syntax der Funktionsdefinition bzw. zur Syntax von case-Ausdrücken.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
172
5 · Funktionsdefinitionen
Lokale Definitionen · 5.3
Beispiel Endgültige Mergesort-Implementierung mittels where und let.
1
2
-- Divide-and-Conquer Sortierung einer Liste
mergesort :: (α -> α -> Bool) -> [α] -> [α]
3
4
mergesort _ [] = []
5
6
mergesort _ [x] = [x]
7
8
9
10
11
mergesort lt xs
= let (l1,l2) = split xs
in merge (mergesort lt l1) (mergesort lt l2)
where
12
13
14
15
16
-- splitte eine
split []
split [x]
split (x:x’:xs)
17
Liste in zwei gleich lange Teile
= ([],[])
= ([x],[])
= let (l1,l2) = split xs
in (x:l1,x’:l2)
18
19
20
21
22
23
24
-- mische zwei sortierte Listen
merge [] ys = ys
merge xs [] = xs
merge l1@(x:xs) l2@(y:ys)
| x ‘lt‘ y = x : merge xs l2
| otherwise = y : merge l1 ys
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
173
5 · Funktionsdefinitionen
5.4
Layout (2-dimensionale Syntax) · 5.4
Layout (2-dimensionale Syntax)
I
Haskells Syntax verzichtet auf Separator- oder Terminator-Symbole
wie ‘;’, um bspw. einzelne Deklarationen voneinander abzugrenzen.
I
Trotzdem analysiert Haskells Parser etwa den Ausdruck
1
2
3
let y
= a * b
f x = (x+y)/y
in f c + f d
eindeutig wie erwartet (der let-Ausdruck definiert den Wert y und die
Funktion f) und nicht als
1
2
3
let y
= a * b f
x
= (x+y)/y
in f c + f d
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
174
5 · Funktionsdefinitionen
Layout (2-dimensionale Syntax) · 5.4
Haskell erreicht dies durch eine 2-dimensionale Syntax (Layout):
I
Die Einrückungen (Spalte im Quelltext) der einzelnen Deklarationen
hinter dem Schlüsselwort let wird zur Auflösung von Mehrdeutigkeiten
herangezogen.
I
Das Layout des Quelltextes ist relevant jeweils
• in Funktionsdefinitionen und
• nach den Schlüsselworten
let
(lokale Dekl.)
Stefan Klinger · DBIS
where
(lokale Dekl.)
of
(case-Alternativen)
Informatik 2 · Sommer 2016
do
(monadische Seq.)
175
5 · Funktionsdefinitionen
Layout (2-dimensionale Syntax) · 5.4
Haskells Layout wird durch einfache Vereinbarungen definiert:
Abseits-Regel (off-side rule)
1. Das erste Token nach einem let, where, of oder do definiert die obere
linke Ecke einer Box.
ggT x y = ggT’ (abs x) (abs y)
where ggT’ x 0 = x
ggT’ x y =
ggT (x ‘rem‘ y)
..
.x ‘rem‘ y = x - y * (x ‘div‘ y)
2. Das erste Token, das links von der Box im Abseits steht, schließt die
Box (hier: kgV):
ggT x y = ggT’ (abs x) (abs y)
where ggT’ x 0 = x
ggT’ x y =
ggT (x ‘rem‘ y)
x ‘rem‘ y = x - y * (x ‘div‘ y)
kgV x y = ...
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
176
5 · Funktionsdefinitionen
Layout (2-dimensionale Syntax) · 5.4
Struktur innerhalb der Box
Haskell kennt eine explizite Syntax in der Deklarationen/Alternativen
explizit gruppiert (mittels { · }) und voneinander trennt (mittels ‘;’) sind.
I
• let besitzt bspw. die alternative Syntax:
let { d1 ; d2 ; ... ; dn ;? } in e
1. Vor der Box wird eine öffnende Klammer { eingefügt,
2. hinter der Box wird eine schließende Klammer } eingefügt,
3. vor einer Zeile, die direkt an der linken Box-Grenze startet, wird ein
Semikolon ; eingefügt. (eine neue Deklaration/Alternative beginnt)
Die explizite Syntax für das ggT-Beispiel lautet daher:
I
1
2
3
4
5
6
ggT x y = ggT’ (abs x) (abs y)
where {ggT’ x 0 = x
;ggT’ x y =
ggT (x ‘rem‘ y)
;x ‘rem‘ y = x - y * (x ‘div ‘y)
}kgV x y = ...
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
177
5 · Funktionsdefinitionen
Layout (2-dimensionale Syntax) · 5.4
Die Mehrdeutigkeit des ersten Beispiels wird wie folgt aufgelöst (Box und
explizite Syntax):
let {y = a * b
;f x = (x+y)/y
}in
f c + f d
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
178
5 · Funktionsdefinitionen
I
Layout (2-dimensionale Syntax) · 5.4
Die vorhergehenden Vereinbarungen zum Layout erlauben die explizite
Nutzung von Semikolons ; um Deklarationen innerhalb einer Zeile zu
trennen. Erlaubt wäre beispielsweise
let d1 ; d2 ; d3
d4 ; d5
in e
I
Der Parser fügt automatisch ein schließende Klammer } ein, wenn so ein
Syntaxfehler vermieden werden kann. Beispiel:
let x = 3 in x+x
wird expandiert zu
let {x = 3 }in x+x
I
Sobald der Programmierer von sich aus Klammern { · } setzt, sind die
Layout-Regeln außer Kraft gesetzt.
I
Kommentare (--... und {-...-}) werden bei der Bestimmung des
Abseits-Tokens nicht beachtet.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
179
5 · Funktionsdefinitionen
I
Layout (2-dimensionale Syntax) · 5.4
Die Reichweite einer Funktionsdefinition wird ebenfalls mittels einer
Layout-Box erklärt:
split xs = nsplit ...

alle Zeilen, die rechts
 von der Boxgrenze
..

.
 beginnen, gehören zur
Definition von split




nsplit
 0 xs (l1,_) = ...

ein Token direkt an der
..


.
linken Grenze startet
eine neue Box
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
180
5 · Funktionsdefinitionen
Layout (2-dimensionale Syntax) · 5.4
Bibliographie
I
Simon Thompson. Haskell: the Craft of Functional Programming.
Addison-Wesley, 1997.
I
Paul Hudak, John Peterson, and Joseph H. Fasel. A Gentle Introduction
to Haskell. Yale University, University of California, 1997.
http://haskell.org/tutorial/.
I
Simon Marlow (editor). Haskell 2010 Language Report.
https://www.haskell.org/onlinereport/haskell2010/.
I
John Hughes, and Simon L. Peyton Jones (editors). Haskell 98: A
Non-strict, Purely Functional Language.
http://www.haskell.org/onlinereport/.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
181
6
Referenzielle Transparenz
Nach der alten Rechtschreibung: Referentielle Transparenz;
engl.: referential transparency.
6 · Referenzielle Transparenz
Betrachte 4 Anweisungen einer imperativen Programmiersprache (etwa
Pascal):
1
R := f(m)*n + f(n)*m;
(*
○
1 *)
R := f(m) + f(m);
(*
○
2 *)
R := 2 * f(m);
(*
○
3 *)
R := f(23) + g(42);
(*
○
4 *)
if f() and f() and f() then ...
(*
○
5 *)
2
3
4
5
6
7
8
9
I
1 einen Einfluss auf
Hat die Reihenfolge der beiden Aufrufe von pop in ○
den Wert von R? Können beide Aufrufe parallel erfolgen?
I
2 und ○
3 äquivalent?
Sind die Anweisungen ○
I
Sind + und * kommutativ?
I
5 eine Vereinfachung mittels der Äquivalenz
Ist in ○
“f() and f() ≡ f()” möglich?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
183
6 · Referenzielle Transparenz
Seiteneffekte in imperativen Sprachen
Generell muss die Antwort hier nein lauten! Wo liegt das Problem?
I
Um sein Resultat zu errechnen, kann f auf globale Variablen zugreifen,
die sich von Aufruf zu Aufruf geändert haben können.
I
f kann während der Ausführung die (Speicherinhalte der) Variablen m
und n ändern, also Seiteneffekte erzielen.
Der Wert eines Ausdrucks (hier bspw. f)
I
hängt nicht nur von diesem Ausdruck selbst (hier also der Definition von
f und fs Parametern), sondern vom Speicher- und Registerzustand
der gesamten Maschine ab,
I
wird beeinflusst von der Reihenfolge der Auswertung seiner
Teilausdrücke.
Im mathematischen Sinne ist eine imperative Prozedur f eben keine
Funktion, d.h.
x == y ⇒
6
f x == f y
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
184
6 · Referenzielle Transparenz
Keine Seiteneffekte in funktionalen Sprachen
In funktionalen Programmen stehen Namen für Werte und nicht für
Speicherorte mit veränderlichem Inhalt.
Definition Referenzielle Transparenz
Eine Sprache heißt referenziell transparent, wenn der Wert eines
Ausdrucks nur von seinen freien Variablen abhängt.
Konsequenzen:
Ein Name darf jederzeit durch den von ihm definierten Wert ersetzt
werden.
(Das eröffnet unter Umständen Möglichkeiten für Optimierungen.)
I Der Wert eines Ausdrucks ohne freie Variablen hängt nur von den Werten
seiner Teilausdrücke ab.
I
(Es können keine Seiteneffekte durch Variablen-Updates entstehen.)
I
Die Reihenfolge der Auswertung der Teilausdrücke ist irrelevant.
Insbesondere ist parallele Auswertung möglich.
(Ohne Seiteneffekte gibt es in der Ausdrucksauswertung keine Abhängigkeiten oder
Beeinflussungen von außen.)
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
185
6 · Referenzielle Transparenz
Ausdrucksauswertung in funktionalen Sprachen ist referenziell
transparent:
I
Ersetze Namen durch ihre Definitionen.
I
Wende Funktionen auf Argumente an (β-Reduktion des λ-Kalküls).
Ersetzung und Reduktion kann in beliebiger Reihenfolge (auch parallel)
erfolgen.
Beispiel Sei square x = x · x.
square (3 + 4)
_
_
_
square (3 + 4)
_
Def. square
(3 + 4) · (3 + 4)
Def. +
square 7
_
Def. +
(3 + 4) · 7
Def. square
_
7·7
Def. +
7·7
Def. ·
49
Stefan Klinger · DBIS
_
Def. ·
49
Informatik 2 · Sommer 2016
186
6 · Referenzielle Transparenz
Referenzielle Transparenz. . .
ermöglicht einen einfacheren Zugang zur Programmverifikation.
I
• Einzelne Funktionen können unabhängig vom Rest eines größeren
Programmes analysiert werden.
I
eröffnet die Möglichkeit, Programmtransformationen (etwa zur
automatischen Optimierung durch einen Compiler) auszuführen, die
nachweislich die Bedeutung des Programms nicht ändern.
• Äquivalenz von Ausdrücken ist im mathematischen Sinne beweisbar.
I
erlaubt den Aufbau ganzer Programmalgebren.
• Elemente dieser Algebren sind Programme, Operationen sind
Programmtransformationen.
I
erlaubt effizientere Auswertung durch Techniken wie Memoization,
Sharing, Common Subtree Elimination, oder Parallelisierung.
• z.B. werden beim Sharing mehrfach verwendete Ausdrücke nur einmal
ausgewertet und durch Ihr Ergebnis ersetzt, cf. Seite 382.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
187
6 · Referenzielle Transparenz
Interaktion mit der Welt?
Die Referenzielle Transparenz bereitet allerdings auch Probleme:
Input/Output?
I
2
(λx. (x,x)) (putStr "foo")
(putStr "foo", putStr "foo")
1
getChar == getChar
1
Zufall?
I
1
random == random
Konflikt zur referenziellen
Transparenz:
Die Reihenfolge der Auswertung
der Teilausdrücke ist irrelevant.
Der Wert eines Ausdrucks hängt
nur von den Werten seiner
Teilausdrücke ab.
Zeitsensitive Ausdrücke?
I
1
getClockTime == getClockTime
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
188
6 · Referenzielle Transparenz
Ausweg
I/O und andere solche “real world”-Probleme waren lange Zeit Gegenstand
von intensiven, nicht immer erfolgreichen Arbeiten in der FPL Community.
⇒ Monadic I/O wird aktuell als die beste und sauberste Lösung gesehen,
mit solchen Umwelt-/Zustandsabhängigkeiten umzugehen.
I
Darauf kommen wir erst gegen Ende des Semesters wieder zurück...
Konsequenz
Wir können erstmal nicht:
I
Dateien lesen, schreiben, löschen, ...
I
Zufallswerte erzeugen,
I
nach der Uhrzeit fragen,
I
irgendwie mit der Umwelt kommunizieren
I
...
Das macht nix: Wir werden trotzdem viele interessante Programme schreiben.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
189
7
Exkurs: Berechenbarkeit
7 · Exkurs: Berechenbarkeit
7.1
Mächtigkeit von Programmiersprachen · 7.1
Mächtigkeit von Programmiersprachen
Aus mathematischer Sicht ist ein Programm eine Funktion, die eine
Eingabe zusammen mit dem Zustand der Maschine vor Start des
Programmes abbildet auf eine Ausgabe und einen neuen Zustand nach
Programmende.
program :: (State, Input) → (State, Output)
Diese Funktion ist nicht unbedingt auf jeder Eingabe definiert (es ist dann
insbesondere eine partielle Funktion):
I
I
Ein Programm kann fehlschlagen, z.B. bei Division durch Null. (Nagut,
das könnte man noch als Zustand auffassen).
Wichtiger: Ein Programm muss nicht terminieren (es kann sich
“aufhängen”). Hier wird also kein definierter Zustand erreicht.
Offensichtliche Frage Was kann man überhaupt berechnen?
Antwort Tatsächlich ist nicht jede Funktion berechenbar!
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
191
7 · Exkurs: Berechenbarkeit
Mächtigkeit von Programmiersprachen · 7.1
Turing-Maschine
Die Turing-Maschine ist eine abstrakte Rechenmaschine26 .
I
von Neumann Computer (z.B. unsere PCs) entsprechen
Turing-Maschinen mit nur endlichem Hauptspeicher.
Definition Berechenbare Funktionen
Genau die Funktionen die von einer Turing-Maschine berechnet werden
können, heißen berechenbar.
I
Eine Programmiersprache heißt turingmächtig, wenn man mit ihr jede
berechenbare Funktion ausdrücken (implementieren) kann.
• Die meisten “Allzweck”-Programmiersprachen sind turingmächtig27 .
Beispiele: java, Haskell, C, Assembler, der λ-Kalkül, Brainfuck, ...
Beweis: Implementation einer Turing-Maschine in der jeweiligen Sprache.
• Praktische Bedeutung: Wir können prinzipiell jede berechenbare Funktion
implementieren.
26 Vorlesung Konzepte der Informatik oder
27 unter der Annahme es stünde unendlich
Stefan Klinger · DBIS
Theoretische Informatik
viel Hauptspeicher zur Verfügung!
Informatik 2 · Sommer 2016
192
7 · Exkurs: Berechenbarkeit
Mächtigkeit von Programmiersprachen · 7.1
Anmerkungen
I
Wir machen bei diesen Betrachtungen keine Aussage über Laufzeit und
Speicherbedarf des Programmes.
I
Auch die Einfachheit der Implementierung ist hier nicht relevant.
I
Die Definition von “berechenbar” (cf. Seite 192) ist zwar ausreichend,
aber etwas unbefriedigend, falls wir kein offensichtlich “unberechenbares”
Problem beschreiben können.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
193
7 · Exkurs: Berechenbarkeit
7.2
Das Halteproblem · 7.2
Das Halteproblem
Das Halteproblem ist sowohl praktisch relevant, als auch ein klassisches
Beispiel für eine nicht berechenbare Funktion.
Angenommen wir haben ein Programm p (z.B. in Form von Quellcode),
das eine Eingabe x verarbeitet:
I
1
I
void p( Input x )
...
—in einer fiktiven, z.B. imperativen Sprache
Es wäre schön zu wissen, ob p bei einer bestimmten Eingabe x
terminiert, oder “sich aufhängt”.
• Diese Frage ist das Halteproblem für p(x).
• Ausprobieren ist keine Option. (Warum nicht?)
Frage Kann man ein Programm schreiben, welches entscheiden kann ob ein
übergebenes Programm (durch dessen Analyse) terminiert, welches also
das Halteproblem beantwortet?
Anwort Nein, es ist im Allgemeinen nicht berechenbar, ob ein Programm
hält.
(Im Speziellen, für manche Programme also, ist dies möglich.)
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
194
7 · Exkurs: Berechenbarkeit
Das Halteproblem · 7.2
Beweisskizze
Beweis durch Widerspruch
Angenommen wir hätten ein Programm halts,
1
Bool halts( Sourcecode p, Input x ) { ... }
—in einer fiktiven imperativen Sprache
welches für jedes beliebige Programm p und jede beliebige Eingabe x
berechnen kann, ob das Programm p mit Eingabe x terminiert:
halts(p, x) _ True ⇐⇒ p(x) terminiert
halts(p, x) _ False ⇐⇒ p(x) terminiert nicht
Beispiel Anwendung auf ein terminierendes Programm:
1
void test1( Input x ) {
1
2
3
4
~
print(x);
> test1("hello world")
hello world
> halts(test1, "hello world")
True
Anmerkung
Stefan Klinger · DBIS
}
1
2
3
4
> test1(test1)
void test1( Input x ) { print(x); }
> halts(test1, test1)
True
Wir identifizieren hier Programme mit ihrem Quellcode.
Informatik 2 · Sommer 2016
195
7 · Exkurs: Berechenbarkeit
Das Halteproblem · 7.2
Beispiel Anwendung auf ein nicht immer terminierendes Programm:
1
Int test2( Input x ) {
2
2
while( x > 0 ) {
x := 4;
}
3
4
5
6
3
4
5
6
return 42;
7
8
1
}
Erinnerung
Stefan Klinger · DBIS
7
> halts(test2, -23)
True
> test2(-23)
42
> halts(test2, 23)
False
> test2(23)
8
... terminiert nicht
Das Programm halts terminiert auf jeden Fall.
Informatik 2 · Sommer 2016
196
7 · Exkurs: Berechenbarkeit
Das Halteproblem · 7.2
Wir konstruieren einen Widerspruch
Erinnerung
I
1
halts(p, x) berechnet, ob p(x) terminiert.
Konstruieren jetzt ein Programm evil wie folgt:
Int evil( Sourcecode p ) {
• halts wendet das übergebene
2
if (halts(p, p)) {
while (True) {} —Endlosschleife
}
3
4
5
6
• evil kann in einer Endlosschleife
hängen bleiben.
return 1;
7
8
Programm p auf p selbst an. Das
hatten wir schon, cf. Seite 195.
}
Frage
⇒
⇒
Terminiert evil für ein gegebenes Programm p?
p(p) terminiert
halts(p, p) _ True
evil(p) terminiert nicht
Stefan Klinger · DBIS
⇒
⇒
p(p) terminiert nicht
halts(p, p) _ False
evil(p) terminiert
Informatik 2 · Sommer 2016
197
7 · Exkurs: Berechenbarkeit
Erinnerung
⇔
Das Halteproblem · 7.2
Bis jetzt haben wir ein Programm evil konstruiert:
evil(p) terminiert
1
Int evil( Sourcecode p ) { ... }
p(p) terminiert nicht
I
Jetzt wenden wir evil auf sich selbst an.
I
Terminiert evil(evil)?
Aus der Konstruktion folgt:
evil(evil) terminiert
⇔
evil(evil) terminiert nicht
Ein Widerspruch. Also muss unsere Annahme (die Existenz von halts)
falsch sein.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
198
7 · Exkurs: Berechenbarkeit
Das Halteproblem · 7.2
Anmerkungen
I
Es sieht hier so aus als wäre dieser Beweis unabhängig vom
Rechenmodell. Ein formaler Beweis bezieht sich jedoch z.B. auf die
Turing-Maschine, und verwendet keine “fiktive imperative
Programmiersprache”.
I
Es ist tatsächlich kein mächtigeres Konzept für eine Rechenmaschine
bekannt.
I
Turing-Maschine und der einfache untypisierte λ-Kalkül (den wir bisher
besprochen haben) sind gleich mächtig.
• Auch im λ-Kalkül können wir nicht alles berechnen, ...
• ...aber kein formales System kann mehr berechnen.
• Beweisidee: Im λ-Kalkül eine Turing-Maschine beschreiben, und umgekehrt.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
199
7 · Exkurs: Berechenbarkeit
Konsequenzen · 7.3
Konsequenzen
7.3
Für die Theorie...
I
Es gibt tatsächlich Funktionen die prinzipiell nicht berechenbar sind
(Man sagt auch: unentscheidbare Probleme).
...aber auch für die Praxis:
I
Wir können kein Programm bauen, das im Allgemeinen entscheiden kann,
ob sich ein gegebenes Programm “aufhängt”.
I
Dieses Problem erstreckt sich bereits auf Teile von Programmen:
1
2
3
4
5
Anweisung 1;
Anweisung 2;
...
Anweisung n;
print("hello");
Stefan Klinger · DBIS
• Die Anweisungen 1–n formen bereits ein
Teilprogramm.
• Im Allgemeinen ist also nicht entscheidbar, ob
die Anweisung in Zeile 5 jemals ausgeführt wird.
Informatik 2 · Sommer 2016
200
7 · Exkurs: Berechenbarkeit
Konsequenzen · 7.3
Bedeutung für Compiler
I
Ein Compiler hat auch die Aufgabe in unseren Programmen Fehler zu
finden, damit sie nicht erst im Betrieb auffallen.
• Dazu gehören “offensichtliche” Fehler wie z.B. falsche Syntax,
• weniger offensichtliche, wie nicht deklarierte Variablen oder Typfehler,
• und Fehler wie das “Abstürzen” (unerwarteter Zustand) oder “Aufhängen”
(nicht-Erreichen eines definierten Zustandes).
I
Dazu bieten sich zwei Strategien an:
1. Alles zurückweisen von dem der Compiler nicht beweisen kann, dass es
korrekt ist.
I
I
Das ist eine sehr starke Einschränkung. Solche Sprachen lassen viele legitime
Programme nicht mehr zu.
Der Simply Typed λ-Calculus28 ist ein Beispiel dafür. Dieser ist nicht
turingmächtig, dafür terminieren alle damit formulierten Programme.
2. Nur verbieten was der Compiler als definitiv falsch erkennt.
I
28 Eine
Hier wurde die Grenze der erkennbaren Fehler immer weiter verschoben.
Typsysteme sind eine Methode dazu.
Variante des λ-Kalküls auf die wir nicht weiter eingehen werden.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
201
8
Listenverarbeitung
8 · Listenverarbeitung
I
Funktionen, die Listen als Argumente besitzen, orientieren sich oft an
der rekursiven Struktur von Listen (cf. Seite 141):
Rekursionsabbruch Das Argument ist die leere Liste [] :: [α], oder
Rekursion das Argument ist von der Form x:xs mit x :: α und xs :: [α].
I
Die Definition einer listenverarbeitenden Funktion f :: [α] → β nutzt
daher oft eine entsprechende Fallunterscheidung
f [] = z
f (x : xs) = c (h x) (t xs)
wobei
• z :: β das Ergebnis bei Rekursionsabbruch darstellt, und
• im Rekursionsfall
I h :: α → γ auf den Kopf (head), und
I t :: [α] → δ auf den Rest (tail) des Arguments angewandt werden,
I während c :: γ → δ → β das Ergebnis beider Aufrufe kombiniert.
Dabei ruft t typischerweise f selbst rekursiv auf.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
203
8 · Listenverarbeitung
Beispiel Die Summe über eine Liste
1
2
3
sum :: [Integer] -> Integer
sum [] = 0
sum (x:xs) = x + sum xs
1
2
> sum [1..10]
55
I
Dabei sind z = 0, h = id, t = sum und c = (+).
I
Die Identitätsfunktion id :: α → α ist Teil der standard prelude und hat
die Definition id = \x -> x, also id = λx. x
Beispiel map f wendet die Funktion f auf jedes Element einer Liste an.
1
2
3
map :: (α -> β) -> [α] -> [β]
map f [] = []
map f (x:xs) = f x : map f xs
1
2
> map (+1) [1..10]
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
I
Dabei sind z = [], h = f, t = map f und c = (:).
I
Man sagt auch: Eine Funktion über eine Liste mappen.
Frage Welche Funktion f erhält man mittels
z = False, h = (e ==), t = f e und c = ||?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
204
8 · Listenverarbeitung
Haskells Standard Prelude definiert nützliche Funktionen über Listen:
1
2
head (x:_)
tail (_:xs)
= x
= xs
init [x]
init (x:xs)
= []
= x : init xs
1
2
3
3
4
5
4
5
6
6
7
8
11
last [x]
last (_:xs)
= x
= last xs
length []
length (_:xs)
= 0
= 1 + length xs
8
9
12
13
14
17
18
19
take n _ | n <= 0 =
take _ []
=
take n (x:xs)
=
— analog: drop
22
reverse []
reverse (x:xs)
11
filter _ [] = []
filter p (x:xs)
| p x
= x : filter p xs
| otherwise = filter p xs
12
14
[]
++ ys = ys
(x:xs) ++ ys = x : xs ++ ys
15
[]
[]
x : take (n-1) xs
16
17
= []
= reverse xs ++ [x]
concat []
= []
concat (xs:xss) = xs ++ concat xss
18
19
20
20
21
10
13
(x:_) !! 0
= x
(_:xs) !! n | n>0 = xs !! (n-1)
15
16
zip = zipWith (,)
7
9
10
zipWith _ []
_
= []
zipWith _ _
[]
= []
zipWith f (x:xs) (y:ys)
= f x y : zipWith f xs ys
21
22
23
dropWhile _ [] = []
dropWhile p xs@(x:xs’)
| p x
= dropWhile p xs’
| otherwise = xs
— analog: takeWhile
Typen? Funktionsweise? — Die tatsächliche Implementation weicht teilweise ab.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
205
8 · Listenverarbeitung
Was ist Foldable? Sollten das nicht Listen sein?
Moderne Versionen des GHCi zeigen für manche Funktionen
überraschend einen Typ mit Foldable statt mit Listen an:
I
1
2
Prelude> :t length
length :: Foldable t => t a -> Int
I
Das verstehen wir leider erst später, cf. Seite 299.
I
Vorerst stellen wir uns vor es ginge um Listen29 , d.h., wir lesen
Foldable t ⇒ ... t α ...
einfach als
... [α] ...
ersetzen also jedes t α durch [α].
Beispiele Wir verwenden zunächst nur:
null :: [α] → Bool
length :: [α] → Int
concat :: [[α]] → [α]
29 Tatsächlich
statt
statt
statt
null :: Foldable t ⇒ t α → Bool
length :: Foldable t ⇒ t α → Int
concat :: Foldable t ⇒ t [α] → [α]
geht es um allgemeinere Datenstrukturen die wir aber wohl nicht besprechen werden.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
206
8 · Listenverarbeitung
8.1
foldr und foldl · 8.1
foldr und foldl
Das besprochene Rekursionsschema ist in der Standard Prelude mit zwei
Funktionen, foldr (fold right) und foldl (fold left), implementiert.
foldr (auch bekannt unter dem Namen reduce)
I
“reduziert” eine Liste vom Typ [α] zu einem Wert des Typs β.
I
Informell gilt:
(dabei ist ⊕ ein Infix-Operator des Typs α → β → β)
foldr (⊕) z [x1 , x2 , ..., xn ]
≡
x1 ⊕ (x2 ⊕ (· · · (xn ⊕ z) · · · ))
Damit ist foldr :: (α → β → β) → β → [α] → β.
I Eselsbrücken: Die Klammerung ist rechts-assoziativ, und das z erscheint ganz rechts.
Ein mögliche Definition von foldr ist:
1
2
3
foldr :: (α -> β -> β) -> β -> [α] -> β
foldr (⊕) z []
= z
foldr (⊕) z (x:xs) = x ⊕ foldr (⊕) z xs
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
207
8 · Listenverarbeitung
foldr und foldl · 8.1
Alternativ lässt sich der Effekt von foldr damit auch wie folgt illustrieren:
foldr (⊕) z
(x1 : (x2 : (...( xn : [] )...)))
↓ ↓ ↓ ↓
↓ ↓ ↓
= (x1 ⊕ (x2 ⊕ (...( xn ⊕ z )...)))
I
Die Funktion foldr ersetzt also alle Listen-Konstruktoren, und zwar
• : durch ⊕, und
• [] durch z.
Beispiel Viele Standardfunktionen
implementieren:
sum
=
product =
concat
=
and
=
or
=
Stefan Klinger · DBIS
lassen sich einfach mittels foldr
foldr (+) 0
foldr ? ?
foldr ? ?
foldr ? ?
foldr ? ?
Informatik 2 · Sommer 2016
208
8 · Listenverarbeitung
Lösung:
I
foldr und foldl · 8.1
=
=
=
=
=
sum
product
concat
and
or
foldr (+)
0
foldr (*)
1
foldr (++)
[]
foldr (&&) True
foldr (||) False
Mit der Behauptung sum so definieren zu können, behaupte ich eine
Äquivalenz von sum von Folie 204 und foldr (+) 0:
sum ≡ foldr (+) 0
(Die entsprechenden Beweise ggf. später in den Übungen.)
~
Obacht Wir hatten vereinbart (cf. Seite 111), dass zwei λ-Ausdrücke
e1 , e2 äquivalent sind, gdw. sie zur gleichen Normalform m reduziert werden
können, d.h.:
e1 ≡ e2
⇔
∃m. e1 _∗ m ∧ e2 _∗ m
Frage Was ist z.B. mit sum ≡ foldr (+) 0 — gibt es so ein m?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
209
8 · Listenverarbeitung
foldr und foldl · 8.1
Gleichheit von Funktionen
Das Extensionalitätsprinzip
Antwort Leider nicht. Ohne Argumente können wir nicht weit reduzieren.
I
I
Die Aussage dass sich zwei Funktionen gleich verhalten macht aber
natürlich trotzdem Sinn.
Deswegen erweitern wir unsere Definition von Äquivalenz etwas:
Definition Äquivalenz von Funktionen
Zwei Funktionen f , g heißen äquivalent gdw. sie für das gleiche
Argument äquivalente Ergebnisse liefern, d.h.:
f ≡g
⇔
∀x. f x ≡ g x
Für die Äquivalenz auf der rechten Seite bemühen wir diese Definition
erneut, oder verwenden die bekannte Äquivalenz von Seite 111.
I
Dieses Prinzip ist in der Mengenlehre als Extensionalitätsprinzip
bekannt. Man sagt auch: f und g sind extensional gleich.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
210
8 · Listenverarbeitung
foldr und foldl · 8.1
Monoid
Nochmal:
sum
product
concat
and
or
≡
≡
≡
≡
≡
foldr (+)
0
foldr (*)
1
foldr (++)
[]
foldr (&&) True
foldr (||) False
Beobachtung All diesen Beispielen ist gemeinsam, dass ⊕ assoziativ ist
und sich z bzgl. dieser Operation neutral verhält.
Definition Monoid
Eine Menge M mit einer binären Verknüpfung ⊕ :: M × M → M heißt
Monoid, genau dann, wenn
(Dabei sei M das Universum der Quantoren)
I
die Operation ⊕ assoziativ ist,
I
und es ein Neutralelement e gibt.
∀a b c. a ⊕ (b ⊕ c) ≡ (a ⊕ b) ⊕ c
∃e. ∀a. e ⊕ a ≡ a ≡ a ⊕ e
Übrigens: Falls ⊕ :: α → β → β assoziativ ist gilt bereits α = β. Warum?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
211
8 · Listenverarbeitung
I
foldr und foldl · 8.1
Natürlich kann foldr auch auf Strukturen angewendet werden, die keinen
Monoid bilden:
Beispiel
1
2
3
4
filter p
length
reverse
takeWhile p
5
6
7
=
=
=
=
foldr (\x xs -> if p x then x:xs else xs) []
foldr (\_ n -> 1+n) 0
foldr (\x xs -> xs ++ [x]) []
foldr (#) []
where
x # xs | p x
= x:xs
| otherwise = []
Damit könnte takeWhile (<3) [1..4] etwa wie folgt reduziert werden:
1
takeWhile (<3) [1..4]
2
3
4
5
6
Stefan Klinger · DBIS
_
foldr (#) [] (1 : (2 : (3 :
_ 1 # (2 # (3 #
_ 1 : (2 # (3 #
_ 1 : (2 : (3 #
_ 1 : (2 : [])
_ [1,2]
Informatik 2 · Sommer 2016
(4
(4
(4
(4
:
#
#
#
[]))))
[])))
[])))
[])))
212
8 · Listenverarbeitung
foldr und foldl · 8.1
Linksassoziative Variante von foldr
Analog zu foldr gibt es die vordefinierte Funktion foldl.
foldl (also fold left)
I klammert die Listenelemente während der Reduktion nach links.
I Informell gilt:
(dabei ist ⊗ ein Infix-Operator des Typs β → α → β)
foldl (⊗) z [x1 , x2 , ..., xn ]
≡
(· · · ((z ⊗ x1 ) ⊗ x2 ) · · · ) ⊗ xn
Damit ist foldl :: (β → α → β) → β → [α] → β
I Eselsbrücken: Die Klammerung ist links-assoziativ, und das z erscheint ganz links.
Ein mögliche Definition von foldl ist:
1
2
3
foldl :: (β -> α -> β) -> β -> [α] -> β
foldl (⊗) z [] = z
foldl (⊗) z (x:xs) = foldl (⊗) (z ⊗ x) xs
Hier übernimmt z die Rolle eines akkumulierenden Parameters, in dem
das Endergebnis aufgesammelt wird.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
213
8 · Listenverarbeitung
foldr und foldl · 8.1
Beispiel Eine praktische Anwendung von foldl ist die Funktion pack, die
eine Liste von Ziffern [xn−1 , xn−2 , ..., x0 ] mit xi ∈ {0...9} in den durch sie
“dargestellten” Wert transformiert:
n−1
X
xk · 10k
k=0
1
2
3
pack :: [Integer] -> Integer
pack xs = foldl (#) 0 xs
where n # x = 10 * n + x
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
214
8 · Listenverarbeitung
foldr und foldl · 8.1
Das 1. Dualitätstheorem
I
Auch hier gilt: Falls ⊗ :: β → α → β assoziativ ist, dann ist α = β.
I
Dann haben foldl und foldr den gleichen Typ:
foldr, foldl :: (α → α → α) → α → [α] → α
Es gilt sogar:
Satz 1. Dualitätstheorem
Falls (⊗, α) einen Monoid mit Neutralelement e bilden, dann gilt:
foldr (⊗) e
≡
foldl (⊗) e
Beweis Evtl. Übung, nach Kapitel über Induktion (cf. Seite 226).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
215
8 · Listenverarbeitung
foldr und foldl · 8.1
Unmittelbare Konsequenz:
I
Die Funktionen von Seite 211 können wir auch mit foldl bauen.
sum
product
concat
and
or
I
≡
≡
≡
≡
≡
foldl (+)
0
foldl (*)
1
foldl (++)
[]
foldl (&&) True
foldl (||) False
Es bleibt zu klären welche Variante effizienter ist. Das machen wir später
genauer.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
216
8 · Listenverarbeitung
foldr und foldl · 8.1
Das 2. Dualitätstheorem
Auch ohne einen Monoid sind foldr und foldl eng verwandt:
Satz 2. Dualitätstheorem
Falls für alle x, y , z geeigneten Typs gilt
x ⊕ (y ⊗ z)
x ⊕y
≡ (x ⊕ y ) ⊗ z
und
≡ y ⊗x
dann gilt
foldr (⊕)
≡
foldl (⊗)
Beweis Übung, nach Kapitel über Induktion (cf. Seite 226).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
217
8 · Listenverarbeitung
Beispiel
foldr und foldl · 8.1
length = foldr (λ n. 1 + n) 0
length0 = foldl (λn . 1 + n) 0
I
Diese beiden Funktionen sind äquivalent.
I
Die Variante mit foldl kann effizienter ausgewertet werden.
(Dazu müssen wir aber noch mehr über Auswertestrategien wissen, cf. später.)
Beweis mit dem 2. Dualitätstheorem. Zu zeigen:
foldr (λ n. 1 + n) 0
|
{z
}
≡
⊕
I
Zeigen x ⊕ z ≡ z ⊗ x:
≡
≡
I
⊗
Zeigen x ⊕ (y ⊗ z) ≡ (x ⊕ y ) ⊗ z:
x ⊕z
1+z
z ⊗x
Stefan Klinger · DBIS
foldl (λn . 1 + n) 0
|
{z
}
≡
≡
x ⊕ (y ⊗ z)
1 + (y ⊗ z)
1 + (1 + y )
Informatik 2 · Sommer 2016
≡
≡
(x ⊕ y ) ⊗ z
1 + (x ⊕ y )
1 + (1 + y )
218
8 · Listenverarbeitung
foldr und foldl · 8.1
Das 3. Dualitätstheorem
Schließlich gilt noch:
Satz 3. Dualitätstheorem
Sei reverse wie auf Seite 205 definiert, und sei flip f x y = f y x. Dann gilt
für alle ⊕, z und xs geeigneten Typs:
foldr (⊕) z xs
≡
foldl (flip (⊕)) z (reverse xs)
Mit anderen Worten: Mit x ⊕ y ≡ y ⊗ x gilt:
foldr (⊕) z xs
≡
foldl (⊗) z (reverse xs)
Beweis Kann man als (aufwändige) Übung machen, nach Kapitel über
Induktion (cf. Seite 226).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
219
8 · Listenverarbeitung
foldr und foldl · 8.1
Unendliche Listen
~
Vorsicht Eine Bemerkung zu foldl/foldr auf unendlichen Listen:
foldl (⊗) z (x:xs)
= foldl (⊗) (z ⊗ x) xs
I
foldr (⊕) z (x:xs)
= x ⊕ foldr (⊕) z xs
Für die nicht-leere Liste ruft sich foldl sofort rekursiv selbst auf, und das
Ergebnis des rekursiven Aufrufs ist das Ergebnis der Funktion.
(Funktionen mit der zweiten Eigenschaft nennt man endrekursiv, tail recursive.)
I
Bei foldr hängt das Ergebnis hingegen von ⊕ ab. Der rekursive Aufruf
von foldr ist nur Argument von ⊕.
⇒ Konsequenz:
• foldl terminiert sicher nicht auf unendlichen Listen!
• Bei foldr entscheidet der Operator ⊕ ob Rekursion stattfindet und kann
vor Ende der Liste abbrechen. (cf. Seite 212, takeWhile)
Beispiel head ≡ foldr const ⊥
Stefan Klinger · DBIS
Funktioniert auf unendlichen Listen.
Informatik 2 · Sommer 2016
220
8 · Listenverarbeitung
foldr und foldl · 8.1
Anmerkungen
I
Die Ersetzung der Konstruktoren eines Datentyps (hier für Listen, also
cons (:) und nil []) durch Operatoren bzw. Werte ist ein Prinzip, das
sich auch für andere konstruierte Datentypen sinnvoll anwenden
lässt.
I
Im Gegensatz zu foldr ersetzt foldl nicht nur die Konstruktoren, sondern
ändert die Klammerung der rechtstief konstruierten Liste. Insofern
nimmt es eine Sonderrolle ein.
Beide Punkte werden wir später wieder aufgreifen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
221
8 · Listenverarbeitung
8.2
Effizienz · 8.2
Effizienz
Die Anzahl der Reduktionen bei der Reduktion eines Ausdrucks auf seine
Normalform ist in FPLs ein naheliegendes Maß für die Komplexität einer
Berechnung.
Beispiel Die Länge der ersten Argumentliste
bestimmt die Anzahl der Reduktionen der
Listen-Konkatenation ++.
[3,2] ++ [1]
I
++.2 steht dabei für die 2. Zeile der
Definition von ++, cf. Seite 205.
_
I
Für die Auswertung von xs ++ ys mit
length xs _ n werden n Reduktionen via
++.2, gefolgt von einer Reduktion via ++.1
benötigt.
_
I
Die letzte Zeile ist lediglich eine Änderung
der Schreibweise.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
_
≡
++.2
3:([2] ++ [1])
++.2
3:(2:([] ++ [1]))
++.1
3:(2:[1])
[3,2,1]
222
8 · Listenverarbeitung
Effizienz · 8.2
Beispiel Ähnliche Überlegungen gelten für reverse, das in seiner
Definition ++ nutzt:
reverse [1,2,3]
_
_
_
_
_
_
_
Gilt length xs _ n, dann benötigt
reverse xs
reverse.2
reverse [2,3] ++ [1]
reverse.2
(reverse [3] ++ [2]) ++ [1]
I
n Reduktionen via reverse.2,
I
gefolgt von einer Reduktion via
reverse.1,
I
gefolgt von
reverse.2
((reverse [] ++ [3]) ++ [2]) ++ [1]
reverse.1
1 + 2 + ... + n =
(([] ++ [3]) ++ [2]) ++ [1]
++.1
([3] ++ [2]) ++ [1]
++.2, ++.1
[3,2] ++ [1]
++.2, ++.2, ++.1
n · (n + 1)
2
Reduktionen, um mittels ++ die
Konkatenationen auszuführen.
Damit ist die Anzahl der
Reduktionen in O n2 .
[3,2,1]
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
223
8 · Listenverarbeitung
Effizienz · 8.2
Listeninvertierung in linearer Zeit
Eine Liste lässt sich aber durchaus in linearer Zeit (proportional zur
Listenlänge n) reversieren:
1
2
3
4
5
rev :: [α] -> [α]
rev xs = shunt [] xs
where
shunt ys []
= ys
shunt ys (x:xs) = shunt (x:ys) xs
I
I
Tatsächlich reversiert shunt ys xs nicht nur die Liste xs, sondern
konkateniert diese zusätzlich auch noch mit ys.
Das erste Argument ys von shunt wird akkumulierender Parameter
genannt:
• Zwischenergebnisse der Berechnung werden an die nächste Rekursion
weitergegeben.
• Am Ende der Rekursion enthält ys das Ergebnis (oder einen Teil davon).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
224
8 · Listenverarbeitung
Effizienz · 8.2
Beispiel rev xs benötigt lineare Anzahl (proportional zu (length xs))
Reduktionen
rev [1,2,3]
_
_
_
_
_
rev.1
shunt [] [1,2,3]
shunt.2
shunt [1] [2,3]
shunt.2
shunt [2,1] [3]
shunt.2
shunt [3,2,1] []
shunt.1
[3,2,1]
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
225
8 · Listenverarbeitung
8.3
I
Induktion über Listen · 8.3
Induktion über Listen
Dank referenzieller Transparenz kann man Behauptungen wie
rev ≡ reverse
relativ einfach beweisen.
I
Beweise über listenverarbeitende Funktionen können häufig mittels
Induktion über Listen, analog zu Induktionsbeweisen für Behauptungen
über Elemente aus N, geführt werden:
1. Induktionsanfang (aka. Induktionsverankerung).
I
Beweise die Aussage mit der leeren Liste [].
2. Induktionsschritt von xs zu x : xs.
I
Übung
Dabei wird die Induktionshypothese (die Aussage mit einer beliebigen, festen
Liste xs) verwendet, um die Aussage mit x : xs für alle x zu zeigen.
Für alle Listen xs gilt xs ++ [ ] ≡ xs.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
226
8 · Listenverarbeitung
Induktion über Listen · 8.3
Beispiel Beweisen rev xs ≡ reverse xs für alle Listen xs.
I
Da rev xs _ shunt [ ] xs (cf. Seite 224), genügt es, die allgemeinere
Behauptung zu zeigen:
shunt ys xs
≡
reverse xs ++ ys
, für alle xs, ys :: [α]
Induktionsverankerung Sei xs = [ ], und ys :: [α] beliebig.
shunt ys [ ] ≡ ys
≡ [ ] ++ ys
≡ reverse [ ] ++ ys
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
(shunt.1)
(++.1)
(reverse.1)
227
8 · Listenverarbeitung
Induktion über Listen · 8.3
Induktionshypothese Für ein beliebiges, festes xs :: [α], und für alle ys :: [α]
gelte:
shunt ys xs ≡ reverse xs ++ ys
Induktionsschritt Von xs zu x : xs. Seien x :: α, und ys :: [α].
Mit dem xs aus der Induktionshypothese gilt:
shunt ys (x : xs)
≡
≡
≡
≡
≡
≡
shunt (x : ys) xs
reverse xs ++ (x : ys)
reverse xs ++ (x : ([ ] ++ ys))
reverse xs ++ ([x] ++ ys)
(reverse xs ++ [x]) ++ ys
reverse (x : xs) ++ ys
(shunt.2)
(Hypothese)
(++.1 rückw.)
(++.2 rückw.)
(++ assoziativ)
(reverse.2)
Übung Die hier verwendete Annahme über die Assoziativität von ++ müssen wir
ebenfalls noch beweisen: xs ++ (ys ++ zs) ≡ (xs ++ ys) ++ zs. Auch hier führt
Listeninduktion über den Parameter zum Ziel, über den die Rekursion der betrachteten
Funktion ++ formuliert ist, also xs.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
228
8 · Listenverarbeitung
8.4
Programm-Synthese · 8.4
Programm-Synthese
Bei der Beweisführung über Programme werden Eigenschaften eines
gegebenen Programms Schritt für Schritt nachvollzogen und dadurch
bewiesen (s. rev und reverse).
Programm-Synthese kehrt dieses Prinzip um:
I
Gegeben ist eine formale Spezifikation eines Problems,
I
gesucht ist ein problemlösendes Programm, das durch schrittweise
Umformung der Spezifikation gewonnen (synthetisiert) wird.
Wenn die Transformationen diszipliniert vorgenommen werden, kann die
Synthese als Beweis dafür gelesen werden, dass das Programm die
Spezifikation erfüllt (der Traum aller Software-Ingenieure).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
229
8 · Listenverarbeitung
Programm-Synthese · 8.4
Beispiel Die Funktion init der standard prelude bestimmt das initiale
Segment ihres Listenargumentes, also etwa
init [1..10] _ [1, 2, 3, 4, 5, 6, 7, 8, 9].
I
Damit wäre eine naheliegende Spezifikation für init die folgende:
init xs = take (length xs - 1) xs
wobei xs endlich und nicht leer
“Nimm alle Elemente von xs, aber nicht das letzte Element”
I
Die Synthese versucht eine effizientere Variante von init abzuleiten
(die Spezifikation wäre ja prinzipiell schon ausführbar, traversiert xs zur
Berechnung des Ergebnisses aber zweimal).
Für die Synthese instantiieren wir xs
1. mit [x] und
2. mit x1 : x2 : xs.
Jede nichtleere Liste besitzt die eine oder die andere Form.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
230
8 · Listenverarbeitung
Programm-Synthese · 8.4
Fall 1 [x]
init [x]
=
— Instanziierung
Spezifikation
take (length [x] − 1) [x]
=
length, Arithmetik
take 0 [x]
=
take.1
[]
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
231
8 · Listenverarbeitung
Programm-Synthese · 8.4
Fall 2 x1 : x2 : xs
init (x1 : x2 : xs)
=
— Instanziierung
Spezifikation
take (length (x1 : x2 : xs) − 1) (x1 : x2 : xs)
=
length, Arithmetik
take (length xs + 1) (x1 : x2 : xs)
=
take.3
x1 : take (length xs) (x2 : xs)
=
=
length.2, Arithmetik
x1 : take (length (x2 : xs) − 1) (x2 : xs)
x1 : init (x2 : xs)
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
232
8 · Listenverarbeitung
Programm-Synthese · 8.4
Zusammenfassen der beiden so erhaltenen Gleichungen ergibt
1
2
3
init :: [a] -> [a]
init [x]
= []
init (x1:x2:xs) = x1 : init (x2:xs)
Weitere Verbesserungen
1
2
3
4
I
Das wiederholte Zerlegen und Zusammensetzen der Liste x2 : xs kann
man sich sparen.
I
Für die leere Liste geben wir einen brauchbaren Fehler aus.
init
init
init
init
:: [a] -> [a]
[x]
= []
(x:xs) = x : init xs
[]
= error "init: empty list"
Übung Versuchen Sie Ihren Haskell-Code durch simple
Äquivalenzumformungen zu verbessern. Manchmal gewinnt man dabei
überraschende Einsichten.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
233
8 · Listenverarbeitung
8.5
List Comprehensions · 8.5
List Comprehensions
List Comprehensions sind vor allem in modernen FPLs als eine alternative
Notation für Operationen auf Listen verbreitet30 .
I
Die Notation mittels Set Comprehension31 ist aus der Mathematik
(Mengenlehre) bekannt. Die Idee dabei:
Term Prädikat+
• Beschreibt eine Menge, deren Elemente jeweils von Term beschrieben werden,
• unter den durch die Prädikate bestimmten Bedingungen.
I
List Comprehensions erweitern die Ausdruckskraft der Sprache nicht,
erlauben aber oft eine kompakte, leicht lesbare und elegante Notation
von Listenoperationen.
Wir werden eine Abbildung auf den Haskell-Kern besprechen.
30 Miranda™ (Dave Turner, 1976) sah als erste FPL List Comprehensions syntaktisch
31 aka. “set-builder notation”. Einen deutschen Begriff scheint’s nicht zu geben.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
vor.
234
8 · Listenverarbeitung
List Comprehensions · 8.5
Beispiel Die Menge aller natürlichen geraden Zahlen kann durch eine
set comprehension kompakt notiert werden:
{n | n ∈ N, n mod 2 = 0}
Eine entsprechende List Comprehension (die unendliche Liste aller geraden
Zahlen) wird syntaktisch ganz ähnlich notiert:
[ n | n <- [0 .. ], n ‘mod‘ 2 == 0 ]
Beispiel Die Standardfunktionen map und filter sind mittels List
Comprehensions ohne die sonst notwendige Rekursion formulierbar:
1
2
map :: (α -> β) -> [α] -> [β]
map f xs = [ f x | x <- xs ]
Stefan Klinger · DBIS
1
2
filter :: (α -> Bool) -> [α] -> [α]
filter p xs = [ x | x <- xs, p x ]
Informatik 2 · Sommer 2016
235
8 · Listenverarbeitung
List Comprehensions · 8.5
Syntax der List Comprehension
Die allgemeine Form einer List Comprehension ist
[ e | q1 , q2 , ..., qn ]
wobei
I
der Kopf e ein beliebiger Ausdruck ist, und
I
die Qualifier qi (mit n ≥ 1), eine von drei Formen besitzen:
Generator pi <- ei , wobei ei :: [αi ], und pi ein Pattern für Werte des Typs
αi ist — schreiben salopp pi :: αi .
Prädikat qi :: Bool.
lokale Bindung let { pi1 = ei1 ; pi2 = ei2 ... }. Dabei sind die eij beliebige
Ausdrücke, und die pij entsprechende Patterns.
Beispiel von vorhin: [ n | n <- [0 .. ], n ‘mod‘ 2 == 0 ] .
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
236
8 · Listenverarbeitung
List Comprehensions · 8.5
ListComp
→
[ Expr | Qual (, Qual)∗ ]
Qual
→
|
|
Pattern <- Expr
— Pattern :: α, Expr :: [α]
Expr
— Expr :: Bool
— cf. Seite 167
let { Pattern = Expr (; Pattern = Expr)∗ }
— Zusammenfassung
Semantik der List Comprehension — in Worten
I
Ein Generator qi = pi <- ei versucht das Pattern pi der Reihe nach
gegen die Elemente der Liste ei zu matchen.
• Für jeden erfolgreichen Match werden die nachfolgenden Qualifier
qi+1 , ..., qn ausgewertet.
• Die durch den Match gebundenen Variablen des Patterns pi sind in den
nachfolgenden Qualifiern sichtbar und an entsprechende Werte gebunden.
I
Eine lokale Bindung (let) kann ebenfalls Variablen an Werte binden.
I
Jede Bindung wird solange nach rechts propagiert, bis ein Prädikat
unter ihr zu False evaluiert wird.
I
Der Kopf e wird für alle Bindungen ausgewertet, die alle Prädikate
passieren konnten.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
237
8 · Listenverarbeitung
List Comprehensions · 8.5
Entsprechend dieser Semantik wird also in [ e | p1 <- e1 , p2 <- e2 ]
I
zuerst über die Domain e1 des Generators p1 <- e1 iteriert, und dann
I
für jeden Match von p1 über die Domain e2 des Generators p2 <- e2 .
Dies trifft die Intuition der aus der Mengenlehre bekannten Set
Comprehension:
[ (x,y) | x <- [x1 , x2 ], y <- [y1 , y2 ] ]
_
Stefan Klinger · DBIS
[(x1 ,y1 ), (x1 ,y2 ), (x2 ,y1 ), (x2 ,y2 )]
Informatik 2 · Sommer 2016
238
8 · Listenverarbeitung
List Comprehensions · 8.5
Beispiel Elegante (aber ineffiziente) Variante von Quicksort als 2-Zeiler
1
2
3
4
5
qsort :: (α -> α -> Bool) -> [α] -> [α]
qsort _
[]
= []
qsort (<) (x:xs) = qsort (<) [ y | y <- xs, y < x ]
++ [x] ++
qsort (<) [ y | y <- xs, not (y < x) ]
Beachte: In der split-Phase dieser Implementation wird die Liste xs jeweils
(unnötigerweise) zweimal durchlaufen.
Beispiel Matrix über Typ α als Liste von Zeilenvektoren (wiederum
Listen). Bestimme ersten Spaltenvektor:
1
2
firstcol :: [[α]] -> [α]
firstcol m = [ e | (e:_) <- m ]
firstcol nutzt die Möglichkeit, in Generatoren Patterns zu spezifizieren.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
239
8 · Listenverarbeitung
List Comprehensions · 8.5
Beispiel Alle Permutationen einer Liste xs
1. Die leere Liste [ ] hat sich selbst als einzige Permutation.
2. Wenn xs nicht leer ist, wähle ein Element a aus xs und stelle a den
Permutationen der Liste xs ohne a voran.
3. Führe 2. für jedes Element der Liste xs aus.
1
2
3
perms :: [Integer] -> [[Integer]]
perms [] = [[]]
perms xs = [ a:p | a <- xs, p <- perms $ xs \\ [a] ]
4
5
6
> perms [2, 3]
[[2, 3], [3, 2]]
I
I
Dabei entfernt die Listendifferenz xs \\ ys alle Elemente von ys aus der
Liste xs, etwa: [1, 2, 1, 2, 3] \\ [2, 5] _ [1, 1, 3].
Wie könnte man \\ implementieren?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
240
8 · Listenverarbeitung
List Comprehensions · 8.5
Beispiel Berechne alle Pythagoräischen Dreiecke mit Seitenlänge ≤ n
1
pyth n = [ (a,b,c) | c <- [1..n], a <- [1..c], b <- [1..a], a^2 + b^2 == c^2 ]
2
3
4
> pyth 20
[(4,3,5),(8,6,10),(12,5,13),(12,9,15),(15,8,17),(16,12,20)]
Beispiel Was berechnet die folgende Funktion bar? Wie lautet ihr Typ?
1
bar xs = [ x | [x] <- xs ]
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
241
8 · Listenverarbeitung
Beispiel
1
2
List Comprehensions · 8.5
“Join” zwischen zwei Listen bzgl. eines Prädikates p
join :: (α -> β -> γ) -> (α -> β -> Bool) -> [α] -> [β] -> [γ]
join f p xs ys = [ f x y | x <- xs, y <- ys, p x y ]
Den “klassischen relationalen Join” R1 o
nfst=fst R2 auf binären Relationen
Ri , erhält man dann durch
1
2
3
4
5
foo = join
(\x y -> (fst x, snd x, snd y))
(\x y -> fst x == fst y)
[(1, "John"), (2, "Jack"), (3, "Bonnie")]
[(2, "Ripper"), (1, "Doe"), (3, "Parker"), (2, "Dalton"), (1, "Cleese")]
6
7
8
9
Prelude> foo
[ (1, "John", "Doe"), (1, "John", "Cleese"), (2, "Jack", "Ripper")
, (2, "Jack", "Dalton"), (3, "Bonnie", "Parker")]
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
242
8 · Listenverarbeitung
List Comprehensions · 8.5
Operationale Semantik für List Comprehensions
Die Semantik der List Comprehensions, welche wir vorhin (cf. Seite 237)
eher durch “hand-waving” erklärt haben, lässt sich —ganz ähnlich wie bei
der β-Reduktion des λ-Kalküls— durch Reduktionsregeln formal erklären.
Definition Semantik der List Comprehension, ohne Pattern Matching
Sei e ein Haskell-Ausdruck, v Variable, qs eine Sequenz32 von Qualifiern.
Die folgenden Regeln reduzieren jeweils den ersten Qualifier:
○
1
[ e | v <- [], qs ] _ []
[ e | v <- (x : xs), qs ] _ [ e | qs ][v x]
2
++ [ e | v <- xs, qs ] ○
[ e | False, qs ] _ []
[ e | True, qs ] _ [ e | qs ]
[ e | ] _ [ e ]
32 qs
○
3
○
4
○
5
ist keine Haskell-Liste, sondern ein Konstrukt der Meta-Ebene, cf. Seite 237.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
243
8 · Listenverarbeitung
List Comprehensions · 8.5
I
Die ersten beiden Reduktionsregeln reduzieren einen Generator über einer
1 bzw. nichtleeren ○
2 Liste.
leeren ○
I
3 und ○
4 testen Prädikate.
Regeln ○
I
5 ist anwendbar, sobald die Sequenz der Qualifier vollständig
Regel ○
reduziert wurde.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
244
8 · Listenverarbeitung
List Comprehensions · 8.5
Beispiel Reduktion von [ x^2 | x <- [1,2,3], odd x ]
[ x^2 | x <- [1,2,3], odd x ]
_
=
_
_
_
_
2
verwenden ○
[ x^2 | odd x ][x 1] ++ [ x^2 | x <- [2,3], odd x ]
|
{z
}
A
[ 1^2 | odd 1 ] ++ A
[ 1^2 | True ] ++ A
4
verwenden ○
[ 1^2 | ] ++ A
5
verwenden ○
[1^2] ++ A
A
z
}|
{
1 : [ x^2 | x <- [2,3], odd x ]
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
245
8 · Listenverarbeitung
List Comprehensions · 8.5
1 : [ x^2 | x <- [2,3], odd x ]
_
=
_
_
_
_
_
=
2
verwenden ○
1 : [ x^2 | odd x ][x
2] ++ [ x^2 | x <- [3], odd x ]
|
{z
}
1 : [ 2^2 | odd 2 ] ++ B
B
1 : [ 2^2 | False ] ++ B
3
verwenden ○
1 : [] ++ B
B
z
}|
{
1 : [ x^2 | x <- [3], odd x ]
2
verwenden ○
1 : [ x^2 | odd x ][x
3] ++ [ x^2 | x <- [], odd x ]
1
links wie gehabt (_ 9), und rechts verwenden wir ○
1 : 9 : []
[1, 9]
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
246
8 · Listenverarbeitung
List Comprehensions · 8.5
Abbildung von List Comprehensions auf den Haskell-Kern
I
Prinzipiell erlaubt das System der eben besprochenen Reduktionsregeln,
List Comprehensions auf in Haskell vordefinierte Funktionen
zurückzuführen.
I
Im Folgenden betrachten wir ein Übersetzungsschema J·K, das List
Comprehensions aus Haskell-Code entfernt33 :
J Code mit List Comprehension K = Code ohne List Comprehension
I
J·K kann vom Compiler auf Haskell-Quellcode angewandt werden, um
äquivalenten Code ohne List-Comprehensions zu erhalten.
33 Hier
verwenden wir semantische Klammern etwas anders als gewohnt.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
247
8 · Listenverarbeitung
1
2
List Comprehensions · 8.5
Dabei basiert das Schema J·K auf der Funktion concatMap:
concatMap :: (α -> [β]) -> [α] -> [β]
concatMap f = foldr (\x xs -> f x ++ xs) []
Frage Was tut diese Funktion?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
248
8 · Listenverarbeitung
1
2
List Comprehensions · 8.5
concatMap :: (α -> [β]) -> [α] -> [β]
concatMap f = foldr (\x xs -> f x ++ xs) []
3
4
5
> concatMap (replicate 3) "hello"
"hhheeellllllooo"
Antwort concatMap f xs wendet die Funktion f auf jedes Element von xs
an. Dabei gibt f jeweils eine Liste zurück, welche von concatMap
konkateniert werden.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
249
8 · Listenverarbeitung
List Comprehensions · 8.5
Übersetzungsschema J·K
immer noch ohne Pattern Matching
Sei e ein Ausdruck, v eine Variable, b ein Boolescher Ausdruck, und qs
wieder eine Sequenz von Qualifiern.
J[ e | v <- xs, qs ]K = concatMap (λv . J[ e | qs ]K) JxsK
○
1
2
J[ e | b, qs ]K = if JbK then J[ e | qs ]K else [] ○
J[ e | ]K = [ JeK ]
JeK = Wende J·K rekursiv auf alle nicht-primitiven
○
3
○
4
Teilausdrücke von e an.
I
I
1 : Generatoren; ○
2 : Prädikate)
Wieder wird die Sequenz der Qualifier (○
3 den Fall ohne Qualifier behandeln kann.
reduziert, bis ○
4 steigt rekursiv im AST ab, um evtl. weitere List
Regel ○
Comprehensions zu übersetzen.
2 , statt einfach b zu schreiben?
Frage Wozu brauchen wir JbK in Regel ○
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
250
8 · Listenverarbeitung
List Comprehensions · 8.5
Beispiel Übersetzung von [ x^2 | x <- [1..5], odd x ]
=
=
=
=
J[ x^2 | x <- [1..5], odd x ]K
○
1
concatMap (λx. J[ x^2 | odd x ]K) J[1..5]K
○
4
concatMap (λx. J[ x^2 | odd x ]K) [1..5]
○
2
concatMap (λx. if Jodd xK then J[ x^2 | ]K else []) [1..5]
○
3 und 2 × ○
4
concatMap (λx. if odd x then [ x^2 ] else []) [1..5]
Frage Bisher haben wir Pattern Matching in unserem
Übersetzungsschema J·K nicht berücksichtigt. Wo müssen wir
nachbessern?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
251
8 · Listenverarbeitung
List Comprehensions · 8.5
Pattern Matching in List Comprehensions
Beispiel Was tut die Funktion heads? Was ist der Typ?
1
heads xs = [ y | (y:_) <- xs ]
Übersetzen heads mit dem Übersetzungsschema J·K:
=
=
=
Jλxs. [ y | (y:_) <- xs ]K
○
4
λxs. J[ y | (y:_) <- xs ]K
○
1 , behandeln Pattern wie Variable
λxs. concatMap λ(y:_). J[ y | ]K
○
3,○
1
JxsK
λxs. concatMap λ(y:_). [y] xs
]
η concatMap λ(y:_). [y]
Frage Gilt heads ≡ concatMap λ(y:_). [y]
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
?
252
8 · Listenverarbeitung
List Comprehensions · 8.5
Antwort Nein: Wenn der Pattern Match gegen (y:_) fehlschlägt, wird das
Programm abgebrochen!
1
2
3
4
> heads [[1],[2,3],[4,5,6]]
[1,2,4]
> concatMap (\(y:_)-> [y]) [[1],[2,3],[4,5,6]]
[1,2,4]
5
6
7
8
9
> heads [[1],[],[4,5,6]]
[1,4]
> concatMap (\(y:_)-> [y]) [[1],[],[4,5,6]]
[1*** Exception: <interactive>:23:12-23: Non-exhaustive patterns in lambda
Frage An welcher Stelle können wir das fixen?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
253
8 · Listenverarbeitung
List Comprehensions · 8.5
Definition Übersetzungsschema J·K mit Pattern Matching
Sei e ein Ausdruck, p ein Pattern, v eine neue Variable, b ein Boolescher
Ausdruck, und qs wieder eine Sequenz von Qualifiern.
J[ e | p <- xs, qs ]K = concatMap λv . case v of
p → J[ e | qs ]K _ → []
JxsK
○
1
2
J[ e | b, qs ]K = if JbK then J[ e | qs ]K else [] ○
J[ e | ]K = [ JeK ]
JeK = Wende J·K rekursiv auf alle nicht-primitiven
○
3
○
4
Teilausdrücke von e an.
I
I
I
Variable v matcht gegen jeden Wert aus JxsK (cf. Seite 151),
case prüft dann ob v auf Pattern p matcht (cf. Seite 157),
für fehlgeschlagene Matches werden keine Ergebnisse erzeugt.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
254
8 · Listenverarbeitung
List Comprehensions · 8.5
Beispiel Nochmal: heads xs = [ y | (y:_) <- xs ]
Übersetzen heads mit dem Übersetzungsschema J·K:
=
=
Jλxs. [ y | (y:_) <- xs ]K
○
4
λxs. J[ y | (y:_) <- xs ]K
○
1 , diesmal richtig
λxs. concatMap λv . case v of
(y:_) → J[ y | ]K _ → []
JxsK
○
3,○
1
=
λxs. concatMap λv . case v of { (y:_) → [y]; _ → [] } xs
]
η concatMap λv . case v of { (y:_) → [y]; _ → [] }
Jetzt gilt:
heads ≡ concatMap λv . case v of { (y:_) → [y]; _ → [] }
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
255
8 · Listenverarbeitung
List Comprehensions · 8.5
Weiterführende Literatur
Richard Bird, and Philip Wadler.
Introduction to Functional Programming using Haskell.
Prentice Hall International, Series in Computer Science, 1998.
Jeroen, Fokker.
Functional Programming.
Department of Computer Science, Utrecht University, 1995.
http://www.staff.science.uu.nl/~fokke101/courses/fp-eng.pdf
Torsten Grust, and Marc H. Scholl.
How to Comprehend Queries Functionally.
Journal of Intelligent Information Systems, vol. 12, p. 191–218, 1999.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
256
9
Algebraische Datentypen
9 · Algebraische Datentypen
Dieses Kapitel erweitert Haskells Typsystem, das neben Basistypen
Integer, Float, Char, Bool, ...) und den Typkonstruktoren ([ · ], (,)...
und ->) auch algebraische Datentypen kennt.
I
Ganz analog zum Typkonstruktor [ · ], der die beiden
Konstruktorfunktionen (:) und [] einführte, um Werte des Typs [α]
zu konstruieren, kann der Programmierer neue Konstruktoren
definieren, um Werte eines neuen algebraischen Datentyps zu erzeugen.
I
Wie bei Listen und Tupeln möglich, können Werte dieser neuen Typen
dann mittels Pattern Matching wieder analysiert (dekonstruiert)
werden.
In der Tat ist der eingebaute Typkonstruktor [α] selbst ein algebraischer
Datentyp (s. unten).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
258
9 · Algebraische Datentypen
9.1
Deklaration eines algebraischen Datentyps · 9.1
Deklaration eines algebraischen Datentyps
Mittels einer data-Deklaration wird ein neuer algebraischer Datentyp
spezifiziert mit:
I dem NamenT des Typkonstruktors (Identifier beginnend mit Zeichen
∈ {A...Z}) und seinen Typparametern αj ,
I den Namen Ki der Konstrukturfunktionen (Identifier beginnend mit
Zeichen ∈ {A...Z}) und der Typen βik , die diese als Parameter erwarten.
Syntax einer data-Deklaration
mit n ≥ 0, m ≥ 1, ni ≥ 0, die βik sind entweder Typbezeichner oder βik = αj :
data T α1 α2 ... αn
=
|
..
.
|
K1 β11 ... β1n1
K2 β21 ... β2n2
Km βm1 ... βmnm
Dieses data-Statement deklariert einen Typkonstruktor T und m
Konstruktorfunktionen:
Ki :: βi1 → ... → βini → T α1 α2 ... αn
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
259
9 · Algebraische Datentypen
Deklaration eines algebraischen Datentyps · 9.1
Sonderfälle
1. Die Ki haben keine Argumente, also ni = 0, n = 0.
data T = K1 | K2 | · · · | Km
T ist damit ein reiner Summentyp (auch: Aufzählungstyp) wie aus
vielen Programmiersprachen bekannt (etwa in C: enum).
• Die Konstruktoren haben alle den gleichen Typ, und bilden den Wertevorrat
von T :
Ki :: T
• Der Typ Bool ist ein Aufzählungstyp:
data Bool = False | True
• Dies gilt theoretisch ebenso für die anderen Basisdatentypen in Haskells
Typsystem:
1
2
data Int = -2^63 | ... | -1 | 0 | 1 | ... | 2^63-1 — Pseudo-Code!
data Char = ’a’ | ’b’ | ... | ’A’ | ... | ’1’ | ...
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
260
9 · Algebraische Datentypen
Deklaration eines algebraischen Datentyps · 9.1
2. Es gibt nur eine Konstruktorfunktion, also m = 1, β1i = αi .
data T α1 ... αn = K1 α1 ... αn
T verhält sich damit ähnlich wie der Tupelkonstruktor und wird auch
Produkttyp genannt. In der Typtheorie oft als β11 × β12 × · · · × β1n1
notiert.
• Die (fest eingebauten) Definitionen von Tupeln entsprechen also:
data (α,β)
= (,) α β
data (α,β,γ) = (,,) α β γ
Allgemein führt die data-Deklaration also Alternativen (Summe) von
Produkttypen ein, bezeichnet als sum-of-product types.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
261
9 · Algebraische Datentypen
Deklaration eines algebraischen Datentyps · 9.1
Arbeiten mit Aufzählungstypen
Beispiel Der benutzerdefinierte Aufzählungstyp
data Weekday = Mon | Tue | Wed | Thu | Fri | Sat | Sun
definiert
I
den Typkonstruktor Weekday und
I
die Konstruktorfunktionen Mon, ..., Sun mit z.B. Mon :: Weekday.
Funktionen über algebraischen Datentypen werden mittels Pattern
Matching realisiert:
1
2
3
4
weekend
weekend
weekend
weekend
:: Weekday -> Bool
Sat = True
Sun = True
_
= False
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
262
9 · Algebraische Datentypen
Deklaration eines algebraischen Datentyps · 9.1
Ausgabe von Aufzählungstypen
Bei der Arbeit mit diesen neuen Typen reagiert Haskell merkwürdig:
1
2
3
4
5
6
7
8
9
*Main> Mon
<interactive>:7:1:
No instance for (Show Weekday) arising from a use of ‘print’
In a stmt of an interactive GHCi command: print it
*Main> Tue == Fri
<interactive>:8:5:
No instance for (Eq Weekday) arising from a use of ‘==’
In the expression: Tue == Fri
In an equation for ‘it’: it = Tue == Fri
1. Das Haskell-System hat keine Methode show für die Ausgabe von
Werten des Typs Weekday mitgeteilt bekommen.
• Intuition: Name des Konstruktors Ki benutzen.
2. Gleichheit auf den Elementen des Typs ist nicht definiert.
• Intuition: nur Werte die durch denselben Konstruktor Ki mit identischen
Parametern erzeugt wurden, sind gleich.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
263
9 · Algebraische Datentypen
Deklaration eines algebraischen Datentyps · 9.1
Haskell kann diese Intuitionen automatisch zur Verfügung stellen,
wenn die data-Deklaration durch den Zusatz
I
deriving (Show, Eq)
erweitert wird.
1
2
data Weekday = Mon | Tue | Wed | Thu | Fri | Sat | Sun
deriving (Show, Eq)
I
Der neue Typ T wird damit automatisch Instanz der Typklasse Show aller
druckbaren Typen und Instanz der Typklasse Eq aller Typen mit
Gleichheit (==).
I
Der deriving-Mechanismus ist genereller und wird später noch genauer
besprochen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
264
9 · Algebraische Datentypen
Deklaration eines algebraischen Datentyps · 9.1
Maybe an Integer?
Algebraische Datentypen erlauben die Erweiterung eines Typs um einen
speziellen Wert, der eingesetzt werden kann, wenn Berechnungen kein
sinnvolles oder ein unbekanntes Ergebnis besitzen.
Beispiel Erweitere den Typ Integer um einen “Fehlerwert” None:
1
2
3
data MaybeInt = Val Integer
| None
deriving (Show, Eq)
4
5
6
7
safediv
:: Integer -> Integer -> MaybeInt
safediv _ 0 = None
safediv x y = Val (x ‘div‘ y)
Fragen
I
Was sind in diesem Beispiel Konstruktorfunktionen?
I
Wie lautet jeweils ihr Typ?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
265
9 · Algebraische Datentypen
Deklaration eines algebraischen Datentyps · 9.1
Maybe α
I
Der vordefinierte Typkonstruktor Maybe kann jeden Typ um das Element
Nothing erweitern.
data Maybe α = Just α
| Nothing
deriving (Eq, Show)
I
Der Typkonstruktor ist polymorph (wie etwa auch [α]):
Beispiel Erweitere den Typ Integer um einen “Fehlerwert” Nothing:
1
2
3
safediv
:: Integer -> Integer -> Maybe Integer
safediv _ 0 = Nothing
safediv x y = Just (x ‘div‘ y)
Fragen
I
Welchen Typ konstruiert der Typkonstruktor? Woraus?
I
Was sind die Typen der Konstruktorfunktionen?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
266
9 · Algebraische Datentypen
Deklaration eines algebraischen Datentyps · 9.1
entweder-oder: Union Types
I
Ein Union Type (vgl. Cs union oder Pascals “variant records”) kann
als algebraischer Datentyp dargestellt werden.
Beispiel In der Prelude ist bereits vordefiniert:
data Either α β = Left α
| Right β
deriving (Eq, Show)
I
Die Konstruktoren Left und Right betten Werte der Typen α und β in
einen gemeinsamen Typ Either α β ein, z.B.:
[Left ’x’, Right True, Right False, Left ’y’]
Frage Was sind die Typen der Konstruktorfunktionen? Und was ist der Typ
der Liste oben?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
267
9 · Algebraische Datentypen
data Either α β = Left α
| Right β
deriving (Eq, Show)
nochmal:
I
Deklaration eines algebraischen Datentyps · 9.1
Beim Pattern-Matching liefern die Konstruktoren Information darüber,
von welchem Typ der enthaltene Wert ist.
• Genauer: Wie der Wert konstruiert wurde.
Beispiel Was tut die Funktion getLeft?
1
2
3
4
getLeft
getLeft
getLeft
getLeft
:: [Either a b]
[]
=
(Left a : xs) =
(_:xs)
=
-> [a]
[]
a : getLeft xs
getLeft xs
— Bessere Implementierung in der Übung
Frage Was liefert getLeft [Left True, Right "foo", Left "bar"]?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
268
9 · Algebraische Datentypen
9.2
Rekursive algebraische Typen · 9.2
Rekursive algebraische Typen
I
Die interessantesten Konstruktionen lassen sich durch rekursive
Typ-Deklarationen erzielen.
I
Damit lassen sich vor allem diverse Arten von Bäumen als neue Typen
ausdrücken.
Beispiel BinTree α — binäre Bäume über beliebigem Typ α.
data BinTree α = Empty
| Node (BinTree α) α (BinTree α)
deriving (Eq, Show)
I
Der Konstruktor Empty steht für den leeren Baum,
I
Node repräsentiert einen Knoten mit linkem Unterbaum,
Knotenbeschriftung des Typs α und rechtem Unterbaum.
Frage
Was sind die Typen der Konstruktorfunktionen?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
269
9 · Algebraische Datentypen
Die Konstruktion eines Binärbaums mit Integer-Knotenlabels ist dann
einfach:
I
1
2
3
4
5
6
7
8
9
I
Rekursive algebraische Typen · 9.2
atree' :: BinTree Integer
atree' = Node
(Node Empty 1 (Node Empty 0 Empty))
2
( Node
(Node (Node Empty 3 Empty) 4 Empty)
6
(Node Empty 7 Empty)
)
atree' repräsentiert den folgenden binären Baum (ε bezeichnet leere
Unterbäume):
2
1
6
ε
0
ε
4
ε
Stefan Klinger · DBIS
ε
3
ε
7
ε
ε
ε
Informatik 2 · Sommer 2016
270
9 · Algebraische Datentypen
Rekursive algebraische Typen · 9.2
Um die Notation weiter zu vereinfachen, setzen wir eine Funktion leaf
zur Konstruktion von Blättern ein:
I
1
2
leaf :: a -> BinTree a
leaf x = Node Empty x Empty
Damit notieren wir atree' kürzer als
I
1
2
3
4
5
atree :: BinTree Integer
atree = Node
(Node Empty 1 (leaf 0))
2
(Node (Node (leaf 3) 4 Empty) 6 (leaf 7))
Wegen deriving Eq können wir die beiden Bäume sogar vergleichen:
I
1
2
*Main> atree == atree'
True
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
271
9 · Algebraische Datentypen
Rekursive algebraische Typen · 9.2
Listen als algebraischer Datentyp
I
Der eingebaute Typkonstruktor [ · ] für Listen ist, ganz ähnlich wie
BinTree, ein rekursiver algebraischer Datentyp. Seine Definition
entspricht
data [α] = []
| α : [α]
I
Entgegen der bisherigen Vereinbarungen wird hier der Konstruktor
K2 = (:) in Infix-Notation geschrieben.
• Auch für nutzerdefinierte Konstruktorfunktionen steht dieses Feature zur
Verfügung: Ein Konstruktorname der Form
∗
: !#$%&*+/<=>?@\^|~:.
wird infix verwendet (cf. Seite 133, Syntax der Infix-Operatoren).
• Die Verwendung von eckigen Klammern als Typkonstruktor ist syntaktischer
Zucker und steht für nutzerdefinierte Typen nicht zur Verfügung.
Frage Wie definiert man einen Typ für garantiert nicht-leere Listen?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
272
9 · Algebraische Datentypen
Rekursive algebraische Typen · 9.2
Rationale Zahlen
Beispiel Mittels Infix-Konstruktoren lässt sich bspw. der hier neu
definierte Typ rationaler Zahlen darstellen:
1
2
3
4
data Frac = Integer :/ Integer
deriving Show
> 2 :/ 3
2 :/ 3 :: Frac
Frage Wieso wird hier nicht auch die Gleichheit mittels
deriving (Show, Eq) abgeleitet?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
273
9 · Algebraische Datentypen
9.3
Bäume · 9.3
Bäume
Größe und Höhe eines Baumes
I
Bei der Analyse von Algorithmen auf Bäumen hängt die Laufzeit oft von
der Größe (Anzahl der Knoten) und Höhe (Länge des längsten Pfades
von der Wurzel zu einem Blatt) eines Baumes ab.
I
Wir definieren hier die entsprechenden Funktionen size und depth für
BinTree, cf. Seite 269.
1
size, depth :: BinTree a -> Integer
2
3
4
size Empty
= 0
size (Node l a r) = size l + 1 + size r
5
6
7
I
depth Empty
= 0
depth (Node l a r) = 1 + depth l ‘max‘ depth r
Beide Funktionen orientieren sich an der rekursiven Struktur des Typs
BinTree und sehen je einen Fall für jeden Konstruktor vor (vgl.
Listenverarbeitung).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
274
9 · Algebraische Datentypen
Bäume · 9.3
Beweise entlang der Struktur algebraischer Datentypen
I
Beweise über Algorithmen auf algebraischen Datentypen verlaufen ganz
analog zu Beweisen von Aussagen über Listen: mit Induktion über den
Aufbau.
Beweisschema
Für den Typ BinTree α lautet das Schema für Induktionsbeweise:
1. Induktionsverankerung: leerer Baum Empty,
2. Induktionsschritt: von ` und r zu Node ` a r
Satz
Zwischen der Größe und Tiefe eines Binärbaums t besteht der folgende
Zusammenhang:
depth t
Stefan Klinger · DBIS
≤
size t
≤
Informatik 2 · Sommer 2016
2depth t − 1
275
9 · Algebraische Datentypen
Bäume · 9.3
Beweis Wir verwenden Induktion über die Struktur von t zum Beweis der
zweiten Aussage:
size t ≤ 2depth t − 1
Induktionsverankerung Empty
size Empty
=
size.1
0
=
einfache Arithmetik
20
−1
=
depth.1
2depth Empty
Stefan Klinger · DBIS
Falls wir die Begründung einfache
Arithmetik nicht akzeptieren, können
wir arithmetische Operationen durch
Haskell-Äquivalente ersetzen
(bspw. ab durch power a b), und
deren Definition im Beweis
verwenden.
−1
Informatik 2 · Sommer 2016
276
9 · Algebraische Datentypen
Bäume · 9.3
Induktionshypothese Seien `, r :: BinTree α, fest, beliebig, mit
size ` ≤ 2depth ` − 1
size r ≤ 2depth r − 1
und
Induktionsschritt von ` und r zu Node ` a r
size (Node ` a r )
=
=
size.2
2 · 2depth
size ` + 1 + size r
≤
=
− 1) + 1 +
(2depth r
+
2depth r
` ‘max‘ depth r
=
−1
depth.2
2depth (Node ` a r )
−1
−1
Arithmetik
21+depth ` ‘max‘ depth r
− 1)
Arithmetik
2depth `
≤
=
Induktionshypothese
(2depth `
a ≤ b ⇒ 2a ≤ 2b
−1
a ≤ a ‘max‘ b
2 · (2depth
`
‘max‘ 2depth r ) − 1
Wichtig: Tatsächlich verwenden wir hier zwei Induktionshypothesen, eine
für ` und eine für r .
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
277
9 · Algebraische Datentypen
Bäume · 9.3
Linkester Knoten eines Binärbaumes
Problem Suche den Wert des Knotens “links außen”.
I
Dies kann nicht immer ein sinnvolles Ergebnis liefern: Ein leerer Baum
hat keinen linkesten Knoten.
I
Nutze daher den algebraischen Typkonstruktor Maybe.
leftmost :: Bintree α -> Maybe α
1
2
3
4
leftmost
leftmost
leftmost
leftmost
:: BinTree α -> Maybe α
Empty
= Nothing
(Node Empty a r) = Just a
(Node l a r)
= leftmost l
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
278
9 · Algebraische Datentypen
Bäume · 9.3
Nochmal:
1
2
3
4
leftmost
leftmost
leftmost
leftmost
:: BinTree α -> Maybe α
Empty
= Nothing
(Node Empty a r) = Just a
(Node l a r)
= leftmost l
Alternative Lösung leftmost’
1
2
3
4
5
I
steigt zuerst rekursiv in den Baum ab, und
I
propagiert einen evtl. linkesten Knoten (Just b) nach oben bzw.
I
gibt den aktuell linkesten Knoten zurück (Just a), wenn der linker
Unterbaum leer ist.
leftmost’ :: BinTree α -> Maybe α
leftmost’ Empty
= Nothing
leftmost’ (Node l a r) = case leftmost’ l of
Nothing -> Just a
Just b -> Just b
Frage Welche Variante ist besser?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
279
9 · Algebraische Datentypen
Bäume · 9.3
Nochmal:
1
2
3
4
leftmost
leftmost
leftmost
leftmost
:: BinTree α -> Maybe α
Empty
= Nothing
(Node Empty a r) = Just a
(Node l a r)
= leftmost l
5
6
7
8
9
10
leftmost’ :: BinTree α -> Maybe α
leftmost’ Empty
= Nothing
leftmost’ (Node l a r) = case leftmost’ l of
Nothing -> Just a
Just b -> Just b
Frage Welche Variante ist besser?
Antwort leftmost ist endrekursiv und kann deshalb in konstantem Platz
ausgewertet werden. Dagegen baut leftmost’ eine Sequenz aus
case-Ausdrücken auf und benötigt linearen Platz.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
280
9 · Algebraische Datentypen
I
Bäume · 9.3
Die folgende Variante bestimmt das Element links außen, gibt aber
gleichzeitig den Baum zurück, der bei dessen Entfernung entsteht:
3
ε
3
4
1
splitleftmost'
2
ε
ε
ε ε
1
2
3
4
5
_
2
ε
4
ε
ε
ε
splitleftmost’
:: BinTree α -> Maybe (α, BinTree α)
splitleftmost’ Empty
= Nothing
splitleftmost’ (Node l a r) = case splitleftmost’ l of
Nothing
-> Just (a, r)
Just (a’,l’) -> Just (a’, Node l’ a r)
Übung splitleftmost’ orientiert sich an dem Rekursionsschema für
leftmost’ und nicht an dem für leftmost. Die ganze Arbeit beim
rekursiven Abstieg in den Baum zu leisten ist schwieriger. Wie könnte eine
endrekursive Variante splitleftmost implementiert werden?
~
Nicht ganz einfach.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
281
9 · Algebraische Datentypen
Bäume · 9.3
Linearisierung von Bäumen
Dieser Abschnitt befasst sich mit der Überführung von Bäumen in
Listen von Knotenmarkierungen. Man unterscheidet:
Tiefendurchlauf aka. DFS, depth-first search
I Folgt der rekursiven Struktur der Bäume und ist vergleichsweise simpel
zu implementieren.
I Je nachdem, ob man die Markierung eines Knotens vor (→ Preorder),
zwischen (→Inorder) oder nach (→ Postorder) der Linearisierung
seiner Teilbäume ausgibt, erhält man verschiedene Tiefendurchläufe.
Breitendurchlauf aka. BFS, breadth-first search
I Zählt die Knoten ebenenweise von der Wurzel ausgehend auf. Die
nächste Ebene wird erst nach Aufzählung der darüber liegenden
abgearbeitet.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
282
9 · Algebraische Datentypen
Bäume · 9.3
Tiefendurchläufe
1
2
3
inorder :: BinTree a -> [a]
inorder Empty = []
inorder (Node l a r) = inorder l ++ [a] ++ inorder r
Frage Wie lauten die Gleichungen für Preorder und Postorder?
Beispiel inorder atree _ [1,0,2,3,4,6,7].
~
Die Effizienz von inorder wird durch die Laufzeit der
Listenkonkatenation ++ bestimmt, die linear im ersten Argument ist. Der
worst-case für inorder ist somit ein linksentarteter Baum.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
283
9 · Algebraische Datentypen
Bäume · 9.3
Linkstiefer Baum
I
1
2
3
Die Funktion leftist erzeugt einen linksentarteten Baum aus einer Liste
von vorgegebenen Knotenmarkierungen:
leftist :: [α] -> BinTree α
leftist []
= Empty
leftist (x:xs) = Node (leftist xs) x Empty
I
Aufgrund der Laufzeit von ++ benötigt inorder (leftist [1..n])
eine Laufzeit in O(n2 ).
Übung: Für inorder lässt sich eine Implementation finden, die linear in n
ist. Die Lösung orientiert sich an der Idee zur Beschleunigung von reverse,
cf. Seite 224.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
284
9 · Algebraische Datentypen
Bäume · 9.3
Breitendurchlauf
Wir setzen eine Hilfsfunktion traverse ein, die eine Liste ts von
Teilbäumen (einer Ebene) erhält, und deren Knoten entsprechend
aufzählt:
I
1
2
3
traverse :: [BinTree α] -> [α]
traverse [] = []
traverse ts = roots ts ++ traverse (childs ts)
Einen Breitendurchlauf erhalten wir dann einfach mittels
I
1
2
levelorder :: BinTree α -> [α]
levelorder t = traverse [t]
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
285
9 · Algebraische Datentypen
Bäume · 9.3
Es fehlen lediglich noch die Funktionen roots zur Bestimmung aller
Wurzeln bzw. childs zur Bestimmung aller Teilbäume einer Liste von
Bäumen:
I
1
2
3
4
roots
roots
roots
roots
:: [BinTree α] ->
[]
(Empty
: ts)
(Node _ a _ : ts)
[α]
= []
= roots ts
= a : roots ts
5
6
7
8
9
childs
childs
childs
childs
:: [BinTree α] ->
[]
(Empty
: ts)
(Node l _ r : ts)
[BinTree α]
= []
= childs ts
= l : r : childs ts
Mit List Comprehension lassen sich beide Funktionen elegant als Einzeiler
realisieren:
I
1
2
roots ts = [ a | Node _ a _ <- ts ]
childs ts = [ t | Node l _ r <- ts, t <- [l, r] ]
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
286
9 · Algebraische Datentypen
Bäume · 9.3
fold über Bäumen
Das allgemeine Rekursionsschema foldr über Listen (cf. Seite 207) lässt
sich auch auf anders konstruierte algebraische Datentypen übertragen.
I foldr (⊕) z xs ersetzt in der Liste xs die Listenkonstruktoren : und []
durch ⊕ bzw. durch z.
I Analog lässt sich eine Funktion tfold (tree fold) über BinTrees
definieren:
1
2
3
I
tfold' :: (β -> α -> β -> β) -> β -> BinTree α -> β
tfold' f z Empty = z
tfold' f z (Node l a r) = f (tfold' f z l) a (tfold' f z r)
Der Effekt von tfold auf atree (cf. Seite 270) ist damit etwa:
2
f2
1
6
tfold f z ε
0
ε
4
ε
7
ε
3
ε
Stefan Klinger · DBIS
f1
ε
ε
_
ε
Informatik 2 · Sommer 2016
z
f0
f4
z z f3 z
z z
f6
f7
z z
287
9 · Algebraische Datentypen
Bäume · 9.3
34
Das go-Idiom
Funktionen wie tfold’ reichen ihre Argumente f und z unverändert an
den rekursiven Aufruf weiter.
Nochmal der Code von der vorigen Folie:
I
1
2
3
tfold' :: (β -> α -> β -> β) -> β -> BinTree α -> β
tfold' f z Empty = z
tfold' f z (Node l a r) = f (tfold' f z l) a (tfold' f z r)
• Das Weiterreichen von f und z verursacht unnötigen Aufwand.
• Für den Programmierer ist dieser Umstand nur schwer zu erkennen.
I
Eleganter ist es, dies im Code explizit auszudrücken:
• Ersetzen den konstanten Teil tfold' f z durch go:
1
2
3
4
5
tfold :: (β -> α -> β -> β) -> β -> BinTree α -> β
tfold f z = go
— η-Konversion: tfold f z tree = go tree
where
go Empty
= z
go (Node l a r) = f (go l) a (go r)
• Aus Sicht der lokalen Definition von go sind f und z freie Variablen.
• Nur der veränderliche Parameter wird von go in der Rekursion
weitergegeben.
34 Don
Stewart über die Herkunft: http://stackoverflow.com/a/5844850
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
288
9 · Algebraische Datentypen
Bäume · 9.3
Anwendungen
Die Funktionen size und depth auf Bäumen können mit tfold
implementiert werden:
I
1
size, depth :: BinTree α -> Integer
2
3
4
size = tfold (\l _ r -> l + 1 + r) 0
— η-Konversion: size t = tfold (...) 0 t
depth = tfold (\l _ r -> 1 + l ‘max‘ r) 0
Die Tiefendurchläufe können ebenfalls als Instanzen von tfold verstanden
werden:
I
1
inorder, preorder, postorder :: BinTree α -> [α]
2
3
4
5
inorder
= tfold (\l a r -> l ++ [a] ++ r) []
preorder = tfold (\l a r -> [a] ++ l ++ r) []
postorder = tfold (\l a r -> l ++ r ++ [a]) []
Schließlich ist auch leftmost' mittels tfold ausdrückbar:
I
1
2
3
4
5
leftmost' :: BinTree α -> Maybe α
leftmost' = tfold f Nothing
where
f Nothing a _ = Just a
f other
_ _ = other
— other matcht (Just _)
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
289
9 · Algebraische Datentypen
9.4
Allgemein: Fold über Algebraische Datentypen · 9.4
Allgemein: Fold über Algebraische Datentypen
Beispiel Betrachten wir nochmal Either von Folie 267:
...erzeugt die Konstruktoren
Die Definition...
1
2
I
data Either α β = Left α
| Right β
1
2
Left :: α -> Either α β
Right :: β -> Either α β
Die Konstruktoren repräsentieren die verschiedenen Möglichkeiten,
ihre jeweiligen Argumente in einen gemeinsamen Typ einzubetten.
• [Left True, Right ’c’] ist eine homogene Liste!
I
Möchte man Either α β auf einen anderen Typ γ abbilden, so muss
man sich für jede dieser Möglichkeiten überlegen, wie man zu einem γ
kommt:
• Um eine Funktion vom Typ Either α β → γ zu konstruieren...
• brauchen wir eine Funktion f :: α → γ, ...
(für mit Left eingebettete Werte)
• und eine Funktion g :: β → γ.
(für mit Right eingebettete Werte)
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
290
9 · Algebraische Datentypen
Allgemein: Fold über Algebraische Datentypen · 9.4
I
Gesucht: Funktion fromEither' :: Either α β → γ .
I
Gegeben: f :: α → γ und g :: β → γ .
Natürlich könnte man das jetzt direkt ausprogrammieren:
I
1
2
f :: Char -> Bool
f = (==' ')
3
4
5
g :: Int -> Bool
g = (<3)
6
7
8
fromEither' (Left x) = f x
fromEither' (Right y) = g y
Fragen Was ist der Typ von fromEither'? Wo liegt hier eine
Einschränkung?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
291
9 · Algebraische Datentypen
Allgemein: Fold über Algebraische Datentypen · 9.4
Antwort fromEither' :: Either Char Int → Bool
Für jeden anderen Zieltyp, anderes f oder anderes g müssen wir die
Fallunterscheidung nochmal programmieren!
Das Schema von fromEither’ ist so simpel (und wird so oft gebraucht),
dass wir von f und g abstrahieren:
I
1
2
fromEither f g (Left x) = f x
fromEither f g (Right y) = g y
• Das nennen wir dann den Fold über Either α β.
Frage Was ist der Typ von fromEither ?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
292
9 · Algebraische Datentypen
Allgemein: Fold über Algebraische Datentypen · 9.4
fromEither ist unter dem Namen either in der Prelude vordefiniert:
I
1
2
3
either :: (α -> γ) -> (β -> γ) -> Either α β -> γ
either f g (Left x) = f x
either f g (Right y) = g y
4
5
6
7
8
*Main> either (*2) head
84
*Main> either (*2) head
0
$
Left 42
$
Right [0,8,15]
Ein Fold für Either konsumiert also für Left und Right jeweils eine
Funktion (f und g ) ...
I ... und wendet automatisch die richtige auf sein Argument vom Typ
Either α βan.
⇒ Wir müssen die Fallunterscheidung nicht jedes Mal ausprogrammieren!
I In der Definition von either sieht man direkt wie die Konstruktoren
durch f bzw. g ersetzt werden.
I
Frage Was ist either Left Right ?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
293
9 · Algebraische Datentypen
Allgemein: Fold über Algebraische Datentypen · 9.4
Beispiele
Der Fold über Maybe heißt maybe:
I
1
2
data Maybe α = Nothing
| Just α
3
4
5
6
maybe :: β -> (α -> β) -> Maybe α -> β
maybe d f Nothing = d
maybe d f (Just x) = f x
7
8
9
*Main> maybe 0 (*7)
*Main> maybe 0 (*7)
$
$
safediv' 18 9
safediv' 18 0
— Ergebnis? (safediv' auf Folie 266)
— ...und hier?
Der Fold über Paare heißt uncurry:
I
1
data (α,β) = (,) α β
2
3
4
uncurry :: (α -> β -> γ) -> (α, β) -> γ
uncurry f (x,y) = f x y
5
6
7
*Main> uncurry (*) (2,3)
6
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
294
9 · Algebraische Datentypen
Allgemein: Fold über Algebraische Datentypen · 9.4
Allgemein
I
Erinnerung: Die data-Deklaration (cf. Seite 259) definiert die
Konstruktorfunktionen Ki für den algebraischen Datentyp T α1 ... αn :
data T α1 ... αn = K1 β11 ... β1n1
| ...
| Km βm1 ... βmnm
I
I
I
Um T α1 ... αn auf einen Typ γ abzubilden, brauchen wir für jeden
Konstruktor Ki eine Funktion fi :: βi1 → ... → βini → γ die seine
Argumente auf den gemeinsamen Ergebnistyp γ abbildet.
Die Funktion
foldT :: (β11 → ... → β1n1 → γ)
→ ...
→ (βm1 → ... → βmnm → γ)
→ T α1 ... αn
→ γ
soll dann je nach Konstruktor Ki die passende Funktion fi auswählen.
Wir müssen foldT passend zum algebraischen Datentyp konstruieren.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
295
9 · Algebraische Datentypen
Allgemein: Fold über Algebraische Datentypen · 9.4
Frage Was ist der Fold über Bool?
1
data Bool = True | False
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
296
9 · Algebraische Datentypen
Allgemein: Fold über Algebraische Datentypen · 9.4
Fold über rekursive algebraische Datentypen
I
Für einfache35 rekursive Datentypen T α1 ... αn taucht bei
(mindestens) einer Konstruktorfunktion der konstruierte Typ als
Argument auf:
Ki :: ... → T α1 ... αn → ... → T α1 ... αn
|
{z
}
|
{z
}
Argumente des Konstruktors Ki
konstruierterTyp
• Beispiel: Listen-Konstruktor cons: (:) :: α → [α] → [α] .
I
Die entsprechende Argumentfunktion fi von foldT hat also den Typ
fi :: ... → γ → ... → γ
~
Obacht Das Argument vom Typ γ stammt aus einem rekursiven
Aufruf von foldT .
1
2
3
4
5
foldr :: (α -> β -> β) -> β -> [α] -> β
foldr (#) z = go
— η-Konversion: foldr (#) z xs = go xs
where
go []
= z
go (x:xs) = x # go xs — Hier ist foldr rekursiv!
35 Komplexer:
T taucht mit anderen Typ(variabl)en im Typ von Ki auf — behandeln wir hier nicht.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
297
9 · Algebraische Datentypen
Allgemein: Fold über Algebraische Datentypen · 9.4
Beispiel
1
2
data BinTree α = Empty
| Node (BinTree α) α (BinTree α)
3
4
5
6
7
8
tfold :: (β -> α -> β -> β) -> β -> BinTree α -> β
tfold f z = go
— η-Konversion: tfold f z tree = go tree
where
go Empty
= z
go (Node l a r) = f (go l) a (go r) — Hier steckt die Rekursion
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
298
10
Typklassen und Overloading
10 · Typklassen und Overloading
10.1
Parametrische Polymorphie · 10.1
Parametrische Polymorphie
Viele der bisher betrachteten Funktionen waren polymorph36 , d.h. der Typ
dieser Funktionen enthielt mindestens eine Typvariable (α, β, ...):
1
2
3
4
5
6
id
snd
length
take
foldr
levelorder
I
I
::
::
::
::
::
::
α -> α
(α,β) -> β
[α] -> Int
Int -> [α] -> [α]
(α -> β -> α) -> α -> [β] -> α
BinTree α -> [α]
Mit einer einzigen Funktionsdefinition wird eine Vielzahl “konkreter”
Funktionen “definiert”, je eine für jede gültige Belegung der Typvariablen.
Das geht nur, insofern die Funktionsweise unabhängig vom Typ der
Argumente definiert werden kann.
• fst :: (α, β) → α —Das Argument muss ein Paar sein, und der Typ der
ersten Komponente entspricht dem Ergebnistyp. Die konkreten Typen der
Komponenten sind jedoch irrelevant.
I
Jede Typvariable kann mit einem beliebigen Typ (konsistent)
“instanziiert” werden.
36 “Polymorphie”
Stefan Klinger · DBIS
= Vielgestaltigkeit
Informatik 2 · Sommer 2016
300
10 · Typklassen und Overloading
Parametrische Polymorphie · 10.1
Man sagt daher auch, die Typvariablen polymorpher Funktionen sind
allquantifiziert. Dann liest sich der Typ von foldr wie
foldr :: ∀α. ∀β. (α → β → α) → α → [β] → α
Funktionen dieser Art werden parametrisch polymorph genannt.
Beispiel In take 3 "foo" und take 10 [0..] wird jeweils die gleiche
Funktionsdefinition von take angewandt:
1
2
3
take :: Int -> [α] -> [α]
take n (x:xs) | n > 0 = x : take (n-1) xs
take _ _
= []
I
Für den Fall take 3 "foo" : α = Char
und damit etwa [] :: [Char] und (:) :: Char → [Char] → [Char],
I
Für den Fall take 10 [0..] : α = Int
und daher [] :: [Int] und (:) :: Int → [Int] → [Int].
take ist parametrisch polymorph, take :: ∀α. Int → [α] → [α].
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
301
10 · Typklassen und Overloading
Parametrische Polymorphie · 10.1
Beispiel Die Identitätsfunktion id = λx. x ist auf Argumente beliebiger
Typen α anwendbar.
I
In den drei Ausdrücken
id 3
id ’a’
id (3,’a’)
= 3
= ’a’
= (3,’a’)
wurde id als Funktion der Typen
• Integer → Integer,
• Char → Char und
• (Integer, Char) → (Integer, Char)
benutzt.
I
Damit lautet die vollständige Typisierung von id
id :: ∀α. α → α
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
302
10 · Typklassen und Overloading
Parametrische Polymorphie · 10.1
Vorsicht Haskell notiert die Allquantifizierung von Typvariablen nicht:
1
2
3
4
Prelude> :t id
id :: a -> a
Prelude> :t take
take :: Int -> [a] -> [a]
— Eigentlich: ∀α. α → α
— Eigentlich: ∀α. Int → [α] → [α]
Alle Typvariablen sind implizit universell quantifiziert.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
303
10 · Typklassen und Overloading
Parametrische Polymorphie · 10.1
Eine konkrete Instanz des polymorphen Typs von id (bspw. bei
Anwendung auf das Argument (3,’a’) bzw. length) erhält man durch die
konsistente Subsitution von Typvariablen durch einen (konkreten) Typ.
Beispiel Selbst innerhalb eines Ausdrucks können durchaus verschiedene
Instanziierungen desselben polymorphen Objektes auftreten. In
id length $ id [True, True, False]
tritt id :: ∀α. α → α in den Instanziierungen
α = [Bool]
α = [Bool] → Int
also id :: [Bool] → [Bool]
also id :: ([Bool] → Int) → ([Bool]→Int)
auf.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
304
10 · Typklassen und Overloading
Parametrische Polymorphie · 10.1
Frage Kann eine Funktion vom Typ ∀α. α → α ihr Argument um 1
erhöhen?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
305
10 · Typklassen und Overloading
10.2
Ad-Hoc Polymorphie (Overloading) · 10.2
37
Ad-Hoc Polymorphie (Overloading)
Eine andere Art von Polymorphie versteckt sich hinter dem Konzept des
Overloading:
I
Ein einzelnes syntaktisches Objekt (Operator, Funktionsname, ...) steht
für verschiedene – aber “semantisch ähnliche” – Definitionen.
Beispiel Typische überladene Funktionen und Operatoren.
(==), (<=)
(*), (+)
show, read
37 Ad
Gleichheit und Ordnung lassen sich für eine ganze
Klasse von Typen sinnvoll definieren.
Arithmetische Ausdrücke über ganzen Zahlen und
Fließkommazahlen werden mit identischen Operatorsymbolen notiert.
Für eine ganze Klasse von Typen lassen sich sinnvolle
externe Repräsentationen (als String) angeben.
hoc, lat. zu diesem, hierfür, in der Bedeutung von zur Sache passend.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
306
10 · Typklassen und Overloading
Ad-Hoc Polymorphie (Overloading) · 10.2
Beobachtungen:
I
Ohne Overloading wären Funktionsnamen wie equalInt und
equalBool, oder showDouble und showBinTree notwendig.
I
Für den Typ BinTree α muss show offensichtlich vollkommen anders
implementiert sein, als für Double.
Offensichtliche Frage
Welchen Typ besitzt (==)? Gilt hier wirklich
(==) :: ∀α. α → α → Bool
?
Warum wird Overloading auch “Ad-Hoc Polymorphie” genannt?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
307
10 · Typklassen und Overloading
10.3
Typklassen · 10.3
Typklassen
I
Eine Typklasse (type class) deklariert eine Familie von Funktionen, die
für verschiedene konkrete Typen (sog. Instanzen der Klasse) jeweils
verschieden implementiert werden können.
I
Die Klasse legt nur den polymorphen Typ der Funktionen fest. Die
Implementation erfolgt dann bei der Instanziierung (oder cf. Seite 316).
Syntax & Semantik
der Klassendefinition:
class C α where
f1 :: τ1
..
.
fn :: τn
(Vorläufig, cf. Seite 319)
I
Ein Typ α kann zu einer Instanz
der Klasse C erklärt werden...
I
...indem die Funktionen fi :: τi
implementiert werden.
I
Hier wird die Klasse C definiert, die Funktionen fi werden deklariert!
I
Die Typen τi müssen die Typvariable α enthalten.
I
Der Klassenname C muss mit einem Zeichen ∈ [A..Z] beginnen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
308
10 · Typklassen und Overloading
Typklassen · 10.3
Beispiel Typen, für deren Werte Tests auf Gleichheit sinnvoll sind,
können die Operatoren der Klasse Eq (equality) überladen:
1
2
3
class Eq α where
(==) :: α -> α -> Bool
(/=) :: α -> α -> Bool
I
Hier wird die Klasse Eq definiert.
I
Die (noch nicht definierten) Funktionen == und /= müssen für einen
konkreten Typen T implementiert werden.
I
Diese Implementierungen für ein konkretes T haben dann entsprechend
den Typ T → T → Bool.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
309
10 · Typklassen und Overloading
Typklassen · 10.3
Beispiel Typen, deren Werte aufzählbar sind, können die Funktionen der
Klasse Enum überladen ...
1
2
3
4
5
6
7
8
9
class Enum α where
succ
pred
toEnum
fromEnum
enumFrom
enumFromThen
enumFromTo
enumFromThenTo
::
::
::
::
::
::
::
::
α -> α
α -> α
Int -> α
α -> Int
α -> [α]
α -> α -> [α]
α -> α -> [α]
α -> α -> α -> [α]
...und dann von synaktischem Zucker wie
I
[x..]
I
[x..y ]
≡ enumFrom x oder
≡ enumFromTo x y
Gebrauch machen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
310
10 · Typklassen und Overloading
Typklassen · 10.3
Type Constraints
I
Jetzt ist klarer, welchen Typ z.B. (==) wirklich besitzt:
Für alle Typen α, welche sämtliche Operationen der Typklasse Eq
implementieren, gilt (==) :: α → α → Bool .
I
Eine Typklasse C kann also als Prädikat auf Typen verstanden werden.
• Das Prädikat ist erfüllt, wenn alle Funktionen der Klasse für diesen Typ
implementiert sind.
• Das drückt die zugehörige Haskell-Syntax mit einem Type Constraint
(aka. Type Context) klar aus:
fi :: C α ⇒ τi
Hier also:
• (==) :: Eq α ⇒ α -> α -> Bool
Obacht Die Typvariable α (also der Parameter von C ), ist in den Typen
τi der Klassenfunktionen nicht allquantifiziert!
I Es tauch tatsächlich kein ∀α im polymorphen Typ von fi auf.
I Typklassen beschränken (kontrollieren) also die Polymorphie.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
311
10 · Typklassen und Overloading
Typklassen · 10.3
Instanziierung
I
I
Die Zugehörigkeit eines Typs α zu einer Typklasse C wird Haskell
mittels einer instance-Erklärung angezeigt.
Danach gilt der Type Constraint C α ⇒ ... als erfüllt.
Syntax
der Instanziierung eines Typs zur Klasse C : (vorläufig, cf. Seite 319)
instance C α where
f1 = Definition von f1
..
.
fn = Definition von fn
I
I
Hier werden die Funktionen fi definiert.
Die Typen der fi dürfen nicht nochmal angegeben werden. Sie wurden
bereits bei der Definition der Klasse C deklariert.
Beispiel Werte des Typs Bool erlauben Test auf Gleichheit.
1
2
3
instance Eq Bool where
x == y = (x && y) || (not x && not y)
x /= y = not (x == y)
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
312
10 · Typklassen und Overloading
Typklassen · 10.3
Beispiel Vergleiche auf Basistypen werden aus Effizienzgründen auf den
entsprechenden primitiven Gleichheitstest der unterliegenden Maschine
zurückgeführt.
Denkbar wären z.B.:
1
2
3
instance Eq Char where
x == y = primEqChar x y
x /= y = not (x == y)
4
5
6
7
8
9
10
instance Eq Integer where
x == y = primEqInteger x y
x /= y = not (x == y)
11
instance Eq Int where
x == y = primEqInt x y
x /= y = not (x == y)
12
13
14
instance Eq Float where
x == y = primEqFloat x y
x /= y = not (x == y)
I
Die genannten Funktionen (primEq...) sind dann ein
Implementationsdetail der Standardbibliothek.
I
Der Programmierer muss sich nicht darum kümmern, und hat auch meist
keinen Zugriff darauf.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
313
10 · Typklassen und Overloading
Typklassen · 10.3
Type Constraints in Intanziierungen
I
Gleichheit von Listen des Typs [α] ist offensichtlich nur dann
wohldefiniert, wenn auch α Gleichheitstests zulässt.
I
Dies kann durch einen Type Constraint auf dem Klassenparameter α
ausgedrückt werden:
1
instance Eq α => Eq [α] where
~
2
3
4
5
[]
== []
= True
(x:xs) == (y:ys) = x == y
_
== _
= False
&&
xs == ys
6
7
I
xs /= ys
= not (xs == ys)
Die (==)-Operatoren
entstammen verschiedenen
Instanziierungen.
(was ist damit gemeint?)
Damit sind jetzt automatisch alle Listen über vergleichbaren Typen
vergleichbar (des rechtfertigt die Syntax =>).
Frage Wie könnten Paare des Typs (α,β) für den Gleichheitstest
zugelassen werden?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
314
10 · Typklassen und Overloading
Typklassen · 10.3
Antwort Die Instanziierung von Eq (α, β) kommt aus der Prelude, und
könnte etwa so aussehen:
1
instance (Eq α, Eq β) => Eq (α, β) where
2
(x, y) == (v, w) = x == v && y == w
3
4
p
5
I
/= q
= not (p == q)
Vergleiche die letzte Zeile mit der Instanziierung von Eq [α].
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
315
10 · Typklassen und Overloading
Typklassen · 10.3
class Defaults
I
I
Oft sind default-Definitionen für die Funktionen einer Typklasse sinnvoll
(s. Eq und (/=)).
Eine default-Definition wird innerhalb des class-Konstruktes
vorgenommen. Jede Instanz dieser Typklasse “erbt” diese Definition,
wenn sie nicht explizit überschrieben wird.
Beispiel Allgemein besteht
eine offensichtliche Beziehung
zwischen Gleichheit und
Ungleichheit.
1
2
3
4
5
6
7
I
I
class Eq a where
— Minimal complete definition: (==) | (/=)
(==) :: a -> a -> Bool
x == y = not (x /= y)
(/=) :: a -> a -> Bool
x /= y = not (x == y)
Bei der Instanziierung muss jetzt nur noch (mindestens!) eine der
Definitionen überschrieben werden.
Defaults erlauben es konsistentes Verhalten der Funktionen einer Klasse
nahezulegen (nicht: erzwingen).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
316
10 · Typklassen und Overloading
Typklassen · 10.3
Beispiel Typen mit Ordnung können die Funktionen der Klasse Ord
überladen:
(data Ordering = EQ | LT | GT)
1
2
3
4
class Ord α where
compare
:: α -> α -> Ordering
(<), (<=), (>=), (>) :: α -> α -> Bool
max, min
:: α -> α -> α
5
6
7
8
9
— Minimal complete definition: (<=) | compare
compare x y | x == y
= EQ
| x <= y
= LT
| otherwise = GT
10
11
12
13
14
x
x
x
x
<=
<
>=
>
y
y
y
y
=
=
=
=
compare
compare
compare
compare
x
x
x
x
y
y
y
y
/=
==
/=
==
GT
LT
LT
GT
=
=
=
=
x
y
x
y
15
16
17
18
19
max x y |
|
min x y |
|
x >= y
otherwise
x <= y
otherwise
Frage Steht hier die “ganze Wahrheit”? Oder haben wir etwas vergessen?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
317
10 · Typklassen und Overloading
Typklassen · 10.3
Antwort compare verwendet ==, benötigt also einen Eq α Kontext:
1
2
3
4
class Eq α => Ord α where
compare
:: α -> α -> Ordering
(<), (<=), (>=), (>) :: α -> α -> Bool
max, min
:: α -> α -> α
5
— Minimal complete definition: (<=) or compare
compare x y | x == y
= EQ
| x <= y
= LT
| otherwise = GT
6
7
8
9
— Hierfür brauchen wir Eq α, ...
10
x
x
x
x
11
12
13
14
<=
<
>=
>
y
y
y
y
=
=
=
=
compare
compare
compare
compare
x
x
x
x
y
y
y
y
/=
==
/=
==
GT
LT
LT
GT
=
=
=
=
x
y
x
y
— ... hierfür nicht. Warum?
15
max x y |
|
min x y |
|
16
17
18
19
I
I
x >= y
otherwise
x <= y
otherwise
Auch Klassendefinitionen können einen Type Constraint aufweisen
Man darf einen Typ nur dann zu Ord instanziieren, wenn er auch
vergleichbar ist!
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
318
10 · Typklassen und Overloading
Typklassen · 10.3
38
Zusammenfassung der Syntax
Ein Type Constraint kann mehrere Klassen oder Typvariablen beinhalten:
(==)
:: Eq α ⇒ α → α → Bool
λx. x + 1 > 3
:: (Num α, Ord α) ⇒ α → Bool
λx y z. (x + 1, y > z) :: (Num α, Ord β) ⇒ α → β → β → (α, Bool)
I
I
Formal hat ein Type Constraint also die Form
? TypeConstraint
→
|
OneConstraint =>
( OneConstraint (, OneConstraint)∗ ) =>
OneConstraint
→
ClassName TypeVariable
Klassendefinition und Instanziierung können Type Constraints
enthalten:
class TypeConstraint? C α where
Deklarationen und Default-Definitionen
instance TypeConstraint? C α where
Definitionen
38 Genauer:
https://www.haskell.org/onlinereport/haskell2010/haskellch4.html#x10-750004.3
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
319
10 · Typklassen und Overloading
10.4
Instanziierung algebraischer Datentypen · 10.4
Instanziierung algebraischer Datentypen
Beispiel Erinnerung: Typ Weekday.
1
data Weekday = Mon | Tue | Wed | Thu | Fri | Sat | Sun
I
I
1
2
3
4
5
6
7
8
9
10
Wie macht man Weekday zu einem aufzählbaren Datentyp (also zu einer
Instanz von Enum)?
Die Prelude definiert39 Defaults für alle Funktionen in Enum bis auf
toEnum und fromEnum:
class Enum a where
toEnum
:: Int -> a
fromEnum
:: a -> Int
.
.
.
succ :: a -> a
succ = toEnum . (+1) . fromEnum
.
.
.
enumFromTo :: a -> a -> [a]
enumFromTo x y = map toEnum [ fromEnum x .. fromEnum y ]
.
.
.
39 http://hackage.haskell.org/package/base-4.8.0.0/docs/Prelude.html#t:Enum
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
320
10 · Typklassen und Overloading
Instanziierung algebraischer Datentypen · 10.4
Verbleibt also die Definition von
toEnum und fromEnum mit der
offensichtlich gewünschten
Eigenschaft fromEnum ◦ toEnum ≡ id.
1
2
3
4
5
6
Hinweis fromEnum und toEnum
etablieren einen Isomorphismus von
Weekday und [0..6].
7
8
9
10
11
12
Jetzt geht zum Beispiel:
1
2
13
*Main> [Wed .. Sat] — Leerzeichen wichtig!
[Wed,Thu,Fri,Sat]
14
15
instance Enum Weekday where
toEnum 0 = Mon
toEnum 1 = Tue
toEnum 2 = Wed
toEnum 3 = Thu
toEnum 4 = Fri
toEnum 5 = Sat
toEnum 6 = Sun
fromEnum Mon = 0
fromEnum Tue = 1
fromEnum Wed = 2
fromEnum Thu = 3
fromEnum Fri = 4
fromEnum Sat = 5
fromEnum Sun = 6
Problem Die Definition derartiger Datentypen als Instanz von Enum (aber
auch Eq, Ord, Show, Read40 ) ist kanonisch, aufwändig, und fehleranfällig.
40 Einige
dieser Typklassen tauchen später noch auf.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
321
10 · Typklassen und Overloading
Instanziierung algebraischer Datentypen · 10.4
Beispiel Wir machen BinTree zur Instanz von Eq, und definieren dazu
den Gleichheitstest auf Werten von BinTree:
1
2
3
4
instance Eq a => Eq (BinTree a) where
Empty
== Empty
= True
Node l1 x1 r1 == Node l2 x2 r2 = x1 == x2 && l1 == l2 && r1 == r2
_
== _
= False
Beobachtungen (reguläres Muster):
I
Die Struktur der Definition von (==) folgt der rekursiven Struktur des
Datentyps BinTree.
I
Nur jeweils durch gleiche Konstruktoren erzeugte Werte können
potentiell gleich sein.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
322
10 · Typklassen und Overloading
Instanziierung algebraischer Datentypen · 10.4
deriving
I
Das soeben beobachtete reguläre Muster der Typklassen-Operationen
ermöglicht es dem Haskell-Compiler, instance-Deklarationen
automatisch abzuleiten.
Syntax Die deriving-Klausel...
data T α1 α2 ... αn = ...
| ...
..
.
deriving (C1 ,...,Cm )
...macht den Typkonstruktor T zur Instanz der Typklassen C1 , ...Cm .
I
Diese Magie ist leider nicht für alle Typklassen Ci verfügbar.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
323
10 · Typklassen und Overloading
Instanziierung algebraischer Datentypen · 10.4
Die Definitionen der Typklassen-Operationen können automatisch erzeugt
werden, für folgende in der Prelude definierten Klassen:
Eq Ableitbar für alle Datentypen mit vergleichbaren Komponenten.
I Gleichheit wird über die Identität von Konstruktoren und
(rekursiv) der Gleichheit der Komponenten des Datentyps
entschieden.
Ord Ableitbar für alle Datentypen mit anordenbaren Komponenten.
I Reihenfolge der Konstruktoren in data-Deklaration
entscheidend, Ordnung wird lexikographisch (rekursiv) definiert.
Enum Datentyp muss ein reiner Summentyp sein (s. vorn), also
data T = K0 | ... | Kn−1
mit fromEnum Ki = i.
Show Textuelle Repräsentation der Konstruktoren, s. später.
... Einige andere die wir hier nicht besprechen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
324
10 · Typklassen und Overloading
Instanziierung algebraischer Datentypen · 10.4
Beispiel BinTree (nochmal)
1
2
3
data BinTree a = Empty
| Node (BinTree a) a (BinTree a)
deriving (Eq, Ord)
führt automatisch zu:
1
2
3
4
instance Eq a => Eq (BinTree a) where
Empty
== Empty
= True
Node l1 x1 r1 == Node l2 x2 r2 = x1 == x2 && l1 == l2 && r1 == r2
_
== _
= False
5
6
7
8
9
10
11
12
instance Ord a => Ord (BinTree a) where
Empty
<= Empty
= True
Empty
<= Node _ _ _ = True
Node _ _ _ <= Empty
= False
Node l1 x1 r1 <= Node l2 x2 r2 = l1 < l2
|| l1 == l2 && x1 < x2
|| x1 == x2 && r1 <= r2
Hinweis zu den == Operatoren: Der Type Constraint Ord α impliziert
bereits Eq α. Das kommt aus der Klassendefinition von Ord, cf. Seite 318
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
325
10 · Typklassen und Overloading
Instanziierung algebraischer Datentypen · 10.4
Beispiel Nicht immer ist deriving die Antwort!
Erinnerung – Frac repräsentiert rationale Zahlen.
1
2
data Frac = Integer :/ Integer
deriving Eq
Hier führt deriving nicht zum Ziel ...
1
2
> 2 :/ 5 == 4 :/ 10
False
Gemeint ist vielmehr:
1
2
instance Eq Frac where
(x1 :/ y1) == (x2 :/ y2)
=
x1 * y2 == x2 * y1
3
4
5
> 2 :/ 5 == 4 :/ 10
True
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
326
10 · Typklassen und Overloading
Instanziierung algebraischer Datentypen · 10.4
Beispiel Weekday (nochmal)
1
2
data Weekday = Mon | Tue | Wed | Thu | Fri | Sat | Sun
deriving (Eq, Ord, Enum, Show)
Damit haben wir automagisch:
1
2
3
4
5
6
7
8
> Mon < Tue
True
> Mon == Sat
False
> [Mon,Wed .. Sun]
[Mon,Wed,Fri,Sun]
> fromEnum Sat
5
~
Frage Was ergibt der Aufruf map toEnum [2,3]?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
327
10 · Typklassen und Overloading
Anmerkung: Mehrdeutige Typen
10.5
I
Anmerkung: Mehrdeutige Typen · 10.5
Die Funktion show :: Show α ⇒ α → String wird vom GHCi
verwendet, um das Ergebnis einer Berechnung auszugeben (cf. Seite 334).
1
2
3
4
5
6
*Main> ['1', '2', '3']
"123"
*Main> [1,2,3]
[1,2,3]
*Main> 123
123
1
2
3
4
5
6
*Main> show ['1', '2', '3']
"\"123\""
*Main> show [1,2,3]
"[1,2,3]"
*Main> show 123
"123"
I
Offensichtlich muss show für verschiedene Typen unterschiedlich
implementiert sein.
⇒ Je nach Typ wird die passende Implementation von show ausgewählt.
I
Welche Implementation soll für toEnum 2 ausgewählt werden?
1
2
*Main> :t toEnum 2
toEnum 2 :: Enum a => a
Stefan Klinger · DBIS
Vollkommen unklar! α kann mit jedem
beliebigen Typen aus der Klasse Enum
instanziiert werden.
Informatik 2 · Sommer 2016
328
10 · Typklassen und Overloading
Anmerkung: Mehrdeutige Typen · 10.5
Fehlermeldung
1
2
3
4
5
6
7
8
9
10
bis GHCi 7.6.3, sonst cf. Seite 332
Prelude> toEnum 2
<interactive>:2:1:
No instance for (Enum a0) arising from a use of ‘toEnum’
The type variable ‘a0’ is ambiguous
Possible fix: add a type signature that fixes these type variable(s)
Note: there are several potential instances:
instance Enum Weekday -- Defined at <interactive>:3:35
instance Enum Double -- Defined in ‘GHC.Float’
instance Enum Float -- Defined in ‘GHC.Float’
...plus 8 others
I
I
Der GHCi sagt uns dass er nicht weiss
welches Enum wir meinen.
Wir können aber für einen Ausdruck
explizit einen Typ angeben:
1
2
3
4
5
6
7
1
2
3
4
*Main> :t 4
4 :: Num a => a
*Main> 4
4
Stefan Klinger · DBIS
8
*Main>
GT
*Main>
'\STX'
*Main>
Wed
*Main>
2
toEnum 2 :: Ordering
toEnum 2 :: Char
— cf. ascii(7)
toEnum 2 :: Weekday
toEnum 2 :: Int
Frage Was passiert hier? Warum kein Fehler?
Informatik 2 · Sommer 2016
329
10 · Typklassen und Overloading
Anmerkung: Mehrdeutige Typen · 10.5
Type Defaulting
Antwort Offensichtlich sucht sich der GHCi einen “passenden Typ” aus.
I
Auf Dauer wäre es anstrengend, auch bei einfachen Fällen (z.B. Zahlen)
immer einen Typ mit angeben zu müssen.
1
I
I
Dieses Vorgehen heisst Type
Defaulting und wird auch für
andere Fälle verwendet:
2
3
4
5
6
*Main> []
— [ ] :: ∀α. [α]
[]
*Main> [] :: String
""
*Main> 2.3
— 2.3 :: Fractional α ⇒ α
2.3
Die Defaulting-Regeln sind im Haskell-Standard41 und im GHCi
Manual42 beschrieben.
41 https://www.haskell.org/onlinereport/haskell2010/haskellch4.html#x10-790004.3.4
42 https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/interactive-evaluation.html#extended-default-rules
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
330
10 · Typklassen und Overloading
I
1
2
3
4
5
6
7
Anmerkung: Mehrdeutige Typen · 10.5
Man kann den ghci mit der Option -Wall starten43 , dann wird ein
Defaulting-Vorgang angezeigt.
Prelude> 5
<interactive>:3:1: Warning:
Defaulting the following constraint(s) to type ‘Integer’
(Show a0) arising from a use of ‘print’ at <interactive>:3:1
(Num a0) arising from a use of ‘it’ at <interactive>:3:1
In a stmt of an interactive GHCi command: print it
5
8
9
10
11
12
13
14
Prelude> []
<interactive>:4:1: Warning:
Defaulting the following constraint(s) to type ‘()’
(Show t0) arising from a use of ‘print’
In a stmt of an interactive GHCi command: print it
[]
Der Unit-Type () enthält genau einen Wert, () :: (), ebenfalls Unit
genannt. Er wird oft verwendet wenn man einen Typ braucht der weiter
kaum Eigenschaften hat.
43 -W
legt fest welche Warnungen gezeigt werden sollen. Hier: all = Alle.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
331
10 · Typklassen und Overloading
Anmerkung: Mehrdeutige Typen · 10.5
Erweiterte Defaulting-Regeln
GHCi ab Version 7.8
Die Defaulting-Regeln wurden im Laufe der Zeit erweitert44 .
Aktuelle Versionen des GHCi verwenden oft () als Default.
I
1
2
3
GHCi, version 7.8.4: http://www.haskell.org/ghc/
Prelude> :set -Wall
Prelude> toEnum 3
:? for help
4
5
6
7
8
I
I
<interactive>:3:1: Warning:
Defaulting the following constraint(s) to type ‘()’
...
*** Exception: Prelude.Enum.().toEnum: bad argument
Hier wird also toEnum 3 :: Enum α ⇒ zu toEnum 3 :: () konkretisiert.
Die Fehlermeldung wird dann von der konkreten Funktion toEnum 3 :: ()
ausgegeben:
• () ist zwar Instanz der Enum-Klasse,
• enthält aber nur einen Wert! (Ausprobieren: toEnum 0)
44 https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/interactive-evaluation.html#extended-default-rules
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
332
10 · Typklassen und Overloading
Anmerkung: Mehrdeutige Typen · 10.5
~
Obacht
I
Defaulting ist eine Krücke die die Arbeit mit dem interaktiven
Interpreter vereinfachen soll.
I
Dadurch wird die Sprache Haskell nicht klarer, oder einfacher,
vielmehr fallen magische Effekte vom Himmel.
I
Verlassen Sie sich beim Programmieren nicht auf Defaulting,
sondern geben Sie die Typen Ihrer Funktionen an!
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
333
10 · Typklassen und Overloading
10.6
Die Typklasse Show · 10.6
Die Typklasse Show
Die Überführung ihrer Werte in eine externe Repräsentation (vom Typ
String) ist eine der Kernaufgaben jeder Programmiersprache, z.B. für
I
• Ein-/Ausgabe von Werten, interaktive Programme,
• Speichern/Laden/Übertragung von Daten.
In Haskell wird die benötige Funktionalität von Instanzen der
Typklasse Show bereitgestellt45 :
I
1
2
3
4
class Show α where
show :: α -> String
— Minimal complete definition: show
...
45 Typklasse
Read leistet die Umkehrabbildung, das sog. parsing.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
334
10 · Typklassen und Overloading
Die Typklasse Show · 10.6
Beispiel Ausgabe von Werten des Typs Frac als Brüche der Form
1
2
3
4
5
6
7
8
9
10
x
.
y
showFrac :: Frac -> String
showFrac (x :/ y) = replicate (div (l-lx) 2) ' ' ++ sx ++ "\n" ++
replicate l '-'
++ "\n" ++
replicate (div (l-ly) 2) ' ' ++ sy
where
sx = show x
sy = show y
lx = length sx
ly = length sy
l = max lx ly
11
12
13
instance Show Frac where
show = showFrac
Haskell-Interpreter GHCi benutzt show :: Show α ⇒ α → String, um
in interaktiven Sessions Werte darzustellen:
1
2
3
4
*Main> 421 :/ 6546516
421
------6546516
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
335
10 · Typklassen und Overloading
Die Typklasse Show · 10.6
Beispiel “Visualisierung” von Werten des Typs BinTree α .
1
2
3
4
5
6
7
8
9
10
11
showTree :: Show a => BinTree a -> String
showTree = concat . ppTree
where
ppTree :: Show a => BinTree a -> [String]
ppTree Empty
= ["@\n"]
ppTree (Node l x r) = [show x ++ "\n"]
++ ("|--" ++ ls) : map ("|
++ ("‘--" ++ rs) : map ("
where
ls:lss = ppTree l
rs:rss = ppTree r
" ++) lss
" ++) rss
12
13
14
1
2
3
4
5
6
7
8
9
10
instance Show a => Show (BinTree a) where
show = showTree
*Main> Node (leaf 1) 2 (Node (leaf 3) 4 Empty)
2
|--1
| |--@
| ‘--@
‘--4
|--3
| |--@
| ‘--@
‘--@
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
336
10 · Typklassen und Overloading
10.7
Ober-/Unterklassen · 10.7
Ober-/Unterklassen
In größeren Software-Systemen sind hierarchische Abhängigkeiten
zwischen Typklassen üblich (vgl. Vererbung in OO-Sprachen):
Typ T ist Instanz der Typklasse C , was aber nur Sinn macht, wenn T
auch schon Instanz der Typklassen C1 , ..., Cn ist.
Beispiel In Haskell stützt sich Anordnung eines Typs (Ord) auf die
Vergleichbarkeit seiner Werte (Eq), cf. Seite 318.
I
I
I
Jeder Typ in Typklasse Ord muss auch Instanz von Eq sein.
Ist ein Typ in Ord, sind auf seine Werte die Operationen aus Ord und Eq
anwendbar.
Haskell Syntax
class (C1 α, ..., Cn α) => C α where ...
• Die Klassenhierarchie muss azyklisch sein.
• Die einzige Typvariable die bei den Ci auftreten darf (und muss), ist α.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
337
10 · Typklassen und Overloading
Ober-/Unterklassen · 10.7
Anmerkung zur Schreibweise C α => D α
Die Syntax der Type Constraints nicht einfach als Implikation lesen!
1
2
class C α => D α where
...
1
2
C , D sind also Typklassen.
instance C α => D (T α) where
...
C , D Typklassen, T Typkonstruktor.
I
Definiert die Klasse D.
I
Nur wenn α Instanz von C ist
darf α auch zur Instanz von D
erklärt werden.
Instanziierung ⇒ C α
von α zu D ist
I
erlaubt.
I
Es gilt nicht C α ⇒ D α.
I
Es gilt D α ⇒ C α.
Für alle α die Instanz von C
sind wird T α zur Instanz von D.
C α ⇒ D (T α)
I
Es ist nicht jeder von T
konstruierte Typ automatisch
eine Instanz von D.
~
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
338
10 · Typklassen und Overloading
Ober-/Unterklassen · 10.7
Haskells numerische Klassen
Beispiel Alle numerischen Typen sind Instanz der Oberklasse Num.
1
2
3
4
5
class Num α where
(+), (-), (*)
negate
abs, signum
fromInteger
::
::
::
::
α -> α -> α
α -> α
α -> α
Integer -> α
6
— Minimal complete definition: All, except negate xor (-)
x - y
= x + negate y
negate x
= 0 - x
7
8
9
I
Num enthält nur solche Operationen, die auf alle numerischen Typen
anwendbar sind.
• +, - und * sind für alle numerischen Typen sinnvoll.
• Jede ganze Zahl lässt sich als reelle, rationale, komplexe, ... Zahl
interpretieren: fromInteger.
I
-x ist syntaktischer Zucker für negate x.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
339
10 · Typklassen und Overloading
Ober-/Unterklassen · 10.7
Beispiel Speziellere Zahlen: Fractional — Typen mit Division.
1
2
3
4
class Num α => Fractional α where
(/)
:: α -> α -> α
recip
:: α -> α
fromRational :: Rational -> α
5
— Minimal complete definition: fromRational, (recip | (/))
recip x
= 1 / x
x / y
= x * recip y
6
7
8
I
Der Type Constraint Num α drückt aus, dass Teilbarkeit eine
zusätzliche Eigenschaft von speziellen Numerischen Werten ist.
I
Fractional ist eine Spezialisierung von Num (cf. Seite 338)
Fractional α ⇒ Num α
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
340
10 · Typklassen und Overloading
Ober-/Unterklassen · 10.7
Beispiel Eine andere Teilbarkeit: Integral — ganzzahlige Typen.
1
2
3
4
5
6
7
8
class (Real α, Enum α) => Integral α where
quot :: α -> α -> α
rem :: α -> α -> α
div :: α -> α -> α
mod :: α -> α -> α
quotRem :: α -> α -> (α, α)
divMod :: α -> α -> (α, α)
toInteger :: α -> Integer
9
10
11
class (Num a, Ord a) => Real a where
toRational :: a -> Rational
I
-- "reelle" Zahlen kann man annähern
Integral ist eine Spezialisierung von Real und Enum:
• Ganze Zahlen können als Rationale Zahlen aufgefasst,
• und auch aufgezählt werden.
I
Die Definition von Klassenhierarchien ist nicht einfach und immer wieder
Gegenstand von Diskussionen.
• Haskells vordefinierte Klassenhierarchie ist in der Sprachdefinition46
beschrieben.
46 https://www.haskell.org/onlinereport/haskell2010/haskellch6.html#x13-1270006.3
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
341
10 · Typklassen und Overloading
Ober-/Unterklassen · 10.7
Numerische Konstanten
I
Eine numerische Konstante wie 42 ist in Haskell selbst überladen, da
sie – je nach Kontext – als Wert des Typs Integer aber auch z.B. als
Double angesehen werden kann.
I
Die Konstante 42 ist tatsächlich nur syntaktischer Zucker für
fromInteger 42.
I
Damit gilt 42 :: Num α ⇒ α, also 42 kann jeden beliebigen
numerischen Typ annehmen).
1
2
*Main> :t 42
42 :: Num a => a
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
342
10 · Typklassen und Overloading
Ober-/Unterklassen · 10.7
Philip Wadler, and Stephen Blott.
How to make Ad-hoc Polymorphism less Ad Hoc.
16th Symposium on Principles of Programming Languages. Austin,
Texas, 1989.
http://www.cs.bell-labs.com/~wadler/papers/class/class.ps.gz
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
343
11
Fallstudie: Reguläre Ausdrücke
11 · Fallstudie: Reguläre Ausdrücke
11.1
Regular Expressions · 11.1
Regular Expressions
Mit den bisher eingeführten Sprachelementen von Haskell lassen sich
bereits substantielle Projekte realisieren. Dieses Kapitel
1. definiert einen algebraischen Datentyp zur Repräsentation von regulären
Ausdrücken (regular expressions) und
2. entwickelt einen interessanten Algorithmus zum regular expression
matching, der ohne DFAs operiert.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
345
11 · Fallstudie: Reguläre Ausdrücke
Regular Expressions · 11.1
Erinnerung
I
Reguläre Ausdrücke hatten wir bereits besprochen, cf. Seite 31 oder in
der Schlüsselqualifikation für Informatiker.
Ihre Struktur kann man leicht angeben (cf. Seite 41), wir nehmen noch
Ergänzungen vor:
RegEx →
|
|
|
|
|
I
A
— ein Buchstabe aus dem Alphabet
RegEx , RegEx — verwenden , für Konkatenation
RegEx | RegEx — Alternative
RegEx *
— Wiederholung
ε
— Akzeptiert den leeren String
∅
— Akzeptiert überhaupt keinen String
Symbole können Zeichen (Char) sein, aber auch Zahlen, Bits, IP-Pakete,
Events einer XML-Parsers, ...
Wenn A den Typ der Zeichen repräsentiert, dann steht Strings also
generell für [A].
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
346
11 · Fallstudie: Reguläre Ausdrücke
Regular Expressions · 11.1
Algebraischer Datentyp RegEx
Diese Struktur können wir offenbar recht einfach in einen algebraischen
Datentyp übersetzen:
I
1
2
3
4
5
6
7
data RegEx α = RxSym α
| RxSeq (RegEx α) (RegEx α)
| RxAlt (RegEx α) (RegEx α)
| RxRep (RegEx α)
| RxEpsilon
| RxNone
deriving (Show, Eq)
—A
— r,s
— r|s
— r*
—ε
—∅
• Dabei lassen wir das Alphabet (also den Typ der Zeichen) frei.
⇒ Typvariable α.
Die Operatoren + und ? können wir bei Bedarf außerhalb des
Datentyps definieren als
I
1
2
rxPlus r = r ‘RxSeq‘ RxRep r
rxOpt
= RxAlt RxEpsilon
Frage Was sind die Typen dieser beiden Funktionen?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
347
11 · Fallstudie: Reguläre Ausdrücke
Regular Expressions · 11.1
Beispiel Den regulären Ausdruck 1, 1?, (0, 0)+ über dem Alphabet
Integer kann man als so konstruieren:
1
2
3
*Main> RxSym 1 ‘RxSeq‘ rxOpt (RxSym 1) ‘RxSeq‘ rxPlus (RxSym 0 ‘RxSeq‘ RxSym 0)
RxSeq (RxSeq (RxSym 1) (RxAlt RxEpsilon (RxSym 1))) (RxSeq (RxSeq (RxSym 0) (RxSy
m 0)) (RxRep (RxSeq (RxSym 0) (RxSym 0))))
Beachte Auf Haskell-Ebene (= Metaebene) klärt die explizite
Schachtelung der Konstruktoren (Klammern) mögliche Mehrdeutigkeiten.
Ein Konstruktor für Klammern in RegEx (= Objektebene) ist also
überflüssig.
(Diese Idee hatten wir schon einmal, cf. Seite 41.)
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
348
11 · Fallstudie: Reguläre Ausdrücke
11.2
RegEx als Instanz von Show · 11.2
RegEx als Instanz von Show
I
Um Werte des Typs RegEx α ausgeben zu können, ist RegEx α Instanz
von Show.
I
Um eine möglichst lesbare Ausgabe zu bekommen, programmieren wir
die Ausgabe selbst:
1
2
3
4
5
6
7
8
9
instance Show α => Show (RegEx α) where
showsPrec _ RxNone = showString "{}"
showsPrec _ RxEpsilon = showString "eps"
showsPrec _ (RxSym c) = shows c
showsPrec _ (RxRep r) = showsPrec 2 r . showChar ’*’
showsPrec p (RxSeq r s)
= showParen (p > 1) (showsPrec 1 r . showString ", " . showsPrec 2 s)
showsPrec p (RxAlt r s)
= showParen (p > 0) (showsPrec 0 r . showString " | " . showsPrec 1 s)
10
11
12
*Main> RxSym 1 ‘RxSeq‘ rxOpt (RxSym 1) ‘RxSeq‘ rxPlus (RxSym 0 ‘RxSeq‘ RxSym 0)
1, (eps | 1), (0, 0, (0, 0)*)
• Dazu muss man natürlich die deriving Show-Klausel bei der Definition von
data RegEx löschen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
349
11 · Fallstudie: Reguläre Ausdrücke
I
RegEx als Instanz von Show · 11.2
Einige Dinge sollten klar sein:
• Der Constraint Show α erklärt sich aus Zeile 4: Wir wollen die Zeichen in
einem regulären Ausdruck ausgeben können.
• Die Funktion showsPrec behandelt jeden Konstruktor von RegEx a mittels
Pattern Matching.
I
Aber: Wieso wird hier showsPrec definiert und nicht show?
○
1 Warum wird hier Funktionskomposition statt ++ zur Konkatenation von
Strings benutzt?
○
2 Wozu dient das zusätzliche (meist ignorierte) Argument von showsPrec?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
350
11 · Fallstudie: Reguläre Ausdrücke
RegEx als Instanz von Show · 11.2
1 : Schnelle Konkatenation mit “Fast Strings”
Zu ○
I
++ hat lineare Komplexität in der Länge des linken Argumentes.
⇒ Wiederholte Konkatenation von Teilergebnissen (Strings) in einer
Funktion show, wie sie für RegEx α notwendig wäre, führt zu einer Laufzeit,
die quadratisch in der Größe des regulären Ausdrucks ist (s. vorn).
Idee Wandle Funktionen der Form
f :: α -> String
um in Funktionen der Form
f 0 :: α -> (String -> String)
Dabei transformiert f 0 x s das Argument x :: α in einen String und
konkateniert das Ergebnis mit s, d.h.
f x
Stefan Klinger · DBIS
≡
f 0 x ""
Informatik 2 · Sommer 2016
351
11 · Fallstudie: Reguläre Ausdrücke
RegEx als Instanz von Show · 11.2
In der Prelude sind bereits vordefiniert:
1
type ShowS = String -> String
— “Fast Strings” with constant time concatenation
showString :: String -> ShowS
showString = (++)
— convert String into a Fast String
showChar :: Char -> ShowS
showChar = (:)
— convert Char into a Fast String
2
3
4
5
6
7
I
Das Schlüsselwort type führt einen Typ Alias ein. ShowS ist lediglich
eine Abkürzung für String → String.
I
Die Komposition ◦ wirkt auf Fast Strings wie die Konkatenation.
I
Durch Anwendung auf den leeren String "" kann ein Fast String in einen
normalen String umgewandelt werden.
Beispiel
1
2
*Main> showString "foo" . showString "bar" . showString "qux"
"foobarqux"
$ ""
Frage Wieso ist das schneller als einfach ++ zu verwenden?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
352
11 · Fallstudie: Reguläre Ausdrücke
RegEx als Instanz von Show · 11.2
Antwort Es geht darum links-tiefe Klammerungen von ++ zu vermeiden.
I
a ++ (b ++ c) ist besser als (a ++ b) ++ c.
I
ShowS nutzt ++ auch dann rechts-geklammert, wenn ursprünglich links
geklammert wurde.
Beispiel Konkatenation von "foo", "bar" und "baz":
(showString "foo" ◦ showString "bar") ◦ showString "baz"
_
_
_
""
showString
(("foo" ++ ) ◦ ("bar" ++ )) ◦ ("baz" ++ )
""
◦
(("foo" ++ ) ◦ ("bar" ++ )) ("baz" ++ "")
◦
"foo" ++ ("bar" ++ ("baz" ++ ""))
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
353
11 · Fallstudie: Reguläre Ausdrücke
RegEx als Instanz von Show · 11.2
Haskells Typklasse Show ist für den Einsatz dieser Technik entsprechend
vorbereitet:
1
2
3
class Show α where
show :: α -> String
showsPrec :: Int -> α -> ShowS
— type ShowS = String -> String
4
5
6
7
8
— Minimal complete definition: show|showsPrec
show x
= showsPrec 0 x ""
showsPrec _ x s = show x ++ s
.
.
.
9
10
11
showString :: String -> ShowS
showString = (++)
12
13
14
showChar :: Char -> ShowS
showChar = (:)
15
16
17
shows :: Show α => α -> ShowS
shows = showsPrec 0
Stefan Klinger · DBIS
— mehr dazu gleich...
Informatik 2 · Sommer 2016
354
11 · Fallstudie: Reguläre Ausdrücke
RegEx als Instanz von Show · 11.2
2 : Einsparen von Klammern bei der Ausgabe
Zu ○
I
Setze um einen Konstruktor Klammern genau dann, wenn der
umgebende Konstruktor eine höhere Priorität p aufweist.
Beispiel Wegen der Schachtelung ist die Priorität des umgebenden
Konstruktors immer bekannt. Der äußerste Konstruktor liegt in einem
“virtuellen Konstruktor” der Priorität 0 (cf. shows, letzte Folie).
p=0
p=2
z
}|
{
0, 0 | 1, 1
| {z } | {z }
z }| {
(0, 0)*
| {z }
p=1
I
I
p=1
p=1
showsPrec :: Integer → α → ShowS
Erstes Argument repräsentiert die Priorität des umgebenden
Konstruktors
showParen :: Bool → ShowS → ShowS
Erstes Argument bestimmt, ob zweites in Klammern gesetzt werden soll
oder nicht.
Frage Implementation von showParen?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
355
11 · Fallstudie: Reguläre Ausdrücke
11.3
Reguläre Sprachen, Wortproblem · 11.3
Reguläre Sprachen, Wortproblem
Jedem regulären Ausdruck r ist die Menge L r der Worte zugeordnet, die r
akzeptiert. L r heißt die von r akzeptierte Sprache47 .
Definition Das Wortproblem regulärer Ausdrücke
Gegeben sei ein regulärer Ausdruck r über einem Alphabet A, sowie eine
Eingabe i :: [A]. Das Wortproblem ist die Frage, ob i ∈ L r gilt, d.h. ob
der reguläre Ausdruck r die Eingabe i akzeptiert.
I
Wir betrachten im Folgenden eine “nicht-Standard”-Lösung für das
Wortproblem.
• Die Standard-Lösung für dieses Problem arbeitet mit Hilfe von endlichen
Automaten (Zustandsmaschinen). Die aus r konstruierte Maschine wird dann
mit i als Eingabe gestartet; stoppt sie in einem Endzustand, dann ist i ∈ L r .
47 Sprachen
(also Mengen von Worten) die durch einen regulären Ausdruck beschrieben werden
können, heißen reguläre Sprachen, cf. Theoretische Grundlagen der Informatik.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
356
11 · Fallstudie: Reguläre Ausdrücke
I
Reguläre Sprachen, Wortproblem · 11.3
Recht einfach können wir die akzeptierte Sprache für jeden Konstruktor
angeben:
r
∅
ε
c
s,t
s|t
s*
Stefan Klinger · DBIS
Lr
∅
{[]}
{[c]}
{ x ++ y | x ∈ L s, y ∈ L t }
L s ∪L t
{[]} ∪ L (s,s*)
Informatik 2 · Sommer 2016
357
11 · Fallstudie: Reguläre Ausdrücke
I
Reguläre Sprachen, Wortproblem · 11.3
Dies können wir nutzen, um die Sprachen der nicht-primitiven
Konstruktoren zu berechnen.
Beispiel
L (’x’?)
Stefan Klinger · DBIS
=
=
=
=
=
L(ε|’x’)
L ε ∪ L ’x’
{[]} ∪ {[’x’]}
{[], [’x’]}
{"", "x"}
Informatik 2 · Sommer 2016
358
11 · Fallstudie: Reguläre Ausdrücke
Reguläre Sprachen, Wortproblem · 11.3
Beispiel
L(1+)
=
=
=
=
=
=
=
=
=
=
..
.
L(1, 1*)
{ x ++ y | x ∈ L 1, y ∈ L(1*) }
{ x ++ y | x ∈ {[1]}, y ∈ L(1*) }
{ [1] ++ y | y ∈ L(1*) }
{ [1] ++ y | y ∈ {[]} ∪ L(1, 1*) }
{ [1] ++ y | y ∈ {[]} ∪ { x ++ z | x ∈ L(1), z ∈ L(1*) }
{ [1] } ∪ {[1] ++ y | y ∈ { x ++ z | x ∈ L(1), z ∈ L(1*) }
{ [1] } ∪ {[1] ++ y | y ∈ { [1] ++ z | z ∈ L(1*) } }
{ [1] } ∪ {[1] ++ [1] ++ y | y ∈ { z | z ∈ L(1*) } }
{ [1] } ∪ {[1, 1] ++ y | y ∈ L(1*) }
= { [1], [1, 1], [1, 1, 1], ...}
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
359
11 · Fallstudie: Reguläre Ausdrücke
Reguläre Sprachen, Wortproblem · 11.3
Schlaue Konstruktoren
I
Zwei reguläre Ausdrücke r , s sind äquivalent (r ≡ s), falls L r = L s.
I
Dies können wir nutzen, um bereits bei der Konstruktion
Vereinfachungen an regulären Ausdrücken vorzunehmen, ohne die
akzeptierte Sprache zu verändern.
Beispiele
1
2
I
r , ε ≡ ε, r ≡ r
I
r |∅≡∅|r ≡r
4
I
∅? ≡ ε
6
I
∅* ≡ ε
8
3
5
7
9
I
rxSeq r RxEpsilon = r
rxSeq RxEpsilon r = r
rxAlt r RxNone = r
rxAlt RxNone r = r
rxOpt RxNone = RxEpsilon
rxRep RxNone = RxEpsilon
Diese wrapper-Funktionen werden auch smart constructors genannt,
weil sie prinzipiell weitergehende Bedingungen für die Datenstruktur
erzwingen48 können.
48 Module
wie z.B. Data.Set exportieren die “echten” Konstruktoren für Set α nicht!
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
360
11 · Fallstudie: Reguläre Ausdrücke
Frage
Reguläre Sprachen, Wortproblem · 11.3
Wie lauten die Vereinfachungen hier?
I
∅, r
≡
I
r, ∅
≡
I
∅+
I
ε+
I
r **
≡
I
r *?
≡
≡
≡
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
361
11 · Fallstudie: Reguläre Ausdrücke
11.4
I
1
2
Die Ableitung regulärer Ausdrücke · 11.4
Die Ableitung regulärer Ausdrücke
Das Wortproblem: Wir suchen eine Funktion, die den Input i gegen den
regulären Ausdruck r matcht:
match :: RegEx α -> [α] -> Bool
match r i = i ∈ L r — Pseudo-Code!
• Da L r potenziell unendlich ist,
scheidet explizites Aufzählen
aus.
Idee Wir zerlegen das Problem: Die Eingabe i hat die Form c : cs.
1. Können Worte in L r überhaupt mit c anfangen?
(Wenn nicht, sind wir fertig: i 6∈ L r ⇒ match r i _ False)
2. Wenn ja, welcher reguläre Ausdruck r 0 beschreibt die erlaubten Reste
aller Worte in L r die mit c anfangen?
3. Gilt cs ∈ L r 0 ? —Dieses Problem ist einfacher!
Notation r 0 nennen wir die Ableitung von r nach c, und schreiben:
r 0 = ∂c r
Also muss gelten: L(∂c r ) = { cs | c:cs ∈ L r }.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
362
11 · Fallstudie: Reguläre Ausdrücke
Die Ableitung regulärer Ausdrücke · 11.4
Was bleibt zu tun?
I
Wenn wir ∂c r für jedes Symbol c und jeden regulären Ausdruck r
berechnen können, ist das Wortproblem weitgehend gelöst. Denn z.B.
⇔
⇔
⇔
I
[c1 , c2 , c3 ] ∈ L r
[c2 , c3 ] ∈ L ∂c1 r
[c3 ] ∈ L ∂c2 (∂c1 r )
[ ] ∈ L ∂c3 (∂c2 (∂c1 r ))
Damit haben wir unser Problem gelöst, sofern wir nun noch entscheiden
können, ob ein regulärer Ausdruck die leere Eingabe [] akzeptiert.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
363
11 · Fallstudie: Reguläre Ausdrücke
Die Ableitung regulärer Ausdrücke · 11.4
Diesen sogenannten nullable Test kann man tatsächlich sehr einfach
ausführen:
I
1
nullable :: RegEx α -> Bool
2
3
4
5
nullable RxNone
= False
nullable RxEpsilon = True
nullable (RxSym _) = False
— akzeptiert keine Eingabe, auch nicht die leere
— akzeptiert genau die leere Eingabe
— hier muss genau ein Buchstabe stehen
6
7
8
9
nullable (RxRep r)
=
nullable (RxSeq r s) =
nullable (RxAlt r s) =
?
?
?
Frage Wie lauten die fehlenden Gleichungen?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
364
11 · Fallstudie: Reguläre Ausdrücke
Die Ableitung regulärer Ausdrücke · 11.4
Sei
I
derive :: Eq α ⇒ RegEx α → α → RegEx α
die Haskell-Implementation von ∂, also ∂x r = derive r x.
Dann ist match ein Einzeiler:
I
1
2
match :: Eq α => RegEx α -> [α] -> Bool
match r = nullable . foldl derive r
Fragen Warum fold-left?
Stefan Klinger · DBIS
Wofür wird Eq α benötigt?
Informatik 2 · Sommer 2016
365
11 · Fallstudie: Reguläre Ausdrücke
Die Ableitung regulärer Ausdrücke · 11.4
Beispiel
foldl derive r [c0 , c1 , c2 ]
_
foldl derive (r ‘derive‘ c0 ) [c1 , c2 ]
_
foldl derive ((r ‘derive‘ c0 ) ‘derive‘ c1 ) [c2 ]
_
foldl derive (((r ‘derive‘ c0 ) ‘derive‘ c1 ) ‘derive‘ c2 ) []
_
((r ‘derive‘ c0 ) ‘derive‘ c1 ) ‘derive‘ c2
foldl
foldl
foldl
foldl
Frage Verhält sich match r [] richtig, d.h. wird auch die leere Eingabe
korrekt gematcht?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
366
11 · Fallstudie: Reguläre Ausdrücke
Die Ableitung regulärer Ausdrücke · 11.4
Die Ableitungsfunktion ∂
I
1
Es verbleibt die Implementation von derive:
derive :: Eq α => RegEx α -> α -> RegEx α
2
3
4
5
derive (RxSym c) x
| c == x
= rxEpsilon
— Hier brauchen wir Eq α
6
7
8
derive r@(RxRep s) x
= derive s x ‘rxSeq‘ r
9
10
11
derive (RxAlt r s) x
= derive r x ‘rxAlt‘ derive s x
12
13
14
15
16
17
~
derive (RxSeq r s) x
— Dies ist der einzige trickreiche Schritt
| nullable r
= (derive r x ‘rxSeq‘ s) ‘rxAlt‘ derive s x
| otherwise
= derive r x ‘rxSeq‘ s
18
19
20
derive _ _
= rxNone
Stefan Klinger · DBIS
— Alle anderen Fälle: ε, ∅, RxSym c | c6=x.
Informatik 2 · Sommer 2016
367
11 · Fallstudie: Reguläre Ausdrücke
Die Ableitung regulärer Ausdrücke · 11.4
Beispiel Die Ableitung derive implementiert tatsächlich unsere Idee (cf.
Seite 362, unten)
=
=
=
=
L (∂x ε)
=
{ cs | x:cs ∈ L ε }
=
{ cs | x:cs ∈ {[]} }
=
∅
=
L∅
=
=
=
=
=
Stefan Klinger · DBIS
L (∂x x)
{ cs | x:cs ∈ L x }
{ cs | x:cs ∈ {[x]} }
{[]}
Lε
L (∂x (r |s))
{ cs | x:cs ∈ L (r |s) }
{ cs | x:cs ∈ L r ∪ L s }
{ cs | x:cs ∈ L r } ∪ { cs | x:cs ∈ L s }
L (∂x r ) ∪ L (∂x s)
L (∂x r |∂x s)
Informatik 2 · Sommer 2016
368
11 · Fallstudie: Reguläre Ausdrücke
1
2
Die Ableitung regulärer Ausdrücke · 11.4
*Main> r
1, (eps | 1), (0, 0, (0, 0)*)
3
4
5
6
7
8
9
10
11
*Main> derive it 1
— im GHCi ist it das Ergebnis der letzten Berechnung
(eps | 1), (0, 0, (0, 0)*)
*Main> derive it 0
0, (0, 0)*
*Main> derive it 0
(0, 0)*
*Main> derive it 1
{}
12
13
14
15
16
*Main> foldl derive r [1,0]
0, (0, 0)*
*Main> nullable it
False
17
18
19
20
21
*Main> foldl derive r [1,0,0]
(0, 0)*
*Main> nullable it
True
22
23
24
25
26
*Main> match r [0,1,0,1,0]
False
*Main> match r [1,1,0,0,0,0]
True
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
369
12
Lazy Evaluation
12 · Lazy Evaluation
I
Im λ-Kalkül ist die Reihenfolge der Reduktionsschritte (_) für einen
Ausdruck nicht a priori festgelegt.
I
Bisher haben wir nur intuitiv erklärt, wie lazy evaluation arbeitet:
Teilausdrücke werden erst bei Bedarf reduziert.
I
Im Folgenden werden wir genauer betrachten
• was das bedeutet (→ nicht-strikte Semantik),
• und wie das implementiert werden kann (→ Auswertestrategien).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
371
12 · Lazy Evaluation
12.1
I
Strikte und nicht-strikte Semantik · 12.1
Strikte und nicht-strikte Semantik
Strikte Programmiersprachen beginnen die Auswertung eines Ausdrucks
bei der innersten Anwendung.
f (g x)
I
—zuerst wird g x reduziert
Nicht-strikte Sprachen fangen dagegen mit der äußersten Anwendung
an.
f (g x)
—zuerst wird f (...) reduziert
~
Wichtig Hier liegt ein semantischer Unterschied vor, d.h., strikte
und nicht-strikte Semantik ordnen dem gleichen Term unter Umständen
verschiedene Bedeutungen zu.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
372
12 · Lazy Evaluation
Strikte und nicht-strikte Semantik · 12.1
Beispiel Sei g x = ⊥ eine nicht terminierende (oder undefinierte)
Berechnung, und sei f = λy . 3.
Der Term f (g x) hat unter...
... strikter Semantik den Wert ⊥, weil vor der Reduktion von f (...) das
Argument (endlos) reduziert wird.
... nicht-strikter Semantik den Wert 3, weil zuerst die Anwendung von f
reduziert wird: f (g x) = (λy . 3) (g x) _ 3
β
Beispiele
Haskell hat nicht-strikte Semantik.
I
1
2
Prelude> take 10 [1..]
[1,2,3,4,5,6,7,8,9,10]
• Ebenso Miranda (ein direkter Haskell-Vorgänger) und Clean.
I
Die meisten Programmiersprachen sind jedoch strikt, z.B., C, Java, ML
(eine funktionale Sprache die starken Einfluss auf Haskell hatte).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
373
12 · Lazy Evaluation
Strikte und nicht-strikte Semantik · 12.1
Definition Strikte Funktion
Eine Funktion f heisst strikt, genau dann wenn
x _⊥
⇒
f x _⊥
andernfalls heisst die Funktion nicht-strikt.
I
In einer strikten Programmiersprache sind alle Funktionen strikt.
• Im Gegensatz zu nicht-strikten Sprachen lässt sich das Konstrukt
if · then · else · fi nicht als Funktion :: Bool → α → α → α ausdrücken.
I
1
2
In einer nicht-strikten Sprache darf eine Funktion auch dann einen Wert
zurückgeben, wenn ihr Argument ⊥ ist.
Prelude> const 3 $ length [1..]
3
1
2
Prelude> const 3 undefined — ⊥
3
Das muss allerdings nicht für jede Funktion gelten, (1 +) ist z.B. strikt:
1
2
Prelude> 1 + length [1..]
— terminiert nicht
Stefan Klinger · DBIS
1
2
Prelude> 1 + undefined
*** Exception: Prelude.undefined
Informatik 2 · Sommer 2016
374
12 · Lazy Evaluation
12.2
I
Auswertestrategien · 12.2
Auswertestrategien
Zwei mögliche Reduktionsstrategien sind
1. Applicative Order Reduction
(aka. Eager Evaluation oder Call by Value) und
2. Normal Order Reduction (+ Sharing)
(aka. Lazy Evaluation).
I
Sie legen dabei jeweils fest, welcher von im Allgemeinen mehreren
reduzierbaren Teilausdrücken (Redex, reducible expression) in einem
Ausdruck als nächster reduziert wird.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
375
12 · Lazy Evaluation
Auswertestrategien · 12.2
Beispiel Für die Auswertung des Ausdrucks const (sqr 4) (sqr 2) seien
const und sqr definiert durch:
const x y
sqr z
Applicative Order Reduction wählt
jeweils den linkesten innersten
Redex zur Reduktion aus:
= x
= ×z z
Normal Order Reduction wählt
jeweils den äußersten Redex zur
Reduktion aus:
const (sqr 4) (sqr 2)
_
β
_
const (sqr 4) (sqr 2)
const (× 4 4) (sqr 2)
_
β
sqr 4
×44
16
const 16 (sqr 2)
_
_
const 16 (× 2 2)
_
_
const 16 4
_
16
δ
β
δ
β
Stefan Klinger · DBIS
β
δ
Informatik 2 · Sommer 2016
376
12 · Lazy Evaluation
Auswertestrategien · 12.2
In der Baumnotation für Ausdrücke, in der die Applikation f e1 e2 durch
@
e2
@
f
e1
repräsentiert wird, reduziert Applicative Order Reduction zuerst die
inneren Teilbäume e1 und e2 während Normal Order Reduction zuvor den
äußeren linken Pfad von der Wurzel zu f reduziert (aka. Spine).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
377
12 · Lazy Evaluation
Auswertestrategien · 12.2
Beispiel Reduktion von const (sqr 4) (sqr 2) in Applicative Order.
@
@
@
sqr
const @
sqr
sqr
@
=
2
@
@
@
sqr
const @
4
β
2
sqr
2
4
@
×
@
@
const @
4
λz
4
z
@
×
@
_
z
_ nächste Folie
δ
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
378
12 · Lazy Evaluation
Auswertestrategien · 12.2
@
@
@
const 16
const
@
sqr
=
2
@
@
λx
@
16
sqr
_ λy
@
β
2
16
sqr
2
λy
x
@
@
sqr
_ λy
_ λy
@
β
16
2
@
×
δ
4 _ 16
β
16
2
Faustregel Bei Reduktion in Applicative Order wird der nächste Redex
durch den linkesten innersten reduziblen Knoten bestimmt. (Literatur:
leftmost innermost).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
379
12 · Lazy Evaluation
Auswertestrategien · 12.2
Beispiel Betrachten im Gegensatz dazu die Reduktion von
const (sqr 4) (sqr 2) in Normal Order.
@
@
@
=
sqr
const @
sqr
const
@
2
@
@
λx
sqr
λy
4
sqr
@
2
4
x
@
_
λy
@
β
sqr
@
sqr
@
@
_ sqr
β
sqr
4 _
β
2
4 _ 16
@
×
δ
4
4
Faustregel In Normal Order wird als nächstes der Knoten links außen
im Baum reduziert. (Literatur: leftmost outermost)
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
380
12 · Lazy Evaluation
12.3
Graph-Reduktion · 12.3
Graph-Reduktion
Es ist nicht wahr, dass Normal Order Reduction – in der Form wie eben
besprochen – immer weniger Reduktionen benötigt als Applicative Order
Reduction.
Beispiel Für den Ausdruck sqr (+ 4 2)...
...benötigt Normal Order
Reduction vier Reduktionen, ...
...Applicative Order Reduction
kommt mit drei Reduktionen aus.
sqr (+ 4 2)
_
β
_
sqr (+ 4 2)
× (+ 4 2) (+ 4 2)
_
δ
sqr 6
×66
36
× 6 (+ 4 2)
_
_
δ
×66
_
_
× 36
δ
δ
Stefan Klinger · DBIS
β
δ
Informatik 2 · Sommer 2016
381
12 · Lazy Evaluation
Graph-Reduktion · 12.3
Term-Graphen
I
Das Problem besteht hier in der Duplizierung eines Teilausdrucks, so
dass dieser Teilausdruck zweimal reduziert werden muss.
I
Jede Definition, in der ein Parameter auf der rechten Seite mehr als
einmal auftritt, leidet unter diesem Problem (sqr = λz. × z z).
I
FPLs arbeiten daher intern mit Term-Graphen, nicht mit Bäumen, so
können gemeinsame Teilausdrücke einfach repräsentiert werden (sharing).
(Eine Compilertechnik dazu ist die Common Subtree Elimination, welche identische
Teilausdrücke zusammenfasst.)
I
Die Auswertung erfolgt dann durch Graph-Reduktion.
I
Genau auf diese Weise sind auch let...in und where effizient realisierbar.
I
Beachte: Korrektheit der Graph-Reduktion beruht entscheidend auf der
Seiteneffektfreiheit!
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
382
12 · Lazy Evaluation
Graph-Reduktion · 12.3
Beispiel Mit Sharing benötigt auch Normal Order Reduction von
sqr (+ 4 2) lediglich drei Reduktionen:
@
@
sqr
@
@
δ
2
@
+
_
@
×
_
@
+
×
_ 36
δ
6
2
@
4
@
δ
4
Wichtig Mit Graph-Reduktion benötigt Normal Order Reduction nie
mehr Reduktionsschritte als Applicative Order Reduction.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
383
12 · Lazy Evaluation
12.4
Terminierung · 12.4
Terminierung
Applicative Order Reduction findet mitunter keine Normalform, weil die
Reduktionsfolge nicht terminiert.
Beispiel Zusätzlich zu fst seien noch answer und loop definiert:
answer = fst (42, loop)
loop
= tail loop
I
I
Die Auswertung von
terminiert nicht:
answer _ fst
_ fst
_ fst
_ fst
_ ...
answer mittels Applicative Order Reduction
(42,
(42,
(42,
(42,
loop)
tail loop)
tail (tail loop))
tail (tail (tail loop)))
(answer)
(loop)
(loop)
(loop)
Normal Order Reduction reduziert dagegen wie folgt:
answer
Stefan Klinger · DBIS
_ fst (42, loop)
_ 42
Informatik 2 · Sommer 2016
(answer)
(fst)
384
12 · Lazy Evaluation
Terminierung · 12.4
Eigenschaften
Satz
Terminieren sowohl Applicative Order als auch Normal Order Reduction,
so liefern beide dasselbe Ergebnis.
Satz
Wenn ein Ausdruck überhaupt eine Normalform besitzt, dann kann sie
durch Normal Order Reduction gefunden werden.
Ein Teilausdruck wird nur dann reduziert, wenn dies zur Berechnung des
Ergebnisses wirklich notwendig ist.
Beide Sätze ohne Beweis
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
385
12 · Lazy Evaluation
12.5
Weak Head Normal Form (WHNF) · 12.5
Weak Head Normal Form (WHNF)
I
Zentrales Ziel der Lazy Evaluation ist es, alle unnötigen Reduktionen zu
vermeiden.
I
Nicht-strikte FPLs reduzieren Ausdrücke daher nicht auf ihre tatsächliche
Normalform, sondern lediglich auf die sog. weak head normal form
(WHNF).
I
WHNF definiert ein Stop-Kriterium für Normal Order Reduction.
Beispiel Der Ausdruck x : xs ist auch dann schon in WHNF, wenn x und
xs jeweils noch nicht reduziert wurden.
Gleichwertig: Der Baum
@
xs
@
:
x
ist in WHNF, weil kein top-level Redex mehr existiert (obwohl noch
innere Redexe existieren).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
386
12 · Lazy Evaluation
Weak Head Normal Form (WHNF) · 12.5
Definition Weak Head Normal Form (WHNF)
Vorläufig, cf. Seite 397
Ein Ausdruck f e1 e2 ... en ist genau dann in Weak Head Normal Form,
wenn gilt
∀m, m ≤ n. f e1 ... em ist kein Redex.
Gemäß dieser Definition ist der Baum rechts
genau dann in WHNF, wenn er keinen
top-level Redex besitzt, d.h., wenn keiner
der mit f beginnenden Ausdrücke
f
f
f
f
e1
e 1 e2
e1 e2 e3
e1 e2 e3 e4
@
e4
@
e3
@
e2
@
f
e1
auf dem linken äußeren Zweig des Baumes
reduzierbar ist.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
387
12 · Lazy Evaluation
Weak Head Normal Form (WHNF) · 12.5
I
Die Reduktion stoppt, sobald kein top-level Redex mehr existiert.
Dadurch wird die evtl. unnötige Auswertung innerer Redexe verhindert.
I
Ein weiterer Vorteil: Wenn ein top-level Redex keine freien Variablen
beinhaltet (und damit auch seine Argumente nicht), benötigt die
Reduktionsmaschinerie keine α-Konversion, cf. Seite 103.
Beispiele für Terme in WHNF:
42
— Konstanten, dabei ist n = 0.
+3
— Partiell angewandte primitive Funktion.
λx. + 2 3
— Nicht angewandte Funktionen (auch hier n = 0).
(sqr 2, sqr 3)
— Tupelkonstruktor, nicht weiter reduzierbar.
[+ 2 3, length [1..]] — cons, nicht weiter reduzierbar.
I
Die letzten drei Ausdrücke sind in WHNF, aber nicht in Normalform
(dazu müssten die inneren Redexe noch reduziert werden). Ihr
top-level-Ausdruck ist jedoch irreduzibel.
I
Der letzte Ausdruck hat gar keine Normalform.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
388
12 · Lazy Evaluation
Weak Head Normal Form (WHNF) · 12.5
Beispiele für Lazy WHNF Reduction
Lazy Evaluation erlaubt einen daten-orientierten Programmierstil (auch
“listful style”), der in einer Sprache mit Applicative Order Reduction höchst
ineffizient wäre:
I
Daten-orientierte Programme konstruieren (komplexe, unendliche)
Datenstrukturen und
I
manipulieren diese Datenstrukturen Schritt für Schritt durch Anwendung
einer Komposition relativ simpler Funktionen.
Dank Lazy Evaluation werden die Datenstrukturen nie komplett erzeugt.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
389
12 · Lazy Evaluation
Weak Head Normal Form (WHNF) · 12.5
Beispiel Berechne die Summe der Quadrate der Zahlen von 1...n
“Klassische” rekursive Lösung:
I
1
2
3
I
sumsqr :: Integer -> Integer
sumsqr 1 = 1
sumsqr n = n^2 + sumsqr (n-1)
1 konstruiert die Liste [1..n], ○
2
Die Daten-orientierte Lösung ○
3 summiert
berechnet die Quadrate der Listenelemente [1,4,...n2 ] und ○
die Elemente dieser Liste:
sumsqr’ n = sum . map (ˆ2) $ [1..n]
|{z}
| {z }
| {z }
○
3
○
2
○
1
• sumsqr’ scheint mit dem Heap verschwenderisch umzugehen, denn die
1 und ○
2 bauen potentiell große Listen als Zwischenergebnisse auf.
Schritte ○
• Tatsächlich erzeugt Lazy Evaluation + WHNF während der Reduktion
keine der beiden Listen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
390
12 · Lazy Evaluation
Weak Head Normal Form (WHNF) · 12.5
Beispiel Reduktion von sumsqr’ 3
sumsqr0 3
_
sum ◦ map (ˆ 2) $ [1..3]
_
sum (map (ˆ 2) [1..3]))
_
sum (map (ˆ 2) (1 : [2..3]))
_
sum ((ˆ 2) 1 : map (ˆ 2) [2..3])
_
(ˆ 2) 1 + sum (map (ˆ 2) [2..3])
_
1 + sum (map (ˆ 2) [2..3])
_
1 + sum ((ˆ 2) 2 : map (ˆ 2) [3..3])
_
1 + (ˆ 2) 2 + sum (map (ˆ 2) [3..3])
_
1 + 4 + sum (map (ˆ 2) [3..3])
_
5 + sum (map (ˆ 2) [3..3])
_
5 + sum ((ˆ 2) 3 : map (ˆ 2) [ ])
_
5 + (ˆ 2) 3 + sum (map (ˆ 2) [ ])
_
5 + 9 + sum (map (ˆ 2) [ ])
_
14 + sum (map (ˆ 2) [ ])
_
14 + sum [ ] _ 14 + 0 _ 14
Stefan Klinger · DBIS
I
Das jeweils erste Listenelement
wird sofort quadriert und im
nächsten Schritt Teil der
Gesamtsumme.
I
Beachte: Hier steht [1..3] nicht
für die konstante Liste [1,2,3],
sondern für den Listengenerator,
der die Liste der Elemente 1 bis 3
bei Bedarf erzeugen kann.
Informatik 2 · Sommer 2016
391
12 · Lazy Evaluation
Weak Head Normal Form (WHNF) · 12.5
“Effizienzsteigerung” durch listful style
I
Der listful style of programming ermöglicht also die effiziente Verkettung
eines Generators mit einer Sequenz (Komposition) von Transformern.
I
Da Lazy Evaluation den Generator nur bei Bedarf nach einem
nächsten Listenelement aufruft (data on demand), kann der Generator
prinzipiell auch eine unendliche Datenstruktur erzeugen. Darauf kommen
wir gleich zurück.
I
Ausdrucksauswertung mittels Lazy Evaluation zeigt oft nicht sofort
offensichtliche Effekte bzgl. der Effizienz...
(Laufzeitanalyse ist tatsächlich schwierig)
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
392
12 · Lazy Evaluation
Weak Head Normal Form (WHNF) · 12.5
Effizient oder nicht?
Beispiel Bestimme das Minimum aus einer Liste von Zahlen
1 Sortiere die Liste (mittels Insertion Sort, isort
“Listful” Lösung: ○
2 dann enthält der Kopf der Liste das Minimum:
siehe unten), ○
min = head . isort (<)
Insertion Sort sortiert eine Liste, indem das jeweilige Kopfelement der
unsortieren Liste an seine korrekte Position (bzgl. lt) mittels ins in die
Ergebnisliste einfügt wird:
1
2
3
4
5
6
7
isort :: (α -> α -> Bool) -> [α]
isort lt []
= []
isort lt (x:xs) = ins x (isort lt
where
ins x []
ins x (x’:xs) |
|
-> [α]
xs)
= [x]
x ‘lt‘ x’ = x:x’:xs
otherwise = x’:ins x xs
Frage Wie effizient ist Insertion Sort?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
393
12 · Lazy Evaluation
Weak Head Normal Form (WHNF) · 12.5
Reduktion mittels Lazy Evaluation zeigt, dass die Argumentliste nie
vollständig sortiert wird: min hat lineare Komplexität trotz Ausnutzung
von isort in O(n2 ):
min [8, 6, 1, 7, 5]
_
(head . isort (<)) [8, 6, 1, 7, 5]
_
head (isort (<) [8, 6, 1, 7, 5])
_
head (ins 8 (ins 6 (ins 1 (ins 7 (ins 5 [])))))
_
head (ins 8 (ins 6 (ins 1 (ins 7 [5]))))
_
head (ins 8 (ins 6 (ins 1 (5 : ins 7 []))))
_
head (ins 8 (ins 6 (1 : (5 : ins 7 []))))
_
head (ins 8 (1 : (ins 6 (5 : ins 7 []))))
_
head (1 : ins 8 (ins 6 (5 : ins 7 [])))
_
1
Stefan Klinger · DBIS
I
In allen Fällen benötigt ins nur
je einen Reduktionsschritt, um
die Liste, in die eingefügt wird,
in WHNF zu bringen.
I
Der gesamte Listenrest wird nie
sortiert und am Ende ohnehin
“weggeworfen”, da head
lediglich auf den Listenkopf
zugreift.
Informatik 2 · Sommer 2016
394
12 · Lazy Evaluation
12.6
Unendliche Listen · 12.6
Unendliche Listen
I
Die Eigenschaft, Ausdrücke jeweils nur in ihre WHNF zu überführen,
verleiht lazy FPLs die Fähigkeit auch auf (potenziell!) unendlichen
Datenobjekten, vor allem Listen, zu operieren.
I
Im Zusammenspiel mit dem listful style of programming ergibt sich ein
sehr eleganter und doch effizienter Programmierstil.
~
I
Es ist wichtig, sich klarzumachen, dass unendliche Listen nicht die
Eigenschaften unendlicher mathematischer Objekte, etwa Mengen,
besitzen.49
49 Eigentlich
sind sie keine unendlichen Listen, sondern Generatoren, die unbeschränkt viele Elemente
erzeugen könnten.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
395
12 · Lazy Evaluation
Unendliche Listen · 12.6
Beispiel Während die Set Comprehension
{x 2 | x ∈ {1, 2, 3, ...}, x 2 < 10}
das endliche Objekt {1, 4, 9} bezeichnet, liefert die (eben nicht äquivalente)
List Comprehension
[ x 2 | x ← [1..], x 2 < 10 ]
Interpreter [1,4,9 —und terminiert nicht (tatsächlich sehen wir
1:4:9:⊥). Korrekt wäre hier etwa
(takeWhile (<10) . map (^2)) [1..]
Frage Sei cubes = [ x 3 | x ← [1..] ].
Was ist der Wert von elem 64 cubes? Und von elem 65 cubes?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
396
12 · Lazy Evaluation
Unendliche Listen · 12.6
WHNF und strikte Funktionen
Wir brauchen noch eine Erweiterung der Definition von WHNF:
I
Strikte Funktionen mit genügend Argumenten erzwingen die
Reduktion ihrer Argumente auf WHNF. Demnach ist
+ (× 2 3) (− 7 2)
noch nicht in WHNF,
obwohl der top-level Ausdruck + e1 e2 nicht reduzibel ist.
• Da der strikte Operator + mit genügend Argumenten versorgt wird, werden
zunächst e1 _ 6 und e2 _ 5 auf WHNF reduziert, und dann + 6 5 _ 11.
I
Sind nicht genügend Argumente vorhanden, so werden auch die
vorhandenen nicht reduziert. Demnach ist
+ (× 2 3)
bereits in WHNF,
obwohl + strikt ist, und × 2 3 noch reduziert werden könnte.
• Das wird klar wenn man Sections (cf. Seite 139) als syntaktischen Zucker
auffasst: Der Ausdruck + (× 2 3) = λx. + (× 2 3) x ist offenbar eine
nicht-angewandte λ-Abstraktion (Argument fehlt), und deshalb in WHNF.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
397
12 · Lazy Evaluation
Unendliche Listen · 12.6
iterate
Die Funktion iterate aus der standard prelude ist ein Generator
unendlicher Listen (cf. Seite 80 zur iterativen Wurzelbestimmung):
iterate f x = x : iterate f (f x)
Informell berechnet iterate f x also [x,f x,f 2 x,f 3 x,...]
Beispiel
iterate (+1) 1 = [1, 2, 3, 4, 5,...]
iterate (*2) 1 = [1, 2, 4, 8, 16,...]
iterate (‘div‘ 10) 2718 = [2718, 271, 27, 2, 0, 0, 0,...]
oder auch
[m..n]
≡
takeWhile (<= n) (iterate (+1) m)
→ Für den listful style ist iterate ein idealer Generator.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
398
12 · Lazy Evaluation
Unendliche Listen · 12.6
Beispiel digits bestimmt die Liste der Ziffern einer ganzen Zahl
digits = reverse . map (‘mod‘ 10) . takeWhile (/= 0) . iterate (‘div‘ 10)
Diese Sequenz aus Generator und Transformern berechnet dann digits
2718 wie folgt:
2718
↓
[2718, 271, 27, 2, 0, 0, 0,...]
↓
[2718, 271, 27, 2]
↓
[8, 1, 7, 2]
↓
[2, 7, 1, 8]
iterate (‘div‘ 10)
takeWhile (/= 0)
map (‘mod‘ 10)
reverse
Frage: Was berechnet die folgende ganz analog definierte Funktion foo?
foo n = map (take n) . takeWhile (/= []) . iterate (drop n)
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
399
13
Typinferenz
13 · Typinferenz
13.1
I
Typsysteme · 13.1
Typsysteme
Ziel der Typprüfung: alle Operanden von Operatoren haben einen
kompatiblen Typ.
• ... “Operatoren” sehr allgemein verstanden (z.B. auch Zuweisungen,
Funktionsaufrufe, ...)
I
Kompatibler Typ: Vom Operator erwarteter Typ.
• Manche Sprachen erlauben auch Typen, deren Werte automatisch in den
erwarteten Typ konvertiert werden können.
• Diese automatische Typkonvertierung nennt man Coercion, die zugehörigen
Regeln sind oft komplex50 .
I
Ein Typfehler besteht dann in der Anwendung eines Operators auf einen
Wert inkompatiblen Typs.
50 https://www.destroyallsoftware.com/talks/wat
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
401
13 · Typinferenz
Geschmacksrichtungen
Typsysteme · 13.1
tatsächlich ist diese Einteilung etwas schwammig
static/dynamic typing Ein statisches Typsystem überprüft die
Typkorrektheit beim Kompilieren (Haskell, Java, C).
• Zur Laufzeit liegen evtl. gar keine Typinformationen mehr vor (C).
• Ein dynamisches Typsystem prüft erst zur Laufzeit (Python).
strong/weak typing Ein starkes Typsystem erkennt alle Typfehler,
ggf. aber erst zur Laufzeit.
• Ein schwaches Typsystem kann bei unpassenden Typen zu unerwartetem
Verhalten führen (C), oder das Programm abbrechen.
polymorphic typing Polymorphie erlaubt die Verwendung einer Funktion
mit verschiedenen aber passenden Typen.
• Parametrische Polymorphie — Funktionen werden ohne Annahme über den
Typ des Argumentes implementiert.
• Ad-hoc Polymorphie — Für den jeweiligen Typ wird die passende
Implementierung einer Funktion ausgewählt.
• Subtyping — Funktionen für einen bestimmten Typ (z.B. Mammal) können
auch Werte von Untertypen (z.B. Bunny) verarbeiten.
Insgesamt sind Bezeichnungen wie statisch/dynamisch oder stark/schwach eher als
extreme Pole eines Spektrums von möglichen Ausprägungen zu verstehen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
402
13 · Typinferenz
Typsysteme · 13.1
Programmierer profitieren von der Typprüfung ihrer Programme:
I
“Well-typed programs do not ‘go wrong’.” (Robin Milner): ein
typkorrektes Programm wendet Funktionen nur auf Werte an, für die sie
auch definiert wurden.
I
Viele ansonsten schwer zu entdeckende Fehler werden durch den
Type-Checker zur Compile-Zeit erkannt.
1
Prelude> foldl sqrt
2
3
4
<interactive>:2:7:
Occurs check: cannot construct the infinite type: b ~ a -> b
Aber auch der Compiler und die Laufzeitumgebung ziehen Vorteile aus
der Typisierung:
I
Für ein typkorrektes Programm muss das Laufzeitsystem der Sprache
keine Tests auf die Typkorrektheit ausführen (⇒ kürzere Laufzeiten).
I
Der Compiler muss entsprechend keinen Code für solche Tests generieren
(⇒ kompakterer Objekt-Code).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
403
13 · Typinferenz
Typsysteme · 13.1
Überprüfung der Typkorrektheit
I
Type Checking
• Programmierer deklariert zu jedem Objekt (Variablen, Parameter, ...) den
gewünschten Typ.
• Compiler (statisch) bzw. Laufzeitsystem (dynamisch) prüft Typregeln und
meldet ggf. Typfehler.
I
Type Inference
• Programmierer gibt (fast) keine explizite Typisierung von Objekten an.
• Compiler leitet gewünschten Operandentyp aus Operatoren und
Inferenzregeln ab, meldet ggf. Typfehler.
Die meisten Programmiersprachen verwenden teilweise Typinferenz (z.B.
bei numerischen Konstanten), fordern aber auch Typdeklarationen, die
dann (statisch/dynamisch) geprüft werden.
Haskell basiert (fast ausschließlich) auf Typinferenz.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
404
13 · Typinferenz
13.2
I
I
Automatische Typinferenz · 13.2
Automatische Typinferenz
In Haskell ist es bis auf wenige Ausnahmefälle nicht notwendig (aber
sehr hilfreich), Werte und Ausdrücke mit ihren jeweiligen Typen zu
annotieren. Stattdessen leitet der Compiler die Typisierung
automatisch ab.
Die Typinferenz-Komponente eines Compilers
1. prüft, ob ein Programm nach den Typregeln der Sprache korrekt typisiert ist
(type check), und
1 erfüllt sein) leitet automatisch den Typ jedes Ausdrucks innerhalb
2. (sollte ○
dieses Programms ab (type inference).
I
Diese beiden Aufgaben werden verschränkt (nicht eine nach der anderen)
ausgeführt.
I
1 und
Mittels Intuition und “scharfem Hinsehen” lassen sich die Punkte ○
○
2 für viele Ausdrücke und Funktionsdefinitionen auch intuitiv ableiten.
Der später vorgestellte Inferenzalgorithmus orientiert sich an dieser
Intuition.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
405
13 · Typinferenz
Automatische Typinferenz · 13.2
Beispiel Typinferenz für die Funktion foldr mit folgender Definition:
foldr f z = go where go [ ]
= z
go (x : xs) = f x (go xs)
“Scharfes Hinsehen” liefert:
1. go operiert offensichtlich auf Listen und hat daher den Typ [α] → β.
2. Sowohl z als auch f x (go xs) können ein Ergebnis von go darstellen, und
haben also den Typ β.
3. Da x :: α und (go xs) :: β, muss f vom Typ α -> β -> β sein.
Also:
foldr :: (α → β → β) → β → [α] → β
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
406
13 · Typinferenz
Automatische Typinferenz · 13.2
Beobachtungen
Diese intuitive Typinferenz umfasst dabei die
1. Bestimmung eines Typs für einen Ausdruck an sich, wie etwa im letzten
Beispiel für go. Im Allgemeinen wird dieser Typausdruck Typvariablen
beinhalten.
2. Bestimmung eines Typs für einen Teilausdruck bereits typisierter
Ausdrücke, wie eben für z und f geschehen.
Typkorrekt Soll der Gesamtausdruck typkorrekt sein, dürfen sich die
dabei abgeleiteten Typen nicht widersprechen, d.h. sie müssen durch
geeignete Substitution von Typvariablen in dieselbe Form gebracht werden
können.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
407
13 · Typinferenz
Automatische Typinferenz · 13.2
Kernsprache für Typinferenz
Um den Rahmen der Betrachtungen nicht zu sprengen, besprechen wir hier
einen Typinferenz-Algorithmus für einen erweiterten λ-Kalkül:
I
Wir erweitern den einfachen λ-Kalkül um Konstanten (cf. Seite 94) und
lokale Definitionen via let ... in , und erhalten
Expr →
|
|
|
|
Const
Var
Expr Expr
λVar. Expr
let Var = Expr in Expr
Konstanten
Variablen
Applikation
λ-Abstraktion
lokale Definition
• Der let-Ausdruck kann hier tatsächlich nur eine Variable binden, erlaubt
uns aber immerhin Rekursion einzuführen.
• Üblicherweise wird ein mächtigeres let verwendet, das z.B. auch wechselseitig
rekursive Definitionen zulässt.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
408
13 · Typinferenz
13.3
I
Inferenzregeln · 13.3
Inferenzregeln
Inferenzregeln werden in der Logik oft in der Form
Prämisse1
Prämisse2
Prämisse3
verwendete Regel
Folgerung
notiert.
• Auf dieser Notation bauen z.B. Systeme natürlichen Schließens auf, eine
Familie von Kalkuli aus der Logik.
I
Damit lassen sich ganze Beweise elegant als Inferenzbäume
aufschreiben51 :
X ⇒ Y
X
⇒B
Y
Z
Y ∧Z
I
∧E
Angelehnt an diese Notation werden wir die Regeln der Typinferenz
notieren.
51 Dabei
stehen B bzw. E für Beseitigungs- bzw. Einfügeregeln.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
409
13 · Typinferenz
13.4
Regeln für die Typinferenz · 13.4
Regeln für die Typinferenz
Applikation & Abstraktion
I
In diesem Kapitel stehen die Ti für noch unbekannte Typen (i ∈ N).
Die Typvariablen (α, β, ...) verwenden wir dann für polymorphe Typen.
I
Seien f , e beliebige λ-Ausdrücke; x eine Variable.
Applikation
Die Inferenzregel für die Funktionsapplikation f e lautet:
f :: T1 → T2
e :: T1
@
f e :: T2
I
In einer Applikation f e muss f einen Typ haben, der den Typ T1 des
Arguments auf einen Ergebnistyp T2 abbildet.
I
T2 ist dann der Typ des Ausdrucks f e.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
410
13 · Typinferenz
Regeln für die Typinferenz · 13.4
Abstraktion Für die Abstraktion λx. e lautet die Regel:
e :: T2
λx. e :: T1 → T2
λx :: T1
I
Unter der Annahme52 daß die alle in e freien x den Typ T1 haben, ist
e :: T2 .
I
Die Funktion λx. e liefert dann für ein Argument vom Typ T1 Werte vom
Typ T2 des Funktionsrumpfes e.
52 diese
notieren wir etwas unorthodox rechts neben dem Bruchstrich.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
411
13 · Typinferenz
Regeln für die Typinferenz · 13.4
Beispiel Bestimme den Typ T0 von const = λx. λy . x:
I
Für eine Abstraktion λx... verwenden wir immer die λ-Regel:
λy . x :: T2
λx. λy . x :: T0
• Daraus lernen wir schon:
λx :: T1
T0 = T1 → T2 ,
• allerdings bleibt die Frage was T2 für
ein Typ sein soll.
I
Auch dafür wenden wir die λ-Regel an, und schreiben sie darüber:
x :: T1
• Dabei wissen wir schon aus der
λy :: T3
unteren Regel, daß x den Typ T1
λy . x :: T2
haben muss!
λx :: T1
λx. λy . x :: T0
• Neu lernen wir T = T → T .
2
I
3
1
Einsetzen der Gleichung für T2 in die Gleichung für T0 liefert:
T0 = T1 → T3 → T1
I
Diese Erkenntnis gilt offenbar für alle Typen T1 und T3 :
const :: ∀α β. α → β → α
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
412
13 · Typinferenz
Regeln für die Typinferenz · 13.4
Notation
I
Es ist nicht nötig die einzelnen Teilausdrücke bei jedem Schritt
hinzuschreiben:
x :: T1
T2
T0
λy :: T3
λx :: T1
x :: T1
statt
λy . x :: T2
λy :: T3
λx. λy . x :: T0
λx :: T1
• An der Struktur des Inferenzbaumes lässt sich die Struktur des Ausdrucks
ablesen — es ist der AST mit der Wurzel unten.
I
Analog zu λx y z. e = λx. λy . λz. e fassen wir Abstraktionen oft
zusammen: Für λx y . x also
x :: T1
x :: T1
λy :: T3
statt
λx :: T1 , y :: T2
T2
T0
λx :: T1
T0
• Daraus lesen wir direkt ab: T0 = T1 → T2 → T1 .
• Offensichtlich sparen wir uns eine Typvariable — und auch Rechenarbeit.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
413
13 · Typinferenz
Regeln für die Typinferenz · 13.4
Beispiel Typisierung von λx f . f x:
1. Zuerst konstruieren wir den Inferenzbaum:
f :: T2
x :: T1
T3
T0
•
(rechts der AST zum Vergleich)
x
f
@
@
λx :: T1 , f :: T2
λf
λx
~
Wichtig: An alle durch das gleiche Lambda gebundenen Variablen die
gleiche Typvariable schreiben!
2. Dann fangen wir unten (bei der Wurzel T0 ) an, Gleichungen abzulesen:
T0 = T1 → T2 → T3
T2 = T1 → T3
aus der λ-Regel
aus der @-Regel
3. Einsetzen in T0 liefert: T0 = T1 → (T1 → T3 ) → T3 .
4. Allquantifizieren liefert: λx f . f x :: ∀α β. α → (α → β) → β.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
414
13 · Typinferenz
Regeln für die Typinferenz · 13.4
Beispiel Die Typisierung von S = λf g x. f x (g x):
Wir konstruieren den Inferenzbaum
(rechts nochmal der AST zum Vergleich)
I
f :: T1
x :: T3
@
g :: T2
T5
T6
T4
T0
I
lesen ab
I
setzen ein
x :: T3
g
x
f
x
@
@
@
@
@
λf :: T1 , g :: T2 , x :: T3
λf g x
T0 = T1
T5 = T6
T1 = T3
T2 = T3
→ T2 → T3 → T4
→ T4
→ T5
→ T6
T0 = (T3 → T5 ) → (T3 → T6 ) → T3 → T4
= (T3 → T6 → T4 ) → (T3 → T6 ) → T3 → T4
I
und allquantifizieren:
S :: ∀α β γ. (α → β → γ) → (α → β) → α → γ
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
415
13 · Typinferenz
Regeln für die Typinferenz · 13.4
Beispiel Innere Abstraktion und Namensüberdeckung: λx y . x (λx. x y )
x :: T5
y :: T2
T6
x :: T1
T4
T3
T0
@
λx :: T5
liefert
@
λx :: T1 , y :: T2
T0 = T1
T1 = T4
T4 = T5
T5 = T2
→ T2 → T3
→ T3
→ T6
→ T6
~
Wo genau kommen die Typen Ti der einzelnen Variablen her?
Dann nur noch einsetzen und allquantifizieren:
I
T0 = ... = (((T2 → T6 ) → T6 ) → T3 ) → T2 → T3
λx y . x (λx. x y ) :: ∀α β γ. (((α → β) → β) → γ) → α → γ
1
2
3
Prelude> :t \x y -> x (\x -> x y)
— Man kann ja mal fragen
\x y -> x (\x -> x y) :: (((r2 -> r1) -> r1) -> r) -> r2 -> r
— Eigentlich: ∀r r1 r2...., der GHC lässt den Allquantor leider weg.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
416
13 · Typinferenz
13.5
I
Unifikation von Typen · 13.5
Unifikation von Typen
Manchmal erhalten wir mehrere Gleichungen für die selbe Typvariable.
Beispiel twice = λf x. f (f x)
f :: T1
f :: T1
x :: T2
T4
T3
T0
@
@
λf :: T1 , x :: T2
I
Zunächst konstruieren wir den Inferenzbaum.
I
Beide f sind durch das gleiche λ gebunden ⇒ gleiche Typvariable T1 .
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
417
13 · Typinferenz
13.5
I
Unifikation von Typen · 13.5
Unifikation von Typen
Manchmal erhalten wir mehrere Gleichungen für die selbe Typvariable.
Beispiel twice = λf x. f (f x)
f :: T1
f :: T1
x :: T2
T4
T3
T0
@
@
λf :: T1 , x :: T2
T0 = T1 → T2 → T3
I
Die λ-Regel liefert eine Gleichung für T0 .
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
418
13 · Typinferenz
13.5
I
Unifikation von Typen · 13.5
Unifikation von Typen
Manchmal erhalten wir mehrere Gleichungen für die selbe Typvariable.
Beispiel twice = λf x. f (f x)
f :: T1
f :: T1
x :: T2
T4
T3
T0
@
@
λf :: T1 , x :: T2
T0 = T1 → T2 → T3
T1 = T4 → T3
I
Die untere @-Regel liefert eine Gleichung für T1 .
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
419
13 · Typinferenz
13.5
I
Unifikation von Typen · 13.5
Unifikation von Typen
Manchmal erhalten wir mehrere Gleichungen für die selbe Typvariable.
Beispiel twice = λf x. f (f x)
f :: T1
f :: T1
x :: T2
T4
T3
T0
@
@
λf :: T1 , x :: T2
T0 = T1 → T2 → T3
T1 = T2 → T4
T1 = T4 → T3
I
Die obere @-Regel liefert eine weitere Gleichung für T1 .
Die schreiben wir erst mal auf die Seite, und nicht in die linke Liste von Gleichungen!
Frage Zwei Gleichungen für T1 . Was bedeutet das?
Weiter auf Seite 422...
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
420
13 · Typinferenz
Unifikation von Typen · 13.5
Definition Gleichheit von Typen
Typen sind gleich, wenn
I
sie primitiv sind und den gleichen Namen haben, oder
I
wenn sie vom gleichen Konstruktor konstruiert wurden, und die
Komponenten gleich sind.
Beispiele
I
Bool = Bool, Bool 6= Int, und Int 6= Integer
I
[Char] = String
I
(Int, [Char]) = (Int, String)
I
Int → Bool 6= Char → Bool
— Tatsächlich ist String ein Alias für [Char].
— weil Int 6= Char
Verwendung Aus T1 = T4 → T3 und T1 = T2 → T4 gewinnen wir also
zwei neue Gleichungen:
T4 = T2
Stefan Klinger · DBIS
und T3 = T4
Informatik 2 · Sommer 2016
421
13 · Typinferenz
Unifikation von Typen · 13.5
Beispiel Weiter mit twice = λf x. f (f x) von Seite 420:
f :: T1
f :: T1
x :: T2
T4
T3
T0
@
@
λf :: T1 , x :: T2
T0 = T1 → T2 → T3
T1 = T2 → T4
T1 = T4 → T3
I
Beide Gleichungen für T1 beschreiben einen konstruierten Typ.
• Der Typkonstruktor ist in beiden Fällen gleich: →
• Für die Gleichheit der Komponenten muss gelten: T4 = T2 und T3 = T4 .
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
422
13 · Typinferenz
Unifikation von Typen · 13.5
Beispiel Weiter mit twice = λf x. f (f x) von Seite 420:
f :: T1
f :: T1
x :: T2
T4
T3
T0
@
@
λf :: T1 , x :: T2
T0 = T1 → T2 → T3
T1 = T4 → T3
XT1 = T2 → T4
T4 = T2
T4 = T3
I
Die neuen Gleichungen schreiben wir ebenfalls auf die rechte Seite.
• Reihenfolge und Anordnung sind dabei nicht wichtig.
• Konvention: Wenn nur zwei Variablen gleichgesetzt werden, dann die
Variable mit dem größeren Index links vom Gleichzeichen schreiben.
I
Damit haben wir die Gleichung T1 = T2 → T4 abgearbeitet.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
423
13 · Typinferenz
Unifikation von Typen · 13.5
Beispiel Weiter mit twice = λf x. f (f x) von Seite 420:
f :: T1
f :: T1
x :: T2
T4
T3
T0
@
@
λf :: T1 , x :: T2
T0 = T1 → T2 → T3
XT1 = T2 → T4
T1 = T4 → T3
XT4 = T2
T4 = T2
T4 = T3
I
Da links noch keine Gleichung für T4 steht, schieben wir eine der beiden
Gleichungen für T4 nach links.
I
Jetzt haben wir links und rechts eine Gleichung für T4 , daraus gewinnen
wir T2 = T3 .
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
424
13 · Typinferenz
Unifikation von Typen · 13.5
Beispiel Weiter mit twice = λf x. f (f x) von Seite 420:
f :: T1
f :: T1
x :: T2
T4
T3
T0
@
@
λf :: T1 , x :: T2
T0 = T1 → T2 → T3
XT1 = T2 → T4
T1 = T4 → T3
XT4 = T2
T4 = T2
XT4 = T3
T3 = T2
I
Da links noch keine Gleichung für T3 steht, schieben wir die neue
Gleichung ebenfalls nach links.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
425
13 · Typinferenz
Unifikation von Typen · 13.5
Beispiel Weiter mit twice = λf x. f (f x) von Seite 420:
f :: T1
f :: T1
x :: T2
T4
T3
T0
@
@
λf :: T1 , x :: T2
T0 = T1 → T2 → T3
XT1 = T2 → T4
T1 = T4 → T3
XT4 = T2
T4 = T2
XT4 = T3
T3 = T2
XT3 = T2
I
Wir sind fertig sobald rechts keine Gleichungen mehr stehen,
I
und alle Inferenzregeln abgearbeitet wurden.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
426
13 · Typinferenz
Unifikation von Typen · 13.5
Beispiel Weiter mit twice = λf x. f (f x) von Seite 420:
f :: T1
f :: T1
x :: T2
T4
T3
T0
@
@
λf :: T1 , x :: T2
T0 = T1 → T2 → T3
XT1 = T2 → T4
T1 = T4 → T3
XT4 = T2
T4 = T2
XT4 = T3
T3 = T2
XT3 = T2
I
Einsetzen in die Gleichung für T0 liefert T0 = (T2 → T2 ) → T2 → T2 .
I
Allquantifizieren liefert das Ergebnis:
twice :: ∀α. (α → α) → α → α
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
427
13 · Typinferenz
Unifikation von Typen · 13.5
Beispiel Typisierung von E = λf x y . f x (f y x).
f :: T1
f :: T1
x :: T2
y :: T3
@
T7
@
T5
x :: T2
T6
T4
T0
@
@
λf :: T1 , x :: T2 , y :: T3
T0 = T1 → T2 → T3 → T4
XT1 = T3 → T7
T5 = T6 → T4
XT3 = T2
T1 = T2 → T5
XT7 = T5
T7 = T2 → T6
XT5 = T2 → T6
T3 = T2
XT6 = T2
T6 = T2
XT6 = T4
T4 = T2
XT4 = T2
E :: ∀α. (α → α → α) → α → α → α
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
428
13 · Typinferenz
13.6
I
Typfehler · 13.6
Typfehler
Nicht immer lässt sich ein λ-Ausdruck typisieren.
Beispiel λf . f (λx. f )
1
2
3
4
Prelude> :t \f -> f (\x -> f)
<interactive>:1:17:
Occurs check: cannot construct the infinite type:
r2 ~ (r1 -> r2) -> r
f :: T1
f :: T1
T3
λx :: T4
@
T1 = T3 → T2
T2
I
T0 = T1 → T2
T3 = T4 → T1
λf :: T1
T0
Beim Einsetzten von T3 in die Gleichung für T1 stößt man auf den Zykel
T1 = (T4 → T1 ) → T2
⇒ Typfehler wegen unendlicher Typen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
429
13 · Typinferenz
Typfehler · 13.6
Innere Typfehler
I
...die sieht man nicht gleich
Damit ein Ausdruck typkorrekt ist, müssen alle seine Teilausdrücke
typkorrekt sein.
Beispiel Die Typisierung von E = λx. (λy . x) (λf . f f ).
f :: T6
x :: T1
T3
T7
λy :: T4
T2
T0
f :: T6
T5
λx :: T1
@
T0 = T1 → T2 XT3 = T4 → T1
T3 = T5 → T2 XT5 = T4
λf :: T6
T5 = T4
XT2 = T1
@
T2 = T1
XT5 = T6 → T7
T4 = T6 → T7 XT4 = T6 → T7
T6 = T6 → T7
I
Einsetzen liefert T0 = T1 → T2 = T1 → T1 und damit E :: ∀α. α → α.
I
Das ist Falsch weil λf . f f schon einen Typfehler hat: T6 .
• Tatsächlich müssen wir zum Schluß alle Typvariablen durch vollständiges
Einsetzen überprüfen.
• Beim Einsetzen in T3 fällt der Fehler dann auf.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
430
13 · Typinferenz
I
Typfehler · 13.6
In den nachfolgenden Abschnitten werden wir weitere Typfehler sehen,
hier nur ein Überblick:
• Unendlicher Typ, z.B.:
T1 = T1 → T2
oder
T3 = [T3 ]
• Verschiedene primitive Typen, z.B.:
Int = Char
• Inkompatible Typkonstruktoren, z.B.:
(T1 , T2 ) = [T3 ]
Int = T4 → T5
oder
~
Vorsicht Nicht alles was unpassend aussieht ist auch ein Fehler.
Frage Was bedeutet das folgende Gleichungssystem?
T0 = T1 → T2
Stefan Klinger · DBIS
T0 = T3 → T4 → T5
Informatik 2 · Sommer 2016
431
13 · Typinferenz
13.7
I
Vordefinierte Funktionen und Konstanten · 13.7
Vordefinierte Funktionen und Konstanten
Unser λ-Kalkül erlaubt die Verwendung von Konstanten, also etwa
vordefinierten Werten oder Funktionen mit ihrem jeweiligen Typ.
• "hello" :: [Char]
• + :: Int → Int → Int
• foldr :: ∀α β. (α → β → β) → β → [α] → β
I
I
Wenn der Ausdruck eine Konstante enhält, fügen wir den entsprechenden
Typ in das Gleichungssystem ein.
~
Dabei müssen wir zwischen monomorphen und polymorphen
Typen unterscheiden.
Definition Monomorphe Typen
I
Ein Typ heißt monomorph gdw. er keinen Allquantor enthält.
I
Damit ist monomorph das genaue Gegenteil von polymorph.
Wir werden Ad-hoc Polymorphie nicht besprechen, d.h. dass uns Typklassen und Typen
der Form C α ⇒ α → Int nicht begegnen werden.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
432
13 · Typinferenz
Vordefinierte Funktionen und Konstanten · 13.7
Die Verwendung monomorpher Konstanten
I
Beispiele für Konstanten mit monomorphem Typ:
42 :: Int, True :: Bool, even :: Int → Bool, ....
I
Deren Typ kopieren wir einfach als erstes in das Gleichungssystem.
Beispiel Der Typ von λf . even (f 42).
f :: T1
even :: T3
42 :: T5
T4
T2
T0
λf :: T1
@
@
T3 = Int → Bool
XT3 = T4 → T2
T5 = Int
XT4 = Int
T0 = T1 → T2
XT2 = Bool
T4 = Int
T2 = Bool
T1 = T5 → T4
T0 = T1 → T2 = (T5 → T4 ) → T2 = (Int → Int) → Bool enthält keine
freien Typvariablen über die allquantifiziert werden könnte.
λf . even (f 42) :: (Int → Int) → Bool
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
433
13 · Typinferenz
Vordefinierte Funktionen und Konstanten · 13.7
Beispiel Der Typ von λf . f True.
f :: T1
True :: T3
T2
T0
T3 = Bool
@
T0 = T1 → T2
λf :: T1
T1 = T3 → T2
I
Einsetzen liefert T0 = ... = (Bool → T2 ) → T2 .
I
Bool ist eine Typkonstante, über T2 müssen wir allquantifizieren:
λf . f True :: ∀α. (Bool → α) → α
Dieser Ausdruck hat also polymorphen Typ, und enthält Typkonstanten.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
434
13 · Typinferenz
13.8
Polymorphie verwenden · 13.8
Polymorphie verwenden
I
Konstanten mit polymorphem Typ sind z.B. id, map, length, ...
I
Haskell stellt solche Typen mit freien Typvariablen dar, welche
implizit durch einen Allquantor gebunden sind:
1
2
3
4
I
Prelude> :t id
id :: α -> α
Prelude> :t map
map :: (α -> β) -> [α] -> [β]
—eigentlich id :: ∀α. α → α
—eigentlich map :: ∀α β. (α → β) → [α] → [β]
Bei der Verwendung polymorpher Konstanten werden die
allquantifizierten Variablen durch konkrete Typen instanziiert.
Instanziierung Für die Typinferenz heißt das, dass für jede Verwendung
die allquantifizierten Typvariablen konsistent durch neue Typvariablen
ersetzt werden:
const :: ∀α β. α → β → α
instanziiert z.B. zu T23 → T24 → T23
~
Dabei stehen die Ti für neue monomorphe unbekannte Typen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
435
13 · Typinferenz
Polymorphie verwenden · 13.8
Beispiel Gegeben sei id :: ∀α. α → α. Was ist der Typ von id id ?
id :: T1
id :: T2
T0
I
I
@
T1 = T3 → T3
XT1 = T2 → T0
T2 = T4 → T4
XT3 = T2
T3 = T2
XT3 = T0
T0 = T4 → T4
XT2 = T0
Dabei sind T1 und T2 potentiell verschiedene Instanzen des
polymorphen Typs ∀α. α → α, erkennbar an den (noch unbekannten)
Typen T3 und T4 ..
Allquantifizieren von T0 = T4 → T4 liefert id id :: ∀α. α → α.
Wichtig Dies ist die Kernidee der Polymorphie:
I Die “vielgestaltigen Teile” eines polymorphen Typs werden durch
allquantifizierte Variablen markiert.
I Bei jeder Verwendung nimmt eine polymorphe Funktion einen
potentiell anderen monomorphen Typ an, indem die allquantifizierten
Variablen konkretisiert werden.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
436
13 · Typinferenz
Polymorphie verwenden · 13.8
Beispiel Bei falscher monomorpher53 Verwendung von id würden wir
einen Typfehler erhalten:
id :: T1
id :: T1
@
T1 = T1 → T0
—unendlicher Typ!
T0
Erinnerung
“Polymorphie” bedeutet Vielgestaltigkeit.
I
Hier haben wir die Funktion in nur einer Gestalt, nämlich T1 verwendet.
I
Auf der vorherigen Folie haben wir durch die Verwendung
unterschiedlicher Typvariablen T1 und T2 verschiedene Typen erlaubt.
53 d.h.,
ohne Instanziierung mit frischen Typvariablen, also gerade nicht polymorph
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
437
13 · Typinferenz
Polymorphie verwenden · 13.8
Beispiel Der Typ von const 42 const.
const :: T2
42 :: T3
@
T1
const :: T4
@
T0
T2
T3
T4
T1
T5
T6
T0
I
I
= T5 → T6 → T5
= Int
= T7 → T8 → T7
= T4 → T0
= T3
= T4
= Int
XT2
XT5
XT1
XT6
XT5
XT3
XT0
= T3 → T1
= T3
= T6 → T5
= T4
= T0
= T0
= Int
Damit ist const 42 const :: Int.
Interessant: Die jeweiligen konkreten Typen von const:
• T2 = T5 → T6 → T5 = T3 → T4 → T3 = Int → (T7 → T8 → T7 ) → Int
• T4 = T7 → T8 → T7
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
438
13 · Typinferenz
Polymorphie verwenden · 13.8
Beispiel Was ist der Typ von map length "foo" ?
Gegeben seien "foo" :: String, length :: ∀α. [α] → Int, sowie
map :: ∀α β. (α → β) → [α] → [β].
map :: T2
length :: T3
@
T1
"foo" :: T4
@
T0
T2
T3
T4
T1
T5
T6
T0
I
= (T5 → T6 ) → [T5 ] → [T6 ]
= [T7 ] → Int
= [Char]
= T4 → T0
= [T7 ]
= Int
= [T6 ]
XT2
XT3
XT1
XT5
XT6
XT4
XT0
T5
= T3 → T1
= T5 → T6
= [T5 ] → [T6 ]
= [T7 ]
= Int
= [T5 ]
= [T6 ]
= Char
Typfehler in T5 , da der primitive Typ Char nicht mit dem
konstruierten Typ [T7 ] unifiziert werden kann.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
439
13 · Typinferenz
13.9
let-Polymorphie · 13.9
let-Polymorphie
Das let...in-Konstrukt scheint auf den ersten Blick redundant zu sein, da
man die Variablenbindung auch über eine λ-Abstraktion erreichen kann:
let x = m in e
I
?
≡
(λx. e) m
Ohne Rekursion gilt diese Äquivalenz im untypisierten λ-Kalkül,
allerdings verhalten sie sich unterschiedlich bezüglich ihrer Typisierung.
• Wir definieren also andere Typregeln für let...in als für die λ-Abstraktion.
I
Ausblick: Mit dem bisher beschriebenen Typsystem für den λ-Kalkül
ohne let lässt sich kein typkorrekter Fixpunkt-Kombinator formulieren.
• Dieser sogenannte “simply typed λ-calculus” (auch als λ→ bezeichnet) ist
sogar stark normalisierend, d.h., jedes in λ→ geschriebene Programm
terminiert54 . Damit ist λ→ leider nicht Turing-Vollständig.
• Um rekursive Funktionen definieren zu können, erlauben wir später auch noch
rekursive Definitionen mit let, cf. Seite 448.
54 W.
W. Tait. Intensional Interpretations of Functionals of Finite Type I. JSL 32(2), 1967.
http://www.jstor.org/stable/pdf/2271658.pdf
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
440
13 · Typinferenz
let-Polymorphie · 13.9
Beispiel In dem Ausdruck
(wieder: length :: ∀α. [α] → Int, "foo" :: String)
id length (id "foo")
wird id :: ∀α. α → α verschieden instanziiert.
I
I
Aber wo genau kommt dieses id her?
Können wir selber Ausdrücke schreiben die eine polymorphe Funktion
einführen und verwenden?
• Bisher hatten wir ja erst am Ende der Typinferenz allquantifiziert, und
• verwendete polymorphe Terme waren immer vordefinierte Konstanten.
I
Der erste Ansatz
(λf . f length (f "foo")) (λx. x)
führt zu einem Typfehler der Form
T5 = [T9 ] → Int
∧
T5 = [Char]
Fragen Warum ist das ein Typfehler? Warum tritt genau dieser Typfehler
auf? Und was ist das grundlegende Problem?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
441
13 · Typinferenz
let-Polymorphie · 13.9
let führt polymorphe Typen ein
I
Der Kern des Problems liegt in der λ-Regel:
e :: T2
λx. e :: T1 → T2
λx :: T1
Sie erzwingt den gleichen monomorphen Typ T1 für alle in e freien
Vorkommen von x.
• Es gibt (deutlich komplexere) Typsysteme, die diese Einfschränkung
aufweichen.
I
Dagegen erlaubt let die Definition von lokalen, polymorphen
Ausdrücken, die dann zu unterschiedlichen konkreten Typen instanziiert
werden können:
let x = m in e
erlaubt die polymorphe Verwendung von x in e.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
442
13 · Typinferenz
let-Polymorphie · 13.9
Beispiel let f = λx. x in f length (f "foo")
f :: T5
x :: T2
f :: T1
length :: T6
@
f :: T8
T4
λx :: T2
"foo" :: T9
T7
T3
@
@
let f
T0
I
Gegeben sind
T6 = [T10 ] → Int
length :: ∀α. [α] → Int
T9 = [Char]
"foo" :: String
I
Durch Instanziieren erhalten wir Typgleichungen
für T6 und T9 .
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
443
13 · Typinferenz
let-Polymorphie · 13.9
Beispiel let f = λx. x in f length (f "foo")
f :: T5
x :: T2
f :: T1
length :: T6
@
f :: T8
T4
λx :: T2
"foo" :: T9
T7
T3
@
@
let f
T0
I
I
Die let-gebundene Variable f wird im in-Zweig
polymorph verwendet.
⇒ verschiedene Typvariablen T5 , T8 .
T6 = [T10 ] → Int
T9 = [Char]
Für T5 und T8 fehlt uns noch der polymorphe Typ
von f .
⇒ Den müssen wir zuerst bestimmen...
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
444
13 · Typinferenz
let-Polymorphie · 13.9
Beispiel let f = λx. x in f length (f "foo")
f :: T5
x :: T2
f :: T1
length :: T6
@
f :: T8
T4
λx :: T2
"foo" :: T9
T7
T3
@
@
let f :: ∀α. α → α
T0
I
I
Vollständige Typinferenz für den let-Zweig liefert
(hier: durch nur eine neue Gleichung) T1 .
Allquantifizieren der freien Typvariablen ergibt den
polymorphen Typ von f im in-Zweig...
T6 = [T10 ] → Int
T9 = [Char]
T1 = T2 → T2
f :: ∀α. α → α
I
...der dann zu T5 und T8 instanziiert wird.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
445
13 · Typinferenz
let-Polymorphie · 13.9
Beispiel let f = λx. x in f length (f "foo")
f :: T5
x :: T2
f :: T1
length :: T6
@
f :: T8
T4
λx :: T2
"foo" :: T9
T7
T3
@
@
let f :: ∀α. α → α
T0
I
I
Vollständige Typinferenz für den let-Zweig liefert
(hier: durch nur eine neue Gleichung) T1 .
Allquantifizieren der freien Typvariablen ergibt den
polymorphen Typ von f im in-Zweig...
f :: ∀α. α → α
I
T6 = [T10 ] → Int
T9 = [Char]
T1 = T2 → T2
T5 = T11 → T11
T8 = T12 → T12
...der dann zu T5 und T8 instanziiert wird.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
446
13 · Typinferenz
let-Polymorphie · 13.9
Beispiel let f = λx. x in f length (f "foo")
f :: T5
x :: T2
f :: T1
length :: T6
@
f :: T8
T4
λx :: T2
"foo" :: T9
T7
T3
@
@
let f :: ∀α. α → α
T0
I
I
Der Typ des in-Zweiges ist immer auch der Typ
des gesamten let...in-Ausdrucks.
Mit diesen Gleichungen wird die Typinferenz im
in-Zweig fortgesetzt, und liefert schließlich
T9 = [Char]
T1 = T2 → T2
T5 = T11 → T11
T0 = Int
T5 = ([Char] → Int) → [Char] → Int
T8 = [Char] → [Char]
Stefan Klinger · DBIS
T6 = [T10 ] → Int
Informatik 2 · Sommer 2016
T8 = T12 → T12
T3 = T0
447
13 · Typinferenz
let-Polymorphie · 13.9
let-Rekursion
I
Wir verwenden das let-Konstrukt auch, um rekursive Funktionen zu
definieren55 , cf. Seite 440.
let f = e1 f in e2 f
I
Damit die Typisierung der rekursiven Definition von f gelingen kann,
müssen wir f im let-Zweig einen monomorphen Typ T1 geben.
I
Nur im in-Zweig kann f polymorph verwendet werden, und bekommt
bei jeder Verwendung eine neue Typvariable, hier T5 .
e1 :: T2
f :: T1
@
e2 :: T4
f :: T1
T3
f :: T5
@
let f
T0
55 Hier
steht ei f allgemein für einen beliebigen λ-Ausdruck der f frei enthält.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
448
13 · Typinferenz
let-Polymorphie · 13.9
Beispiel let g = λx. cons x (g x) in g .
cons :: T5
x :: T2
g :: T1
@
T4
T6
T3
g :: T1
Mit cons :: ∀α. α → [α] → [α].
x :: T2
@
@
λx :: T2
g :: T7
let g
T0
I
I
I
Im let-Zweig bekommt g den monomorphen Typ T1 .
Typinferenz, von unten bis in den let-Zweig, liefert T1 = T2 → [T2 ].
Im in-Zweig verwenden wir g polymorph. Allquantifizieren von T1 liefert
g :: ∀α. α → [α]
I
was zu T7 = T9 → [T9 ] instanziiert wird.
Mit T7 = T0 und Allquantifizieren der freien Typvariablen folgt
let g = λx. cons x (g x) in g :: ∀α. α → [α]
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
449
13 · Typinferenz
let-Polymorphie · 13.9
Gebundene Typvariablen
I
I
I
Der durch let-eingeführte Term kann Variablen enthalten, die unterhalb
des let gebunden wurden
Deshalb können Typvariablen aus der Gleichung
y :: Ty
für Ty in der Gleichung für Tx auftauchen.
·
·
·
·
·
Tx = Ta → Tc
·
x :: Tx
let x
Ty = Tb → Tc
~
·
·
Diese dürfen nicht allquantifiziert werden!
·
Falls Ta nicht ebenfalls in Ty auftaucht, gilt hier:
λy :: Ty
T0
x :: ∀α. α → Tc
Folgerung Nach der Typinferenz für den let-Zweig müssen die
Gleichungen für alle unterhalb (durch λ oder let) gebundenen Typvariablen
vollständig eingesetzt werden. Dann erst kennen wir die freien
Typvariablen, die allquantifiziert werden müssen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
450
13 · Typinferenz
let-Polymorphie · 13.9
Beispiel λx. let xs = cons x xs in xs.
cons :: T5
x :: T1
Mit cons :: ∀α. α → [α] → [α].
@
T4
xs :: T3
@
xs :: T3
xs :: T6
T2
T0
λx :: T1
Typinferenz von unten bis in den let-Zweig
liefert die Gleichungen rechts, also T3 = [T1 ].
~
Obacht Die Typvariable T1 ist außerhalb
des let gebunden. Der Typ von xs ist nicht
polymorph in T1 . Allquantifizieren liefert also
“nur” xs :: [T1 ] und damit T6 = [T1 ].
I
let xs
T5 = T7 → [T7 ] → [T7 ]
T0 = T1 → T2
T6 = T2
T4 = T3 → T3
T7 = T1
T3 = [T7 ]
Der Rest der Typinferenz verläuft wie gewohnt, und liefert ∀α. α → [α].
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
451
13 · Typinferenz
let-Polymorphie · 13.9
Aufschlussreich ist der Vergleich der beiden vorangegangenen Beispiele:
I
let g = λx. cons x (g x) in g
cf. Seite 449
• Hier ist g eine Funktion, die eine Liste bestehend aus Wiederholungen ihres
Arguments erzeugt.
• g kann auf Elemente beliebigen Typs angewandt werden, ist also polymorph.
I
λx. let xs = cons x xs in xs
cf. Seite 451
• Auch hier erzeugt xs eine Liste bestehend aus Wiederholungen von x.
Allerdings steht das zu verwendende Element x, und damit sein Typ, bei der
Definition von xs schon fest.
• Damit ist xs monomorph.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
452
13 · Typinferenz
let-Polymorphie · 13.9
Inferenzregeln
für let...in
Typinferenz von m. Jedes freie Vorkommen
von x hat den gleichen Typ Tj .
··
·
x :: Tj
Typinferenz von e. Neue Typvariablen
für jedes freie Vorkommen von x.
··
·
e :: Ti
let x
let x = m in e :: Ti
I
Im let-Zweig wird x monomorph verwendet.
• Alle freien Vorkommen von x in m bekommen den gleichen Typ.
I
Typinferenz von m liefert Gleichung für Tj .
• Allquantifizieren der freien56 Typvariablen liefert polymorphen Typ von x.
I
Im in-Zweig wird x polymorph verwendet:
• Jedes freie Vorkommen von x in e wird mit neuen Typvariablen instanziiert.
I
Typinferenz von e liefert dann den Typ des gesamten let-Ausdrucks.
56 nicht
ausserhalb des let gebundenen, cf. Seite 450
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
453
13 · Typinferenz
let-Polymorphie · 13.9
Beispiel λf . let g = λy . f True in g g .
f :: T1
True :: Bool
@
T5
g :: T3
λy :: T4
T2
T0
I
I
I
g :: T7
g :: T8
T6
@
let g :: ∀α. α → T5
λf :: T1
Typinferenz von unten bis in den let-Zweig liefert g :: T4 → T5 .
Ausserhalb gebunden ist f :: Bool → T5 .
Damit ist T5 im Typ von g gebunden, nur T4 ist frei. Im in-Zweig ist
g :: ∀α. α → T5
I
Insgesamt liefert die Typinferenz dann
λf . let g = λy . f True in g g :: ∀τ. (Bool → τ ) → τ
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
454
13 · Typinferenz
let-Polymorphie · 13.9
Beispiel foo = λx. let f = (,) x in f f
(,) :: T4
x :: T1
@
f :: T6
f :: T3
f :: T7
T5
T2
T0
Mit (,) :: ∀α β. α → β → (α, β).
@
let f :: ∀α. α → (T8 , α)
λx :: T1
T4 = T8 → T9 → (T8 , T9 )
XT4 = T1 → T3
T0 = T1 → T2
XT8 = T1
T5 = T2
XT3 = T9 → (T8 , T9 )
T8 = T1
T3 = T9 → (T8 , T9 )
T3 = T9 → (T1 , T9 )
und
x :: T1 gebunden, also
T6 = T10 → (T1 , T10 )
XT6 = T7 → T5
T7 = T11 → (T1 , T11 )
XT10 = T7
T10 = T7
XT5 = (T1 , T10 )
T2 = (T1 , T10 )
XT2 = (T1 , T10 )
T0 = ... = T1 → (T1 , T11 → (T1 , T11 ))
Stefan Klinger · DBIS
f :: ∀α. α → (T1 , α)
⇒
foo :: ∀α β. α → (α, β → (α, β))
Informatik 2 · Sommer 2016
455
13 · Typinferenz
let-Polymorphie · 13.9
Beispiel λx. let c = λy . x in c c
x :: T1
c :: T3
c :: T6
λy :: T4
T2
T0
c :: T7
T5
@
let c
λx :: T1
T0 = T1 → T2
T5 = T2
T3 = T4 → T1
T3 = T4 → T1
und
also
c :: ∀α. α → T1
T6 = T8 → T1
XT6 = T7 → T5
T7 = T9 → T1
XT8 = T7
T8 = T7
XT5 = T1
T2 = T1
XT2 = T1
T0 = T1 → T2 = T1 → T1
Stefan Klinger · DBIS
T0 = T1 → T2
⇒
λx. let c = λy . x in c c :: ∀α. α → α
Informatik 2 · Sommer 2016
456
13 · Typinferenz
13.10
I
Literatur · 13.10
Literatur
Das in diesem Kapitel besprochene Typsystem ist als Hindley-Milner
Typsystem oder Hindley-Damas Typsystem bekannt57 .
• Luis Damas, Robin Milner. Principal Type-Schemes for Functional Programs.
1982.
I
Haskell verwendet ein ähnliches System mit vielen Erweiterungen,
insbesondere Typklassen.
• Philip Wadler, Stephen Blott. How to make Ad-hoc Polymorphism less Ad
Hoc. 16th Symposium on Principles of Programming Languages. Austin,
Texas, 1989. http://homepages.inf.ed.ac.uk/wadler/papers/class/class.ps.gz
I
Es gibt viele verschiedene Typsysteme, mit ganz unterschiedlichen
Ausprägungen. Einen Überblick verschafft
• Benjamin C. Pierce. Types and Programming Languages.
ISBN 0-262-16209-1. http://www.cis.upenn.edu/~bcpierce/tapl/
57 https://en.wikipedia.org/wiki/Hindley-Milner_type_system
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
457
13 · Typinferenz
13.11
Exkurs: Subtypen · 13.11
Exkurs: Subtypen
Dieser Abschnitt bezieht sich nicht auf Haskell, weil dieses keine Subtypen kennt.
Allgemein beschreibt die Subtyp-Relation S ≺ T eine Form der
Ersetzbarkeit: Ein Term des Typs S kann in jedem Kontext verwendet
werden, in dem ein Term vom Typ T erwartet wird.
e :: S
S ≺T
≺
e :: T
I
Dadurch können Werte verschiedene Typen annehmen (oben: e :: S und
e :: T , es handelt sich also um eine Art der Polymorphie.
I
Polymorphie in objekt-orientierten Sprachen bezieht sich meist auf
Klassenhierarchien:
Amsel ≺ Vogel ≺ Tier
Unsere parametrische Polymorphie wird dort eher generische Programmierung genannt.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
458
13 · Typinferenz
I
Exkurs: Subtypen · 13.11
Obacht: “Subtyp” bedeutet nicht einfach nur “Teilmenge”:
N ⊂ Q 6 ⇒ Int ≺ Double
Eine Methode foo(Double x) führt evtl. eine Division auf x aus, die für
Integer nicht unbedingt definiert ist.
I
Vielmehr ist gemeint dass der Subtyp spezieller sein muss, und
insbesondere alle Fähigkeiten des Obertyps besitzt.
Das Liskov Substitutionsprinzip ist eine strenge Formulierung für
Subtypen (die nicht von jeder Programmiersprache umgesetzt wird):
Sei φ eine für alle Werte des Typ T beweisbare Eigenschaft, dann muss
diese auch für alle Werte aller Subtypen von T gelten:
(x :: T ⇒ φ x) ⇒ (y :: S ∧ S ≺ T ⇒ φ y )
Grob: Wenn S ≺ T , dann können in einem Programm alle Objekte vom Typ T
durch Objekte vom Typ S ersetzt werden, ohne das Programm zu verändern.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
459
13 · Typinferenz
Exkurs: Subtypen · 13.11
Ko- und Kontravarianz
Sei S ≺ T . Wie verhalten sich dann aus S und T konstruierte Typen
zueinander?
I
Sei K ein einstelliger Typkonstruktor. K heißt
kovariant wenn K S ≺ K T , also die Richtung erhalten bleibt,
kontravariant K S K T , sich also die Richtung umkehrt,
invariant sonst (≺ ist eine partielle Ordnung auf Typen, die Beziehung
kann also auch in keiner der beiden Richtungen bestehen).
I
Für mehrstellige Typkonstruktoren betrachtet man Varianz für jede Stelle
einzeln (cf. Seite 462).
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
460
13 · Typinferenz
Exkurs: Subtypen · 13.11
Beispiel: Arrays
Seien String ≺ Object, Integer ≺ Object. Wie verhalten sich Arrays?
I
Kovariant: String[] ≺ Object[] ?
(Java macht das so)
• Eine Funktion die aus einem Object[] liest, wird auch mit einem String
zufrieden sein, weil String ≺ Object.
• Was passiert hier:
1
2
3
I
String[] strings = new String[12];
Object[] objects = strings;
objects[0] = new Integer(1);
Kontravariant: Object[] ≺ String[] ?
• Das macht keinen Sinn: Jede Funktion die aus einem String[] liest, müsste
mit einem beliebigen Object klar kommen.
⇒ Arrays sollten invariant sein. Java ist an dieser Stelle nicht typsicher!
Allgemein können read-only Datenstrukturen kovariant sein, write-only
Datenstrukturen können kontravariant sein, read-write Datenstrukturen
sollten invariant sein.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
461
13 · Typinferenz
Exkurs: Subtypen · 13.11
Beispiel: Funktionen
f :: Df → Rf
und
g :: Dg → Rg
Frage Wann ist es typsicher eine Funktion f durch eine Funktion g zu
ersetzen, wann gilt also für die Funktionstypen
Dg → Rg
?
≺ Df → R f
Antwort Wenn g einen allgemeineren Typ akzeptiert, und einen
spezielleren Typ zurückgibt, also:
Dg Df
I
und
Rg ≺ Rf
Der Typkonstruktor → ist also kontravariant im
ersten, und kovariant im zweiten Argument.
Erinnerung: Wir übergeben (schreiben) das Argument, und
lesen das Ergebnis.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
D1 → R1
g f f
D2 → R2
462
14
Monadische Berechnungen
Dieses Kapitel basiert größtenteils auf dem hervorragenden Artikel
Typeclassopedia58 .
58 Brent
Yorgey. Typeclassopedia. https://wiki.haskell.org/Typeclassopedia
14 · Monadische Berechnungen
14.1
Referenzielle Transparenz · 14.1
Referenzielle Transparenz
I
Reine FPLs bieten per Definition keine Seiteneffekte, z.B. keine
veränderbaren Variablen.
I
Die damit verbundenen Probleme bzgl. I/O haben wir am Anfang der
Vorlesung bereits angesprochen (Stichwort “Referenzielle Transparenz”):
1
(putStr "foo", putStr "bar")
Die Reihenfolge der Auswertung der
Teilausdrücke ist irrelevant.
1
getChar == getChar
random == random
getClockTime == getClockTime
Der Wert eines Ausdrucks hängt
nur von den Werten seiner
Teilausdrücke ab.
2
3
I
Eine der herausragenden Eigenschaften von Haskell im Vergleich zu
anderen FPLs ist die funktional saubere Integration von IO, und
anderen effektbehafteten Berechnungen.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
464
14 · Monadische Berechnungen
Referenzielle Transparenz · 14.1
Lösungsvorschlag: Zustand der Welt
I
Eine “Variable” world :: World könnte den Zustand der Welt
(Terminal, Festplatte, Netzwerkverbindungen, ...) beinhalten.
I
Referenzielle Transparenz verbietet das Verändern von Variablen.
Daher modellieren wir die Welt als zusätzliche Parameter der
entsprechenden Funktionen.
Beispiel Für IO mit einzelnen Zeichen könnte das so aussehen:
1
2
putChar :: Char → World → World
getChar :: World → (World, Char)
Anwendung auf ein Terminal, auf dem schon der Text “hell” steht
I
1
putChar ’o’ hell_
_ hello_
Angenommen auf stdin wartet der String "xy" darauf gelesen zu
werden:
I
1
2
getChar "xy" _ ( "y" , ’x’)
getChar "y" _ ( "" , ’y’)
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
465
14 · Monadische Berechnungen
Referenzielle Transparenz · 14.1
Beispiel Damit ließen sich auch komplexere Funktionen bauen:
1
2
3
4
5
6
getString :: World → (World,String)
getString w0
| stdin w0 = let (w1,c) = getChar w0
(w2,cs) = getString w1
in (w2, c:cs)
| otherwise = (w0,[])
I
-- stdin :: World -> Bool
Dabei ist das explizite Weiterreichen der Welt arg umständlich — und
fehleranfällig.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
466
14 · Monadische Berechnungen
Referenzielle Transparenz · 14.1
~
Problem Das explizite Anwenden einer Funktion auf einen Zustand
kann zu Inkonsistenzen führen:
1
clash w = ( putString "good" w , putString "evil" w )
Es kann nur eine Welt geben59 (das Terminal kann sich nicht
verdoppeln). Der Zustand der Welt (world) ist nicht mehr konsistent,
im besten Fall ist das Verhalten des Programmes undefiniert.
Deswegen suchen wir eine andere Lösung
und dazu müssen wir ein wenig ausholen...
59 Wir
ignorieren hier die theoretische Physik, u.a. David Deutsch
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
467
14 · Monadische Berechnungen
14.2
Typklassen · 14.2
Typklassen
Erinnerung
wie man...
Im Kapitel über Typklassen (cf. Seite 299) haben wir gelernt
...Klassen definiert
I
1
2
3
class Eq α where
(==) :: α → α → Bool
(/=) :: α → α → Bool
• Hier steht α für einen noch unbekannten Typen.
...und wie man einen Typ zur Instanz dieser Klasse erklärt:
I
1
2
3
instance Eq Frac where
(x1 :/ y1) == (x2 :/ y2) = x1 * y2 == x2 * y1
a
/= b
= not $ a == b
— (==) :: Frac → Frac → Bool
— (/=) :: Frac → Frac → Bool
• Hier wird α an Frac gebunden, und die Typen der Funktionen entsprechend
konkretisiert.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
468
14 · Monadische Berechnungen
Typklassen · 14.2
Typkonstruktoren
Neu ist, dass man in Klassendefinitionen nicht nur von einem Typ, sondern
sogar von einem Typkonstruktor abstrahieren kann:
I
Bei der Definition der Klasse taucht dann die Variable t im Typ der
Funktionen immer mit einem Argument auf, das den Typ vervollständigt:
class ClassName t where
f :: t α
g :: α → t α
I
Bei der Instanziierung eines Konstruktors C zu dieser Klasse werden die
Typen der Funktionen entsprechend konkretisiert:
instance ClassName C where
f = ...
— :: C α
g = ...
— :: α → C α
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
469
14 · Monadische Berechnungen
Typklassen · 14.2
Beispiel Benutzer eines aktuellen GHCi (ab Version 7.10) haben das
schon gesehen:
1
2
Prelude> :t foldr
foldr :: Foldable t ⇒ (α → β → β) → β → t α → β
3
4
5
6
7
8
9
10
11
12
Prelude> :i Foldable
class Foldable t where
...
foldr :: (α → β → β) → β → t α → β
foldl :: (β → α → β) → β → t α → β
null :: t α → Bool
length :: t α → Int
elem :: Eq α ⇒ α → t α → Bool
...
In der Prelude ist dann schon definiert:
1
2
3
4
instance Foldable [] where — hier nur der Typkonstruktor [] für Listen
...
foldr = ...
— :: (α → β → β) → β → [α] → β
...
I
Statt auf die Klasse Foldable einzugehen, besprechen wir eine
wesentlich einfachere...
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
470
14 · Monadische Berechnungen
14.3
1
2
Functor · 14.3
Functor
(dt. Funktor)
class Functor f where
fmap :: (α → β) → f α → f β
I
An den f α und f β sehen wir, dass f kein Typ, sondern ein
Typkonstruktor sein muss.
I
fmap nimmt eine Funktion von α nach β, und liefert ein Funktion die
f α nach f β abbildet.
fmap :: (α → β) → (f α → f β)
• Fasst man f α als Container für Werte vom Typ α auf, dann wird also
fmap g die Funktion g auf die Elemente des Containers anwenden.
• Abstrakter kann f auch einen Kontext repräsentieren, in dem die Funktion g
verwendet wird.
Das klingt sehr vage, bringt uns aber später mehr: Wir legen uns nicht auf
Container fest.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
471
14 · Monadische Berechnungen
Functor · 14.3
Beispiele die den Functor als Container auffassen
Listen
I
1
2
3
instance Functor [] where
fmap _ []
= []
fmap g (x:xs) = g x : fmap g xs
4
5
6
7
8
*Main> fmap length ["hello","world","how","are","you"]
[5,5,3,3,3]
*Main> fmap length []
[]
• Tatsächlich ist fmap für Listen das gleiche wie map.
Maybe
I
1
2
3
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap g (Just x) = Just (g x)
4
5
6
7
8
*Main> fmap length Nothing
Nothing
*Main> fmap length $ Just "foo"
Just 3
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
472
14 · Monadische Berechnungen
Functor · 14.3
Was macht einen Functor aus?
I
I
Meist kann man fmap einfach implementieren.
Aber das genügt noch nicht, um ein “echter” Functor im
mathematischen Sinn zu sein.
• Ganz analog dazu, weshalb data Frac = Int :/ Int deriving Eq zwar
“irgendwie geht”, aber nicht “richtig” ist.
Gesetze Für einen Functor müssen folgende Eigenschaften gelten:
1. fmap id ≡ id
2. fmap (g ◦ h) ≡ fmap g ◦ fmap h
Die Intuition dabei: fmap g verändert nur die Elemente des Containers,
nicht den Container selbst.
Anmerkungen
Man kann zeigen, dass für jeden Typkonstruktor f höchstens eine
Instanz von Functor existiert, die diese Gesetze erfüllt.
I Ebenfalls gilt: Das erste Gesetz impliziert schon das zweite.
I
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
473
14 · Monadische Berechnungen
Functor · 14.3
Kein Functor
Vorsicht Folgender Code ist typkorrekt (kompiliert, kann verwendet
werden), ist aber trotzdem kein richtiger Functor. Warum?
1
2
3
instance Functor [] where
fmap _ [] = []
fmap g (x:xs) = g x : g x : fmap g xs
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
474
14 · Monadische Berechnungen
Functor · 14.3
Value :: Type, Type :: Kind
Die Art des Typs
Man kann für einen Typ(konstruktor) sagen wieviele Argumente er
braucht, um zu einem “echten” Typ zu werden. Das ist sozusagen der Typ
des Typs, engl.: the kind of type.
I Alles was Werte enthält bekommt den Kind ∗, das sind die Konstanten
auf Typebene.
Int :: ∗, [Char] :: ∗, Int → Int :: ∗
I
Konstruktoren mit einem Typparameter haben den Kind ∗ → ∗, das sind
Funktionen auf Typebene mit einem (Typ-)Argument.
[ ] :: ∗ → ∗,
I
Konstruktoren mit 2 Typparametern haben dann den Kind ∗ → (∗ → ∗).
Either :: ∗ → ∗ → ∗,
I
Maybe :: ∗ → ∗
(,) :: ∗ → ∗ → ∗,
Es geht sogar:
1
1
2
3
Prelude> data Bar t = B (t Int)
Prelude> :k Bar
Bar :: (* -> *) -> *
Stefan Klinger · DBIS
2
3
4
(→) :: ∗ → ∗ → ∗
Prelude> :t B (Just 4)
B (Just 4) :: Bar Maybe
Prelude> :t B [4]
B [4] :: Bar []
Informatik 2 · Sommer 2016
475
14 · Monadische Berechnungen
Functor · 14.3
Partiell angewandte Typkonstruktoren
Typfehler auf Typebene
Natürlich kann man einen Typkonstruktor auch partiell anwenden:
1
2
3
4
5
6
Prelude> :k Either
Either :: * -> * -> *
Prelude> :k Either Int
Either Int :: * -> *
Prelude> :k Either Int Bool
Either Int Bool :: *
1
2
3
4
5
6
Prelude>
(,) :: *
Prelude>
(,) (Int
Prelude>
(,) (Int
:k
->
:k
->
:k
->
(,)
* -> *
(,) (Int -> Bool)
Bool) :: * -> *
(,) (Int -> Bool) [Char]
Bool) [Char] :: *
Bei der Klassendefinition wird der Kind des Parameters festgelegt:
I
1
2
3
Prelude> :i Functor
— Ältere GHC-Versionen zeigen den Kind nicht an.
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
Bei der Instanziierung muss der Typ dann den gleichen Kind haben:
I
1
2
3
4
5
*Main> instance Functor Either where fmap = undefined
<interactive>:7:18:
Expecting one more argument to ‘Either’
The first argument of ‘Functor’ should have kind ‘* -> *’,
but ‘Either’ has kind ‘* -> * -> *’
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
476
14 · Monadische Berechnungen
Functor · 14.3
Fazit Für Functor brauchen wir einen Typ vom Kind ∗ → ∗, also einen
Typkonstruktor dem noch genau ein Typparameter fehlt.
Beispiele
1
2
3
instance Functor (Either τ ) where
fmap f (Left l) = Left l
fmap f (Right x) = Right $ f x
— Dem Either τ fehlt noch der Typ für Right.
4
5
6
instance Functor ((,) τ ) where
fmap f (x,y) = (x, f y)
— eigentlich: Functor (τ ,) where
Frage Was ist der Typ von fmap bei diesen Instanziierungen?
Erinnerung: class Functor f where fmap :: (α→ β) → f α → f β
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
477
14 · Monadische Berechnungen
Functor · 14.3
Functor ohne Container
Man kann sich vorstellen dass fmap eine Funktion innerhalb eines Kontexts
f anwendet, also in den Kontext hebt (engl. lifting), ohne den Kontext
zu verändern.
~
Beispiele Der Kontext muss dabei kein Container sein.
1
2
instance Functor ((→) τ ) where
fmap = ... — wie geht das?
— eigentlich: instance Functor (τ →) where
Dabei ist (τ →) der Typkonstruktor, welcher Funktionen von τ konstruiert.
Frage 1 Was ist der Typ von fmap bei dieser Instanziierung?
Erinnerung: class Functor f where fmap :: (α→ β) → f α → f β
Frage 2 Was also ist fmap?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
478
14 · Monadische Berechnungen
Functor · 14.3
Antwort 1 fmap :: (α → β) → (τ → α) → (τ → β)
Das erhält man durch einfaches Einsetzen von (τ →) für f in die
Deklaration von fmap.
Antwort 2 Dieser Typ sollte bekannt vorkommen: Die Komposition.
I Setzen fmap = (◦)
1
2
I
instance Functor ((→) τ ) where
fmap = (.)
Prüfen die Funktor-Eigenschaft: Für alle Funktionen g gilt
fmap id g _ id ◦ g _ λx. id (g x) _ λx. g x ] g ≡ id g
β
β
β
η
Anwendung
1
2
3
4
5
6
*Main> :t length
length :: [a] → Int
*Main> :t (<3)
(<3) :: Int -> Bool
*Main> :t fmap (<3) length
fmap (<3) length :: [a] → Bool
Stefan Klinger · DBIS
I
Aus [α] → Int wird [α] → Bool.
I
In einer Funktion auf [α] haben wir
den Ergebnistyp von Int in Bool
verwandelt.
Informatik 2 · Sommer 2016
479
14 · Monadische Berechnungen
Functor · 14.3
Mehr Argumente
I
1
2
Bisher hatten wir Funktionen mit einem Argument über einen Functor
gemappt:
*Main> :t even — hier mit einfacherem Typ
even :: Int → Bool
3
4
5
I
Wichtig war der Inhalt der
Struktur (Int), der als Argument
zur Funktion even passen muss.
I
Die Struktur selbst war nicht
weiter wichtig:
Maybe, [·], ([α] →), ...
*Main> :t fmap even $ Just 42
fmap even $ Just 42 :: Maybe Bool
6
7
8
*Main> :t fmap even [1..]
fmap even [1..] :: [Bool]
9
10
11
*Main> :t fmap even length
fmap even length :: [α] → Bool
Frage Welche Typen sind bei Funktionen mit mehreren Argumenten zu
erwarten? Beispiel:
(unter der Annahme, dass (<) :: Int → Int → Bool)
fmap (<) [1, 2, 3]
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
480
14 · Monadische Berechnungen
1
2
Functor · 14.3
*Main> :t (<)
(<) :: Int → Int → Bool
3
4
5
*Main> :t fmap (<)
fmap (<) :: Functor f ⇒ f Int → f (Int → Bool)
6
7
8
*Main> :t fmap (<) $ Just 42
fmap (<) $ Just 42 :: Maybe (Int → Bool)
9
10
11
*Main> :t fmap (<) [1..]
fmap (<) [1..] :: [Int → Bool]
12
13
14
*Main> :t fmap (<) length
fmap (<) length :: [α] → Int → Bool
I
Die Strukturen enthalten Funktionen. Also gerade das, was (+)
angewendet auf einen Int liefert.
I
Das ist nicht weiter überraschend, aber ekelhaft, denn. . .
Offensichtliche Frage Wie wenden wir etwas vom Typ f (α → β) auf etwas
vom Typ f α an? Wie können wir so eine Funktion verwenden?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
481
14 · Monadische Berechnungen
14.4
Applicative Functor · 14.4
Applicative Functor
(dt. Applikativer Funktor)
Brauchen eine Klasse um für jede Struktur eine eigene Antwort zu finden:
1
2
3
class Functor f ⇒ Applicative f where
pure :: α → f α
(<*>) :: f (α → β) → f α → f β
Beobachtungen
— infixl 4; schreiben ~ in Formeln
Applicative60 ist offenbar ein besonderer Functor.
I
Mit pure kann man jeden Wert in die Struktur f heben.
I
Der Typ der “Applikation” ~ ist sehr ähnlich zu $ :: (α → β) → α → β.
I
Jede Implementierung von ~ beantwortet die “offensichtliche Frage”
(cf. letzte Folie) für den instanziierten Typ.
⇒ Für einen applikativen Funktor ist diese Frage also geklärt!
60 Vor
GHCi 7.10.2 muss das Modul Control.Applicative importiert werden
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
482
14 · Monadische Berechnungen
Applicative Functor · 14.4
Beispiel Maybe
1
instance Applicative Maybe where
2
3
pure = Just
4
5
6
7
8
9
10
11
12
Just f <*> u = fmap f u
Nothing <*> _ = Nothing
*Main> pure even <*> Just 23
Just False
*Main> Just (<) <*> Just 23 <*> Just 42
Just True
*Main> Just (<) <*> Nothing <*> Just 42
Nothing
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
483
14 · Monadische Berechnungen
Applicative Functor · 14.4
Beispiel Either τ ist auf ganz ähnliche Weise Applicative wie Maybe:
1
instance Applicative (Either τ ) where
2
3
pure = Right
I
Beide stellen “Berechnungen”
dar, die fehlschlagen können.
Right f <*> x = fmap f x
Left l <*> _ = Left l
I
Either τ kann noch eine
“Fehlermeldung” vom Typ τ
tragen, wo Maybe nur
Nothing liefert.
4
5
6
7
8
9
10
11
*Main> Right (+) <*> Right 42 <*> Right 23
Right 65
*Main> Right (+) <*> Left "err" <*> Right 23
Left "error"
Frage Wie könnten Listen zur Instanz von Applicative erklärt werden?
Mit anderen Worten: Für f = [ ] suchen wir Implementationen von
pure :: α → f α
(~) :: f (α → β) → f α → f β
Was soll das sein, [(+2), (+3), (+4)] ~ [5, 6] ?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
484
14 · Monadische Berechnungen
Applicative Functor · 14.4
Plan A: Jeder mit jedem
I
1
2
3
Die Comprehension Strategie
Jede Funktion aus der linken Liste wird auf jedes Element aus der rechten
Liste angewandt.
instance Applicative [] where
pure x = [x]
gs <*> xs = [ g x | g <- gs, x <- xs ]
4
5
6
7
8
9
10
11
12
*Main> [(+2),(+3),(+4)] <*> [5,6]
[7,8,8,9,9,10]
*Main> pure (*10) <*> [1..5]
[10,20,30,40,50]
*Main> pure (+) <*> [1..5] <*> pure 10
[11,12,13,14,15]
*Main> pure (+) <*> [1..5] <*> [10,20,30]
[11,21,31,12,22,32,13,23,33,14,24,34,15,25,35]
I
Das implementiert “nicht-deterministische” Berechnungen:
• Die Funktion + wird auf zwei Argumente angewendet, deren Wert nur
ungefähr bekannt ist.
• Das erste Argument hat einen der Werte 1–5, das zweite einen der Werte 10,
20 oder 30.
I
Auf diese Weise sind Listen standardmäßig als Applicative instanziiert.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
485
14 · Monadische Berechnungen
Applicative Functor · 14.4
Plan B: Jeder nur ein Kreuz
Die ZipList Strategie
Eine andere Antwort auf die Frage: Was ist [(+2), (+3), (+4)] ~ [5, 6] ?
I
1
2
3
Jede Funktion aus der linken Liste wird dem “entsprechenden” Element
aus der rechten Liste zugeordnet. ⇒ zipWith ($).
instance Applicative [] where
pure = repeat
gs <*> xs = zipWith ($) gs xs
4
5
6
7
8
9
10
11
12
*Main> [(+2),(+3),(+4)] <*> [5,6]
[7,9]
*Main> pure (*10) <*> [1..5]
[10,20,30,40,50]
*Main> pure (+) <*> [1..5] <*> pure 10
[11,12,13,14,15]
*Main> pure (+) <*> [1..5] <*> [10,20,30]
[11,22,33]
Frage Welchen Effekt hat die Implementierung von pure?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
486
14 · Monadische Berechnungen
Applicative Functor · 14.4
Die Intuition hinter Applicative
Ganz abstrakt soll Applicative eine “Berechnung mit Effekt” auf funktional
saubere Weise kapseln. Dabei ist der “Effekt” in f versteckt.
I
Langfristiges Ziel: Das Kapseln von Seiteneffekten ohne die referenzielle
Transparenz zu zerstören.
I
Wie abstrakt geht das denn? Oder: Wie unabhängig von der Struktur
können wir Berechnungen formulieren?
Frage Was ist das?
(zur Erinnerung die Typen von ~ und pure anschauen)
pure (<) ~ pure 23 ~ pure 42
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
487
14 · Monadische Berechnungen
Applicative Functor · 14.4
Antwort Das ist die Berechnung von (<) 23 42, allerdings in einer noch
unbekannten Struktur:
1
2
3
4
*Main> pure (<) <*> pure 23 <*> pure 42
• Ambiguous type variable ‘f0’ arising from a use of ‘print’
prevents the constraint ‘(Show (f0 Bool))’ from being solved.
Probable fix: use a type annotation to specify what ‘f0’ should be.
...die Struktur ist tatsächlich unbekannt.
1
2
*Main> pure (<) <*> pure 23 <*> pure 42
Just True
::
Maybe Bool
*Main> pure (<) <*> pure 23 <*> pure 42
[True]
::
[Bool]
*Main> pure (<) <*> pure 23 <*> pure 42
Right True
::
Either Double Bool
*Main> pure (<) <*> pure 23 <*> pure 42
True — Was war hier der Funktor?
$
3
4
5
6
7
8
9
10
11
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
"wasauchimmer"
488
14 · Monadische Berechnungen
Anmerkung
Applicative Functor · 14.4
zur Fehlermeldung auf der vorigen Folie:
I Im interaktiven GHCi verhindert evtl. Type Defaulting (cf. Seite 330) daß wir eine
Fehlermeldung sehen, dabei wird f = IO gesetzt, cf. Seite 491.
I Sie Sehen den Fehler im GHCi, wenn Sie alle bisher besprochenen Klassen selbst
implementieren.
I Auch das Laden einer Datei mit der Definition
1
x = pure (<) <*> pure 23 <*> pure 42
— Ohne einen Typ anzugeben!
liefert aber die erwartete Fehlermeldung:
1
2
3
4
5
6
7
8
9
10
11
• Ambiguous type variable ‘f0’ arising from a use of ‘<*>’
prevents the constraint ‘(Applicative f0)’ from being solved.
Relevant bindings include
foo :: f0 Bool (bound at /tmp/foo.lhs:1:3)
Probable fix: use a type annotation to specify what ‘f0’ should be.
These potential instances exist:
instance Applicative IO -- Defined in ‘GHC.Base’
instance Applicative Maybe -- Defined in ‘GHC.Base’
instance Applicative ((->) a) -- Defined in ‘GHC.Base’
instance Monoid a => Applicative ((,) a) -- Defined in ‘GHC.Base’
instance Applicative [] -- Defined in ‘GHC.Base’
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
489
14 · Monadische Berechnungen
14.5
I
Ausblick: IO · 14.5
Ausblick: IO
Fest mit Compiler und Interpreter verwoben ist der Typkonstruktor
IO :: ∗ → ∗
• Tatsächlich kapselt IO den Zustand der Welt.
• Auf diesen Zustand haben wir keinen Zugriff!
I
Ein Wert vom Typ IO α stellt eine Berechnung mit Effekt dar, die ein
α produziert.
Bildlich: Ein IO α ist eine “Maschine” die ein α produziert.
Beispiel einen String auf das Terminal schreiben:
putStr :: String → IO ()
1
2
3
4
5
6
Prelude> :t putStr "foo"
putStr "foo" :: IO ()
— Das ist also eine “Maschine” die ein () produziert.
Prelude> putStr "foo"
fooPrelude>
— Seiteneffekt: Vor dem Prompt wurde foo auf’s Terminal geschrieben.
Prelude> it
()
— Das Ergebnis der letzten Berechnung war ().
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
490
14 · Monadische Berechnungen
Ausblick: IO · 14.5
Der GHCi und IO
Für jede Eingabe bestimmt der GHCi zunächste den Typ.
Ist der Typ nicht IO α, dann wird das Ergebnis (mittels show) angezeigt.
I
1
2
I
Prelude> "hello world"
"hello world"
Ist der Typ IO α, dann wird die IO-Aktion mit ihren Seiteneffekten
ausgeführt. Das Ergebnis wird an die Variable it :: α gebunden.
• Das Ergebnis wird nicht angezeigt, falls α = (), oder keine Instanz von Show.
1
writeFile :: FilePath -> String -> IO () — Schreibe String in Datei
2
3
4
5
Prelude> writeFile "foo.txt" "hello outside world"
Prelude> it
()
• Sonst wird der Wert von it mittels show angezeigt.
1
readFile :: FilePath -> IO String — Lese String aus Datei
2
3
4
5
6
Prelude> readFile "foo.txt" — readFile :: FilePath → IO String
"hello outside world"
Prelude> it
"hello outside world"
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
491
14 · Monadische Berechnungen
Obacht
1
Ausblick: IO · 14.5
Wir können immer nur eine IO-Operation ausführen lassen:
Prelude> (putStrLn "foo", putStrLn "bar")
2
3
4
5
<interactive>:33:1: error:
• No instance for (Show (IO ())) arising from a use of ‘print’
• In a stmt of an interactive GHCi command: print it
Es handelt sich nicht um eine IO-Operation, sondern um zwei.
Sozusagen zwei Maschinen die etwas tun können, aber wir haben nicht
festgelegt welche wir ausführen wollen.
I
6
7
Prelude> :t (putStr "foo", putStr "bar")
(putStr "foo", putStr "bar") :: (IO (), IO ())
I
Insbesondere ist der Typ nicht IO α, es wird also nichts ausgeführt (cf.
vorige Folie).
I
Analog dazu haben kompilierbare Programme eine Funktion
main :: IO ().
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
492
14 · Monadische Berechnungen
Ausblick: IO · 14.5
Vordefinierte IO-Operationen
I
putStr, putStrLn :: String → IO ()
• Schreiben den übergebenen String auf die Standardausgabe.
• putStrLn fügt einen Zeilenumbruch an.
I
getChar :: IO Char
• Liest das nächste Zeichen von der Standardeingabe, und gibt es zurück.
I
getLine, getContents :: IO String
• getLine liest die nächste Zeile,
• getContents die gesamte Standardeingabe.
I
writeFile :: FilePath → String → IO ()
• Schreibt den übergebenen String in die genannte Datei.
I
readFile :: FilePath → IO String
• Liest die genannte Datei, und gibt den Inhalt als String zurück.
• Sollte nur auf Plain Text Dateien angewendet werden.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
493
14 · Monadische Berechnungen
Ausblick: IO · 14.5
IO ist ein Functor
1
2
3
Prelude> :i IO
...
instance Functor IO -- Defined in ‘GHC.Base’
Damit können wir offensichtlich “gelesene” Daten verarbeiten:
I
1
2
fileSize :: FilePath -> IO Int
fileSize path = fmap length $ readFile path
3
4
5
Prelude> fileSize "foo.txt"
19
fileSize ist also eine Funktion, die einen Dateinamen konsumiert, und
eine Maschine zurückgibt die bei Ausführung einen Int liefert.
Weiteres Beispiel:
I
1
2
3
Prelude> filter (< 'd') ‘fmap‘ getLine
abracadabra
— das ist meine Eingabe
"abacaaba"
— das Ergebnis der Berechnung wird mit show angezeigt
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
494
14 · Monadische Berechnungen
Ausblick: IO · 14.5
IO ist ein Applicative Functor
Aufgabe Schreibe eine Funktion die den Inhalt zweier Dateien
konkateniert:
I
Konkatenieren ist einfach: (++) :: [α] → [α] → [α].
I
Was passiert wenn wir das in den IO-Functor heben?
1
2
I
Prelude> :t
(++) ‘fmap‘ readFile "foo.txt"
(++) ‘fmap‘ readFile "foo.txt" :: IO ([Char] -> [Char])
An dieser Stelle kommen wir mit Functor alleine nicht weiter!
• Wir brauchen eine Möglichkeit die Funktion, die von der Maschine mit Typ
IO ([Char] → [Char]) produziert wir, anzuwenden.
• Warum gibt es keine Funktion :: IO α → α ?
Lösung
1
2
Tatsächlich ist IO auch Instanz von Applicative:
Prelude> pure (++) <*> readFile "foo.txt" <*> readFile "bar.txt"
"Hello outside world!\nThis is from a file\n"
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
495
14 · Monadische Berechnungen
14.6
Gesetze für Applicative · 14.6
Gesetze für Applicative
Damit die Intuition funktioniert, muss Folgendes gelten:
1. pure id ~ v ≡ v
Verträglichkeit mit der Identität
• Wenn man die Identität in einen Kontext hebt, und sie dann auf einen Wert
im Kontext anwendet, darf sich dieser nicht ändern.
2. pure g ~ pure x ≡ pure (g x)
Homomorphismus61
• Eine Berechnung “im Kontext” muss sich genau so verhalten wie die
Berechnung außerhalb, deren Ergebnis man in den Kontext hebt.
3. u ~ pure y ≡ pure ($ y ) ~ u
Austauschbarkeit
• Bei Anwendung einer “Berechnung mit Effekt” u auf ein reines Argument y
spielt keine Rolle in welcher Reihenfolge u und y ausgewertet werden.
4. u ~ (v ~ w ) ≡ pure (◦) ~ u ~ v ~ w
Komposition
• Auch Komposition verträgt sich mit Lifting: Vergleiche: a (b c) ≡ (◦) a b c.
5. fmap g u ≡ pure g ~ u
Das Verhältnis zu Functor
• Die ersten vier Gesetze implizieren dieses (ohne Beweis).
61 Verträglichkeit
Stefan Klinger · DBIS
mit der Anwendung
Informatik 2 · Sommer 2016
496
14 · Monadische Berechnungen
Gesetze für Applicative · 14.6
Anmerkungen
I
Man kann fmap in zwei primitivere Operationen zerlegen: Lifting, und
Anwendung innerhalb des Kontexts.
• Die Gleichung fmap g u ≡ pure g ~ u kann man also auch als Definition
von fmap durch pure und ~ lesen.
• Tatsächlich ist lassen sich beide Functor-Gesetze aus den
Applicative-Gesetzen beweisen.
⇒ Hat man schon eine valide Instanziierung zu Applicative, dann geht auch:
1
2
3
4
I
instance
instance
instance
instance
Functor
Functor
Functor
Functor
Maybe
(Either t)
[]
((->) t)
where
where
where
where
fmap
fmap
fmap
fmap
g
g
g
g
u
u
u
u
=
=
=
=
pure
pure
pure
pure
g
g
g
g
<*>
<*>
<*>
<*>
u
u
u
u
Oft gibt es zu einer Implementation von ~ nur eine Implementation
von pure welche die genannten Gesetze erfüllt.
• Zwar gibt es oft mehrere Möglichkeiten einen Functor Applicative zu
machen (Comprehension, cf. Seite 485 und ZipList, cf. Seite 486).
• Aber für diese beiden Instanziierungen von Listen zu Applicative hatten wir
jeweils keine andere Wahl für pure.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
497
14 · Monadische Berechnungen
Gesetze für Applicative · 14.6
Kanonische Form
Übung
Die drei Gesetze
I
pure g ~ pure x ≡ pure (g x)
I
u ~ pure y ≡ pure ($ y ) ~ u
I
u ~ (v ~ w ) ≡ pure (◦) ~ u ~ v ~ w
Homomorphismus
Austauschbarkeit
Komposition
beschreiben einen Algorithmus, wie man jeden Ausdruck mit pure und ~
so umschreiben kann, dass er die kanonische Form
pure g ~ u1 ~ u2 ~ ... ~ un
hat, in der pure nur einmal vorkommt, und die ~ linkstief geschachtelt sind.
(Erinnerung: ~ ist links-assiziativ)
Intuition Berechnungen mit Effekt haben eine “reine” (pure) Struktur die
durch g gegeben ist, plus eine Reihe von “effektbehafteten”
Unter-Berechnungen in den ui .
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
498
14 · Monadische Berechnungen
Gesetze für Applicative · 14.6
Notation Weil fmap g u ≡ pure g ~ u gilt (cf. Seite 496, 5. Gesetz),
definiert man noch
1
2
3
infixl 4 <$>
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<$>) = fmap
Damit können applikative Berechnung immer in die Form
g h$i u1 ~ u2 ~ ... ~ un
gebracht werden. Dabei sind
I
I
I
die ui :: f αi (für den gleichen Functor f ), und
g :: α1 → α2 → ... → αn → β.
Der ganze Ausdruck ist vom Typ f β.
Prima! Offenbar können wir mehrere IO-Operationen an ein rein
funktionales Programm “anflanschen”.
Nochmal das Beispiel von Folie 483:
1
2
Prelude> (++) <$> readFile "foo.txt" <*> readFile "bar.txt"
"Hello outside world!\nThis is from a file\n"
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
499
14 · Monadische Berechnungen
Gesetze für Applicative · 14.6
nicht-applikative Funktoren
I
Jede Instanz von Applicative bildet notwendigerweise einen Functor,
fmap g u ≡ pure g ~ u (cf. Seite 497).
I
Umgekehrt gilt das nicht!
Das bedeutet: Es gibt Funktoren die sich nicht ohne weiteres62 applikativ
verwenden lassen!
Beispiel Erinnerung: ((,) τ ) ist ein Functor:
1
2
instance Functor ((,) τ ) where
fmap f (x,y) = (x, f y)
I
I
— fmap :: (α → β) → (τ , α) → (τ , β)
Brauchen: pure :: α → (τ, α).
Aber woher soll man den Wert für τ nehmen?
• Ein Ansatz wäre pure x = (⊥, x), aber das kollidiert mit den Gesetzen die für
Applicative gelten sollen, cf. Seite 496 (auch eine schöne Übung).
62 “Weiteres”
nächste Woche im PK2.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
500
14 · Monadische Berechnungen
Gesetze für Applicative · 14.6
Applicative ohne Container
Erinnerung
1
2
instance Functor ((→) τ ) where
fmap = (.)
3
4
5
Der “Kontext” ist hier etwas das einen
Wert vom Typ τ = [α] bereitstellt.
6
7
8
*Main> :t length
length :: [α] → Int
*Main> :t (<3)
(<3) :: Int → Bool
*Main> :t fmap (<3) length
fmap (<3) length :: [α] → Bool
Was brauchen wir für eine Instanziierung Applicative ((→) τ ) ?
1. Was sind die benötigten Typen von pure und ~?
2. Wie können wir die implementieren?
(Tip: “let the types guide you”)
3. Gelten die Gesetze, cf. Seite 496?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
501
14 · Monadische Berechnungen
Gesetze für Applicative · 14.6
Das sind alte Bekannte: Die Schönfinkel-Kombinatoren63
1
2
instance Functor ((→) τ ) where
fmap = (.)
— nur zur Erinnerung nochmal
3
4
5
6
7
8
instance Applicative ((→) τ ) where
— pure :: α → (τ → α)
pure = const
— (~) :: (τ → α → β) → (τ → α) → τ → β
f <*> g = \x -> f x (g x)
— K = λx y . x
— S = λf g x. f x (g x)
9
10
11
12
— Funktion im Functor (Int →)
*Main> :t (&&) <$> even
(&&) <$> even :: Int → Bool → Bool
13
14
15
— Bool-Wert im Functor (Int →)
*Main> :t (<3)
(<3) :: Int → Bool
16
17
18
*Main> :t (&&) <$> even <*> (<3)
(&&) <$> even <*> (<3) :: Int → Bool
— Anwendung der Funktion auf den Wert.
Der Beweis der Gesetze von Folie 484 ist einfache Rechnerei (gute Übung).
63 https://svn.uni-konstanz.de/dbis/inf2_16s/pub/pk2-12.lhs
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
502
14 · Monadische Berechnungen
Gesetze für Applicative · 14.6
Ist Applicative schon genug?
Aufgabe Schreibe eine Funktion list, die einen Dateinamen von stdin
liest, und den Inhalt dieser Datei zurückgibt.
Bekannt sind:
I
Bekannt:
getLine :: IO String
readFile :: String → IO String
Gesucht:
list :: IO String
• Dabei soll das Ergebnis von getLine als Eingabe von readFile dienen,
• das Ergebnis von readFile möchten wir zurück bekommen.
Wie können wir getLine und readFile verknüpfen?
I
1
2
Prelude> :t
Prelude> :t
readFile <*> getLine
readFile <$> getLine
— Was kommt hier raus?
— Was kommt hier raus?
Erinnerung: (~) :: f (α → β) → f α → f β, und fmap :: (α → β) → f α → f β.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
503
14 · Monadische Berechnungen
I
Gesetze für Applicative · 14.6
Wir können IO-Operationen an ein rein funktionales Program
“anschließen” (cf. Seite 499), aber diese Operationen können nicht
miteinander kommunizieren!
(~) :: f (α → β) → f α → f β
• Die beiden Argumente von ~ stehen für separate “Maschinen”, oder
“Berechnungen mit Effekt”,
• die erste :: f (α → β) produziert eine Funktion,
• die zweite :: f α produziert ein Argument,
• ~ konstruiert die Maschine, welche die Funktion auf das Argument anwendet,
und das Ergebnis zurückgibt.
I
Wir brauchen etwas anderes:
f α → (α → f β) → f β
Das zweite Argument :: (α → f β) soll entscheiden können welche
IO-Operation ausgeführt wird.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
504
14 · Monadische Berechnungen
14.7
1
Monad · 14.7
Monad
(dt. Monade)
class Applicative m ⇒ Monad m where
2
(>>=)
3
:: m α → (α → m β) → m β
— dieser Operator heißt Bind
4
(>>)
:: m α → m β → m β
u >> v = u >>= const v
5
6
7
return :: α → m α
return = pure
8
9
Beobachtungen
I
I
— Aus historischen Gründen hier nochmal mit neuem Namen
Monad ist also ein besonderer applikativer Funktor.
Das einzig interessante ist der Bind-Operator >>=.
Erst später kümmern wir uns um:
• >> (aka. then) ist eine spezielle Version von >>=.
• return ist einfach ein neuer Name für pure, der aus historischen Gründen
hier auftaucht, besser wäre wohl man hätte Monad ohne return.
⇒ Konzentrieren wir uns also auf >>=.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
505
14 · Monadische Berechnungen
Monad · 14.7
Lernen aus dem Typ von >>=
m ist ein Typ vom Kind ∗ → ∗, also ein Typkonstruktor mit einem
Argument.
>>= :: Monad m ⇒ m α → (α → m β) → m β
I
Würde man Maybe für m einsetzen, hätte man:
>>= :: Maybe α → (α → Maybe β) → Maybe β
I
Würde man Either τ für m einsetzen, hätte man:
>>= :: Either τ α → (α → Either τ β) → Either τ β
I
Würde man [ ] für m einsetzen, hätte man:
>>= :: [α] → (α → [β]) → [β]
I
Würde man (τ →) für m einsetzen, hätte man:
>>= :: (τ → α) → (α → (τ → β)) → (τ → β)
Frage Wie könnte man die konstruieren?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
506
14 · Monadische Berechnungen
I
1
2
3
Monad · 14.7
Wenn man den Typen folgt, kommt man auf diese Implementierungen:
instance Monad Maybe where
Just x >>= g = g x
Nothing >>= _ = Nothing
4
5
6
7
instance Monad (Either t) where
Right x >>= g = g x
Left l >>= _ = Left l
8
9
10
instance Monad ((->) t) where
h >>= g = \x -> g (h x) x
11
12
13
instance Monad [] where
xs >>= g = concatMap g xs
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
507
14 · Monadische Berechnungen
Monad · 14.7
Gesetze für Monad
I
Wenn man sich m α als “Berechnung” vorstellt, die einen Wert vom Typ
α liefert, dann übergibt >>= dieses Ergebnis an eine Funktion vom Typ
α → m β:
>>= :: Monad m ⇒ m α → (α → m β) → m β
Gesetze Das muss gelten:
1. return x >>= v ≡ v x
• Die Berechnung die x liefert an v gebunden, ist das Gleiche wie v auf x
anwenden.
2. m >>= return ≡ m
• Das Ergebnis einer Berechnung an return übergeben liefert die gleiche
Berechnung mit dem gleichen Ergebnis.
3. (m >>= k) >>= h ≡ m >>=(λx. k x >>= h)
• Das dritte Gesetz beschreibt eine Art Assoziativität für >>=.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
508
14 · Monadische Berechnungen
Monad · 14.7
Monaden sind immer applikative Funktoren
Jeder Monad ist notwendigerweise auch Applicative (und damit auch
gleich noch Functor, cf. Seite 497).
Beweisidee Es genügt pure und ~ durch return und >>= zu definieren,
und die Gesetze für Applicative aus den Gesetzen von Monad herzuleiten.
I
I
pure = return
g ~ v = g >>=(λh. v >>=(λx. return (h x)))
• g :: m (α → β) enthält eine Funktion, der linke Bind-Operator packt diese
aus, und bindet sie an h :: α → β.
• Ebenso gelangen wir an das Argument x :: α das in v :: m α steckt.
• Das Ergebnis von h x :: β wird mit return zurückgegeben. Das ist nötig, weil
das zweite Argument von >>= einen monadischen Typ haben muss, also m β.
I
Das Beweisen der Gesetze ist Rechnerei, aber nicht arg schwierig.
Obacht Nicht jeder applikative Functor lässt sich als Monad auffassen:
Die ZipList-Instanziierung von Listen zu Applicative (cf. Seite 486) lässt
sich nicht zu Monad erweitern!
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
509
14 · Monadische Berechnungen
14.8
monadic IO · 14.8
monadic IO
Ein-/Ausgabe mit Monaden
Wenn man sich unser Interface zu Strukturen, oder “effektbehafteten
Berechnungen” anschaut, sollte etwas auffallen:
fmap :: Functor f
⇒ (α → β) → (f α → f β)
pure :: Applicative f
(~) :: Applicative f
⇒ α→f α
⇒ f (α → β) → (f α → f β)
return :: Monad m
(>>=) :: Monad m
⇒ α→mα
⇒ m α → (α → m β) → m β
Was?
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
510
14 · Monadische Berechnungen
monadic IO · 14.8
~
Wichtig Die “Richtung” ist immer vorgegeben: Alle Funktionen
führen in die Struktur, oder operieren darin.
I
Es gibt keine Funktion die den “Kontext” verlässt, etwa
escape :: Functor f ⇒ f α → α
I
Für spezielle Strukturen kennen wir solche Funktionen zwar, z.B.
maybe :: β → (α → β) → Maybe α → β
aber die Existenz einer solchen Funktion wird von keiner der Klassen
Functor, Applicative oder Monad gefordert.
Konsequenz Was in der Struktur passiert bleibt in der Struktur.
Und das eignet sich hervorragend für IO: Alle Seiteneffekte sind in der
IO-Struktur gefangen, sie können unsere rein funktionale Welt nicht
verschmutzen, oder die Referenzielle Transparenz zerstören.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
511
14 · Monadische Berechnungen
monadic IO · 14.8
Die IO-Monade
IO ist tatsächlich Instanz von Monad:
I
1
2
3
4
5
Prelude>
— ...
instance
instance
instance
:i IO
Monad IO — Defined in ‘GHC.Base’
Functor IO — Defined in ‘GHC.Base’
Applicative IO — Defined in ‘GHC.Base’
Damit können wir die Aufgabe von Folie 491 lösen:
I
1
list = getLine >>= readFile
2
3
4
Prelude> :t list
list :: IO String
5
6
7
8
Prelude> list
foo.txt
"hello outside world\n"
Stefan Klinger · DBIS
— meine Eingabe
— das Ergebnis der Berechnung
Informatik 2 · Sommer 2016
512
14 · Monadische Berechnungen
monadic IO · 14.8
Monadische Operationen zusammensetzen
Als Instanz von Monad können wir das Ergebnis einer IO-Berechnung
verwenden, um eine neue IO-Berechnung zu erzeugen:
Beispiel
1
2
3
— cf. Seite 493
Prelude> getLine >>= putStrLn
flotsch
flotsch
4
5
6
7
Prelude> getLine >>= \_ -> putStrLn "foo"
lala
foo
8
9
10
11
12
Prelude> putStrLn "Name:" >>= (\_-> getLine >>= (\x -> putStrLn ("Hello " ++ x)))
Name:
Joe
Hello Joe
13
14
15
Prelude> length <$> readFile "foo.txt" >>= \n-> putStrLn (show n ++ " Zeichen")
13 Zeichen
Die hervorgehobene Ausgabe ist nicht das Ergebnis der IO-Operation (das ist hier immer
()), sondern die Ausgabe welche von der IO-Operation als Seiteneffekt erzeugt wurde.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
513
14 · Monadische Berechnungen
monadic IO · 14.8
do-Notation
Das ist nicht lesbar:
1
2
3
4
foo'''
= putStrLn "Name:" >>= \_-> getLine >>= \name-> putStrLn "Age:" >>= \_->
stringToInt <$> getLine >>= \age-> let verdict = if (age < 42) then
"young" else "old" in putStrLn $ name++" is "++verdict
Häufig braucht man eine IO-Operation wegen ihrem Seiteneffekt, und ist
an dem Ergebnis der Berechnung nicht interessiert:
I
1
2
3
4
Prelude> putStrLn "Gimme text:"
Gimme text:
hello
5
>>=
\_-> length <$> getLine
Dafür ist eine spezielle Variante von >>= vorgesehen:
I
1
u >> v = u >>= const v
2
3
4
5
Prelude> putStrLn "Gimme text:" >> length <$> getLine
Gimme text: dabadidabadu
12
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
514
14 · Monadische Berechnungen
monadic IO · 14.8
Mit >> wird der code minimal lesbarer:
1
2
3
4
foo''
= putStrLn "Name:" >> getLine >>= \name-> putStrLn "Age:" >> stringToInt
<$> getLine >>= \age-> let verdict = if (age < 42) then "young" else "old"
in putStrLn $ name++" is "++verdict
Wenn man die Zeilenumbrüche nicht völlig willkürlich setzt...
1
2
3
4
5
6
7
8
foo' = putStrLn "Name:" >>
getLine >>= \name->
putStrLn "Age:" >>
stringToInt <$> getLine >>= \age->
let verdict = if (age < 42)
then "young"
else "old"
in putStrLn $ name++" is "++verdict
— Zuweisung name := getLine ?
— age := stringToInt <$> getLine ?
...sieht’s aus wie ein imperatives Programm!
I
u >>= λx. v Bindet das Ergebnis der Berechnung u an die Variable x,
welche dann in v verwendet werden kann.
I
u >> v
Stefan Klinger · DBIS
Führt erst u aus, und dann v . Das Ergebnis von u wird ignoriert.
Informatik 2 · Sommer 2016
515
14 · Monadische Berechnungen
monadic IO · 14.8
Syntaktischer Zucker
I
I
Haskell verstärkt diesen Eindruck mit einer speziellen syntaktischen
Form: do-Notation.
Ein Ausdruck
do { c1 ; ... ; cn }
führt die monadischen Berechnungen c1 , ..., cn nacheinander aus.
I
Eine Berechnung ci (mit i < n) ist dabei entweder
• ein Ausdruck ei :: m αi ,
• eine Variablenbindung pi <- ei mit einem Ausdruck ei :: m αi und einem
Pattern pi :: αi , oder
• eine let-Klausel (nächste Seite).
I
Die letzte Berechnung cn ist immer ein Ausdruck en :: m αn , und
bestimmt den Rückgabewert des do-Blocks.
Wichtig Die monadischen Berechnungen e1 , ..., en operieren auf der
gleichen Monade m, können aber verschiedene Rückgabetypen
α1 , ..., αn haben.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
516
14 · Monadische Berechnungen
monadic IO · 14.8
Operationale Semantik
Ähnlich zur List-Comprehension (cf. Seite 247) werden do-Ausdrücke auf
den Sprachkern abgebildet.
Definition Semantik der do-Notation
Seien e :: m α, p ein Pattern, und decls Deklarationen wie bei
let-Ausdrücken. Sei es eine nicht-leere Sequenz von Haskell-Ausdrücken.
do { e } → e
do { e ; es } → e >> do { es }
do { p <- e ; es } → e >>= \x -> case x of
p -> do { es }
_ -> fail "ERROR"
do { let decls ; es } → let decls in do { es }
I
I
"ERROR" wird vom Compiler erzeugt und sollte über die Position in
Quellcode informieren.
Der letzte Ausdruck in es darf nicht die Formen p <- e oder
let decls haben.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
517
14 · Monadische Berechnungen
monadic IO · 14.8
Damit ist das Programm schön lesbar:
1
2
3
4
5
6
7
8
foo = do putStrLn "Name:"
name <- getLine
putStrLn "Age:"
age <- stringToInt <$> getLine
let verdict = if (age < 42)
then "young"
else "old"
putStrLn $ name++" is "++verdict
9
10
11
12
13
14
15
16
17
*Main> :t foo
foo :: IO ()
*Main> foo
Name:
Agecanonix
Age:
93
Agecanonix is old
I
Wie auch bei let-Ausdrücken wird zweidimensionale Syntax unterstützt,
d.h. die geschweiften Klammern und Semikola sind durch die Einrückung
implizit gegeben.
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
518
14 · Monadische Berechnungen
monadic IO · 14.8
Programme kompilieren
Schreiben eine Datei filesize.lhs mit dem Inhalt:
I
1
> module Main where
2
3
4
Kompilierbare Programme müssen ein Modul Main,
und darin eine Funktion main :: IO() haben.
5
6
7
8
9
10
> main :: IO ()
> main = do putStrLn "Enter file name:"
>
p <- getLine
>
s <- length <$> readFile p
>
putStrLn $ p++" has size "++show s
...Kompilieren und testen:
I
1
2
3
4
5
6
7
$ ghc filesize.lhs
[1 of 1] Compiling Main
Linking filesize ...
$ ./filesize
Enter file name:
filesize.lhs
filesize.lhs has size 174
Stefan Klinger · DBIS
( filesize.lhs, /.../Main.o )
Informatik 2 · Sommer 2016
519
14 · Monadische Berechnungen
monadic IO · 14.8
Monad-Laws und do-Notation
Hier nochmal der Zusammenhang zwischen den Monaden-Gesetzen und der
do-Notation:
1. p >>= return ≡ p
do{ x ← p; return x }
≡
do{ p }
2. return e >>= q ≡ q e
do{ x ← return e; es }
≡
do{ es[x e] }
3. (p >>= q) >>= r ≡ p >>=(λx. q x >>= r )
do{ x ← do{ es; q }; r x}
≡
do{ es; x ← q; r x }
Ohne die Gültigkeit der Monaden-Gesetze könnte man die do-Ausdrücke
nicht so umformen!
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
520
14 · Monadische Berechnungen
monadic IO · 14.8
Literatur
I
Conor McBride, Ross Paterson. Applicative Programming with Effects. In
Journal of Functional Programming 18:1 (2008), pages 1-13.
http://www.soi.city.ac.uk/~ross/papers/Applicative.pdf
I
Brent Yorgey. Typeclassopedia.
https://wiki.haskell.org/Typeclassopedia.
Eine andere Anwendung (ohne IO) für monadische Berechnungen ist
Parsec64 , eine Bibliothek von Kombinatoren zur Konstruktion von Parsern.
Leider ist die Dokumentation gerade etwas unzugänglich.
64 http://hackage.haskell.org/package/parsec
Stefan Klinger · DBIS
Informatik 2 · Sommer 2016
521
Herunterladen