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