Praktikum Funktionale Programmierung - Goethe

Werbung
Anleitung zum
Praktikum Funktionale Programmierung
Sommersemester 2015
PD Dr. David Sabel
Institut für Informatik
Fachbereich Informatik und Mathematik
Goethe-Universität Frankfurt am Main
Postfach 11 19 32
D-60054 Frankfurt am Main
Email: [email protected]
Stand: 7. Juni 2015
Inhaltsverzeichnis
1
2
3
4
Allgemeines
1.1 Organisatorisches . . . . . . .
1.1.1 Webseite . . . . . . . .
1.1.2 Regelmäßiges Treffen .
1.1.3 Modulprüfung . . . .
1.2 Haskell . . . . . . . . . . . . .
1.2.1 Material zu Haskell . .
1.2.2 GHC . . . . . . . . . .
1.2.3 Haskell Platform . . .
1.2.4 Cabal, Hackage . . . .
1.3 Quellcode . . . . . . . . . . .
1.4 Dokumentation . . . . . . . .
1.5 Tests . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
2
2
2
2
3
3
3
4
5
5
5
6
6
.
.
.
.
.
.
7
7
9
9
10
12
14
.
.
.
.
15
15
19
19
23
Teil 2: Type-Check und Transformation in einfachere Syntax
4.1 Das Typsystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.1.1 Gleichungen mit Unifikation lösen . . . . . . . . . . . . . . . . . . . . . . .
30
30
36
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Kurzübersicht über das Projekt
2.1 Das Softwareprojekt . . . . . . . . .
2.1.1 Präsentation der Ergebnisse .
2.2 Die Sprache CHF . . . . . . . . . . .
2.2.1 Die Syntax von Ausdrücken .
2.2.2 Typisierung von Ausdrücken
2.2.3 Beispiele . . . . . . . . . . . .
Teil 1: Lexen und Parsen
3.1 Quelltext lexen . . . . . . . .
3.2 Parsen mit Happy . . . . . .
3.2.1 Ausgabe des Parsers
3.2.2 Die Parserdefinition
D. Sabel, FP-PR, SoSe 2015
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Stand: 7. Juni 2015
Inhaltsverzeichnis
4.2
5
4.1.2 Implementierung des Typchecks . . . . . . . . . . . . . . . . . . . . . . . .
Transformation in eine vereinfachte Syntax . . . . . . . . . . . . . . . . . . . . . .
4.2.1 Transformation in MExpr . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Teil 3: Abstrakte Maschinen
5.1 Die Abstrakte Maschine Mark 1 zur Ausführung funktionaler CHF-Programme
5.1.1 Implementierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.2 Die IO-Mark 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.2.1 Implementierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.3 Die Nebenläufige Maschine – Concurrent Mark 1 . . . . . . . . . . . . . . . . . .
5.4 Der Interpreter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Bibliography
D. Sabel, FP-PR, SoSe 2015
.
.
.
.
.
.
39
42
42
45
45
50
55
61
63
68
69
1
Stand: 26. März 2015
1
Allgemeines
1.1 Organisatorisches
1.1.1 Webseite
Die Webseite zum Praktikum ist unter
http://www.ki.informatik.uni-frankfurt.de/lehre/SS2015/FP-PR/
zu finden. Neben dieser Anleitung und Aufgabenstellungen sind dort Verweise auf weiteres
Material sowie aktuelle Informationen zu finden. Insbesondere ist dort ein Skript namens „Begleitmaterial zum Praktikum Funktionale Programmierung“ zu finden, dass einige kurze Kapitel zu notwendigem Hintergrundwissen zum Praktikum enthält: Neben der Bedienung von
CVS, Hinweisen zum Debuggen in Haskell, werden dort die Werkzeuge Haddock zur Dokumentation und Happy zum Erstellen von Parsern erörtert. Außerdem werden einzelne Konstrukte von Haskell dort erläutert: Datentypdefinitionen insbesondere die Verwendung der sog.
Record-Syntax, Typklassen, das hierarchische Modulsystem sowie monadisches Programmieren und Concurrent Haskell. Das Skript kann als Nachschlagewerk benutzt werden, wenn es
die Aufgaben dieser Anleitung erfordern.
1.1.2 Regelmäßiges Treffen
Donnerstags, um 14 c.t. findet ein regelmäßiges Treffen aller Praktikumsteilnehmer statt. Hier
sollen Fragen und Probleme diskutiert werden, die Anwesenheit ist somit i.A. erforderlich. Ge-
D. Sabel, FP-PR, SoSe 2015
Stand: 26. März 2015
1.2 Haskell
naue Termine bzw. ob das regelmäßiges Treffen ausfällt usw. finden sich auf der Webseite zum
Praktikum.
1.1.3 Modulprüfung
Die Studienleistung wird für die korrekte und vollständige Bearbeitung der Aufgaben vergeben.
Es sollen je 2–3 Teilnehmer für das Praktikum eine Gruppe bilden.
Zur Erfüllung der Aufgaben gehört die Abgabe und Präsentation eines hier auf den Rechnern der RBI laufenden, kommentierten Programms, sowie eine schriftliche Ausarbeitung, die
folgendes enthält:
• eine kurze Erläuterung der Grundlagen, die zur Lösung der Aufgabe notwendig waren.
• eine kurze Beschreibung der Lösung selbst.
• ein Protokoll, das den Prozess der Problemlösung dokumentiert. Hierbei sollte insbesondere erfasst werden, wieviel Zeit für die einzelnen Tätigkeiten benötigt wurden, und wie
die Arbeiten innerhalb der Gruppe aufgeteilt wurden.
• die Dokumentation der durchgeführten Tests.
Es gibt drei Zeitpunkte zu denen (Zwischen-)ergebnisse präsentiert werden sollen. Gleichzeitig soll zu diesen Zeitpunkten der aktuelle Stand der schriftlichen Ausarbeitung und der
implementierten Programme abgegeben werden! Der erste Teil ist in Kapitel 3 beschrieben, für
ihn (Aufgaben 1, 2, 3 und 4) sind 3 Wochen Bearbeitungszeit vorgesehen. Die Bearbeitungszeit
der beiden anderen Teile wird im Anschluss daran festgelegt.
Außerdem sei erwähnt, dass die Beteiligung bzw. das Stellen von Fragen bei den Besprechungen ausdrücklich erwünscht ist.
1.2 Haskell
Die Implementierung der Programme soll in Haskell erfolgen. Kenntnisse in Haskell oder anderen Funktionalen Programmiersprachen sind für die Teilnahme am Praktikum notwendig.
1.2.1 Material zu Haskell
Haskell1 ist eine der zur Zeit wohl bedeutendsten, nicht-strikten, funktionalen Programmiersprachen. Wer sich eingehender in Haskell vertiefen möchte, sei auf den Haskell-Report (Jones,
2003) verwiesen, in dem die Sprache definiert wird.
1
Die offizielle Homepage zu Haskell ist http://haskell.org.
D. Sabel, FP-PR, SoSe 2015
3
Stand: 26. März 2015
1 Allgemeines
Zur Einarbeitung in Haskell sind das deutsche Buch (Chakravarty & Keller, 2004), die Bücher
(Thompson, 1999), (Bird, 1998), (Lipovača, 2011), (O’Sullivan et al., 2008) sowie (Hudak et al.,
2000) und auch die Skripte (und David Sabel, 2014; Schmidt-Schauß, 2014) empfehlenswert.
Für Haskell gibt es mittlerweile eine recht große Anzahl von Standardbibliotheken,
die hierarchisch organisiert sind. Die Dokumentation der Bibliotheken ist online unter
http://www.haskell.org/ghc/docs/latest/html/libraries/index.html zu finden.
Zum Haskell-Programmieren verwenden wir den GHC2 .
1.2.2 GHC
Der Glasgow Haskell Compiler3 (GHC) hält sich an den Haskell-Report. Im GHC gibt es Erweiterungen, die z.B. die Typklassen betreffen. Der GHC ist fast vollständig in Haskell geschrieben
und erzeugt C-Code als Zwischencode, der dann von einem auf dem System verfügbaren CCompiler in ein ausführbares Programm übersetzt wird. Diese Tatsache macht den GHC äußerst
portierbar. Für einige Plattformen gibt es zusätzlich einen Code-Erzeuger. GHC bietet zusätzlich
noch den GHCi, der eine interaktive Version des GHC darstellt und als Interpreter zum schnellen
Programmieren und Testen verwendet werden kann.
Der GHCi wird in einer Konsole mit dem Kommando
ghci
gestartet (dafür muss der Pfad zur GHC-Installation im Suchpfad der Konsole eingetragen sein).
Im Interpreter bietet das Kommando :help eine Hilfe und eine Übersicht über die Interpreterkommandos an. Mit :load kann eine Quelldatei geladen werden. Alternativ kann dies auch
direkt beim Starten des GHCi als Parameter durchgeführt werden mit
ghci "Dateiname"
Haskell Quellcode-Dateien haben die Endung .hs oder für Dateien im Literate-Style mit .lhs:
Im „normalen“ Stil sind alle Zeilen Programme, außer solchen die explizit als Kommentar gekennzeichnet sind (mit „-- “ wird ein Zeilenkommentar markiert, mit „{-“ und „-}“ ein geklammerter Kommentar). Hingegen werden im „Literate Haskell“-Stil alle Text-Zeilen als Kommentar interpretiert, es sei denn sie werden explizit als Programm gekennzeichnet. Programmzeilen beginnen mit einen Größerzeichen und einem Leerzeichen (“> ”), zusätzlich muss sich
zwischen Kommentar und Codezeilen immer eine Leerzeile befinden, ansonsten bekommen
wir eine Fehlermeldung:
.... line 8: unlit: Program line next to comment
phase ‘Literate pre-processor’ failed (exitcode = 1)
Ein vollständige Liste aller Haskell-Implementierungen ist unter http://www.haskell.org/haskellwiki/Implementations
zu finden.
3
Die Homepage des GHC ist http://haskell.org/ghc.
2
Stand: 26. März 2015
4
D. Sabel, FP-PR, SoSe 2015
1.3 Quellcode
Zum Compilieren eines Programms mit dem GHC muss i.A. ein Modul namens Main definiert
werden, welches eine Funktion main vom Typ IO () enthält (diese Funktion wird beim Programmstart ausgeführt). Ein solches Modul kann dann compiliert werden durch den Aufruf:
ghc --make -o "OutputName" Main.hs
Dieser Aufruf compiliert die Datei namens Main.hs und deren Importe (hierfür dient u.a. der
Parameter --make) und erstellt eine ausführbare Datei namens OutputName.
1.2.3 Haskell Platform
Sofern verfügbar empfiehlt es sich die Haskell Platform http://hackage.haskell.org/platform/
zu installieren. Diese beinhaltet neben dem GHC noch weitere Werkzeuge, wie den LexerGenerator Alex und den Parser-Generator Happy, sowie einige zusätzliche Programmbibliotheken.
1.2.4 Cabal, Hackage
Neben den Standardbibliotheken gibt es unter http://hackage.haskell.org eine große Sammlung
weiterer Pakete, die in einem standardisierten Format vorliegen (so genannte Cabal-Packages).
Unter http://www.haskell.org/haskellwiki/Cabal/How_to_install_a_Cabal_package kann man
nachlesen, wie diese installiert werden.
Der vorgegebene Code beinhaltet auch eine cabal-Beschreibungsdatei für das Projekt des
Praktikums. Z.B. kann mit cabal configure und anschließendem cabal build das ganze Projekt kompiliert werden, ebenso kann cabal haddock die Haddock-Dokumentation erstellt werden.
1.3 Quellcode
Der Quellcode sollte wartbar sein, und dementsprechend aufgebaut, dokumentiert und kommentiert werden. Es empfiehlt sich die Dateien mithilfe der hierarchischen Modulstruktur zu
strukturieren. Der vorgegebene Quellcode ist hierarchisch angelegt und sämtlicher neuer Quellcode sollte in diese Struktur eingefügt werden. Zudem sollte durch geeignete Import- und
Export-Listen in den Moduldeklarationen eine geeignete Kapselung erfolgen.
Damit der ghci das Modul auch findet, muss das oberste Verzeichnis der Modulstruktur für
ihn auffindbar sein. Dafür gibt es beim ghci den Parameter
-i<dir>
Search for imported modules in the directory <dir>.
Wenn wir z.B. gerade im Verzeichnis CHF/Parse/ sind und wollen das Modul mit Dateinamen
Lexer.lhs laden, so sollten wir ghci wie folgt aufrufen:
ghci -i:../../ Lexer.lhs
D. Sabel, FP-PR, SoSe 2015
5
Stand: 26. März 2015
1 Allgemeines
Nahezu sämtliche vorgegebenen Quellcode-Dateien sind im „Literate Haskell“-Stil verfasst.
Im Praktikum wird der Quellcode mittels CVS4 verwaltet werden (siehe (Sabel, 2015)).
1.4 Dokumentation
Die erstellten Programme sollten ausführlich kommentiert werden, so dass ein „Leser“
des Programms dieses nachvollziehen kann. Zusätzlich soll für die Module eine HTMLDokumentation mit Hilfe des Tools Haddock5 erstellt werden (siehe (Sabel, 2015)). Es sei jedoch
angemerkt, dass eine reine Haddock-Dokumentation nicht ausreicht, da mit Haddock nur die
exportierten Funktionen und Datentypen dokumentiert werden und zudem eher deren Verwendung und nicht deren Implementierung erklärt wird.
1.5 Tests
Sämtliche implementierten Funktionen, Datenstrukturen und Module müssen getestet werden.
Teilweise sind Testdaten bzw. Testaufrufe vorgegeben. Diese müssen durchgeführt und im Idealfall auch bestanden werden. Es ist aber auch notwendig eigene Tests mit sinnvoll überlegten Testaufrufen durchzuführen. Sämtliche Tests sind zu dokumentieren und derart zu gestalten, dass sie leicht erneut durchgeführt, d.h. reproduziert, werden können. Zum Testen bietet sich eventuell auch die QuickCheck-Bibliothek (http://hackage.haskell.org/cgi-bin/hackagescripts/package/QuickCheck) an.
4
5
Concurrent Versions System, http://www.nongnu.org/cvs/
http://haskell.org/haddock
Stand: 26. März 2015
6
D. Sabel, FP-PR, SoSe 2015
2
Kurzübersicht über das Projekt
2.1 Das Softwareprojekt
Ziel des Praktikums ist es, einen Interpreter für die nebenläufige funktionale Programmiersprache CHF zu entwickelt. In (Sabel & Schmidt-Schauß, 2011) wurde CHF definiert und eine
kontextuelle Semantik dafür untersucht. CHF wertet verzögert aus und verfügt zusätzlich über
Konstrukte zur imperativen und zur nebenläufigen Programmierung:
• MVars sind Speicherzellen in denen Werte abgelegt und gelesen werden können. Eine
MVar ist entweder leer oder gefüllt. Will ein nebenläufiger Thread aus einer leeren MVar
einen Wert lesen, so wird der Thread so lange warten, bis ein anderer Thread die MVar
füllt. Umgekehrt führt der Versuch des Schreibens in eine bereits gefüllte MVar dazu, dass
der entsprechende Thread solange blockiert ist, bis ein anderer Thread die MVar entleert
hat. Bei mehreren Threads die gleichzeitig auf eine MVar zugreifen möchten, darf stets
nur ein Thread zugreifen, die anderen Threads werden blockiert.
• Futures sind nebenläufige Threads, die als Ergebnis einen Wert zurückliefern können. Sie
können daher benutzt werden, um nebenläufig (oder auch parallel) einen Wert zu ermitteln. In CHF wird durch eine Future eine sogenannte monadische Berechnung durchgeführt, die daher auch imperativ sein darf (d.h. auf MVars zugreifen darf).
Man kann CHF als eine Kernsprache für Concurrent Haskell ansehen, wobei nebenläufige
Threads durch nebenläufige Futures ersetzt wurden.
Stand: 26. März 2015
D. Sabel, FP-PR, SoSe 2015
2 Kurzübersicht über das Projekt
Im Praktikum sollen die wesentlichen Phasen des Kompilierens für CHF implementiert werden, und eine abstrakte Maschine zur Ausführung von CHF-Programmen erstellt werden. Ohne die Sprache CHF im Detail darzustellen, geben wir an dieser Stelle einen Überblick über das
Projekt. Wir beschreiben zunächst allgemein die Phasen eines Compilers
Lexikalische Analyse, Scanning Hierbei wird der Quelltext in einen Strom von Token transformiert, Leerzeichen, Umbrüche, Kommentare usw. werden dabei entfernt. Ein Token ist
eine syntaktische Einheit, wie z.B. Schlüsselwörter (letrec, case, in,. . . ) oder Zahlen. Der
Lexer sollte dabei den Eingabestring nur einmal durchlaufen und somit lineare Zeit (in der
Anzahl der Zeichen) verbrauchen. Die Ausgabe des Lexers wird an den Parser übergeben.
Syntaktische Analyse, Parsing Hier wird geprüft, ob der Tokenstrom von der kontextfreien
Grammatik hergeleitet wird, d.h. das Programm syntaktisch korrekt ist. Es gibt hierfür
verschiedene Parse-Methoden, wir werden einen sog. Shift-Reduce-Parser automatisch
mithilfe eines Parsergenerators erzeugen. Die Ausgabe des Parsers ist i.A. ein Syntaxbaum,
wir werden den Syntaxbaum mittels eines Haskell-Datentyps darstellen.
Semantische Analyse Hierbei wird das Programm auf semantische Fehler überprüft. Z.B. wird
bei manchen imperativen Programmen geprüft, ob alle benutzten Variablen auch deklariert sind, oder auch ein Typcheck durchgeführt.
Zwischencodeerzeugung Hier wird Code für eine abstrakte Maschine erzeugt.
Codeoptimierung Der erzeugte Zwischencode wir optimiert.
Codeerzeugung Erzeugung eines Programms für eine reale Maschine.
Wir werden die wesentlichen Phasen des Compilierens für die Sprache CHF implementieren,
wobei wir im Gegensatz zu einem Compiler, die letzten beiden Phasen (wenn überhaupt) vermutlich nur rudimentär implementieren werden. Daher entwicklen wir eher einen Interpreter,
der den Quelltext nimmt die ersten 4 Phasen durchläuft und anschließend das Programm auf
einer abstrakten Maschine ausführt. Ein großer Teil des Praktikums wird sich der Implementierung solcher abstrakter Maschinen widmen.
Abbildung 2.1 gibt einen ungefähren Überblick über die einzelnen Teilschritte unseres Interpretes (CHFi), welchen wir im Praktikum implementieren werden.
Neben den einzelnen Phasen des Compilers wird im Praktikum eine abstrakte Maschine implementiert. Basierend auf der abstrakten Maschine von Sestoft (Sestoft, 1997) (Mark 1) für funktionale Ausdrücke werden wir eine Maschine implementieren die monadisches IO (den Zugriff
auf die MVars) ausführen kann (IO-Mark 1). Diese Maschine wird letztendlich um nebenläufige
Futures erweitert (Concurrent Mark 1). Der rechte Teil von Abbildung 2.1 zeigt die einzelnen
Maschinen.
Das Praktikum ist in 3 Teile eingeteilt:
Stand: 26. März 2015
8
D. Sabel, FP-PR, SoSe 2015
2.2 Die Sprache CHF
Abbildung 2.1: Projektübersicht
• In Teil 1 wird der Lexer und der Parser implementiert. Da wir in diesem Teil nicht allzuviel
Zeit verlieren möchten, ist hier schon ein Großteil des Parsers vorgegeben.
• In Teil 2 wird die semantische Analyse durchgeführt daher ein Typchecker für CHFAusdrücke implementiert.
• In Teil 3 werden die abstrakten Maschinen implementiert.
2.1.1 Präsentation der Ergebnisse
Ergebnisse sollen dreimal präsentiert werden:
• Nach Abschluss von Teil 1
• Nach Abschluss von Teil 2
• Nach Abschluss aller Teile.
Zu diesen drei Zeitpunkten soll auch der aktuelle Stand der Ausarbeitung sowie die bis dahin
vorhandenen Programme abgegeben werden.
2.2 Die Sprache CHF
In diesem Abschnitt wird die Programmiersprache CHF vorgestellt.
D. Sabel, FP-PR, SoSe 2015
9
Stand: 26. März 2015
2 Kurzübersicht über das Projekt
2.2.1 Die Syntax von Ausdrücken
Abbildung 2.2 zeigt die Syntax von Ausdrücken Expr der Sprache CHF. Diese benutzen als Unterausdrücke monadische Ausdrücke MExpr.
e, ei ∈ Expr ::= x | me | (λx → e) | (e1 e2 ) | (c e1 . . . ear(c) ) | (seq e1 e2 )
| (caseT e of {((cT,1 x1 . . . xar(cT,1 ) ) → eT,1 );
...
((cT,|T | x1 . . . xar(cT,|T | ) ) → eT,|T | ))}
| (letrec x1 = e1 ; . . . ; xn = en in e) wobei n ≥ 1
me ∈ MExpr ::= (return e) | (e1 >>= e2 ) | (future e)
| (takeMVar e) | (newMVar e) | (putMVar e1 e2 )
Abbildung 2.2: Vollgeklammerte Syntax der Sprache CHF
Die Sprache CHF verfügt über
• Variablen x aus einer Menge von Variablen Var
• Abstraktionen λx → e, die anonyme Funktionen darstellen. Durch λx wird der formale
Parameter x im Rumpf e gebunden. Man kann den Ausdruck λx → e auffassen, wie die
mathematische Definition einer Funktion f (x) = e, wobei der Name f nicht definiert
wird. In Ausdrücken, die die Funktion verwenden möchten, steht daher anstelle von f
die Definition λx → e selbst.
• Anwendungen (e1 e2 ), um Funktionen (oder sogar beliebige Ausdrücke) auf Argumente
anzuwenden.
• rekursive letrec-Ausdrücke: In letrec x1 = e1 , . . . , xn = en in e, werden die Werte
der Variablen xi definiert. Da das letrec rekursiv ist, dürfen die xi sowohl in allen ei als
auch in e verwendet werden (sie sind dort gebunden). Beachte, dass alle Variablennamen
x1 , . . . , xn verschieden sein müssen und das „leere letrecs“ der Form letrec in e nicht
erlaubt sind.
• seq-Ausdrücke zur sequentiellen Auswertung: Die Auswertung von (seq e1 e2 ) wertet
zunächst e1 und danach e2 aus. Sind beide Auswertungen erfolgreich so ist der Wert von
(seq e1 e2 ) der Wert von e2 .
• Kontruktoranwendungen (ci e1 . . . en ), wobei ci ein Datenkonstruktor ist und n die
Stelligkeit des Konstruktors ist (die wir mit ar(ci ) bezeichnen). Wir nehmen an, dass
Stand: 26. März 2015
10
D. Sabel, FP-PR, SoSe 2015
2.2 Die Sprache CHF
eine bestimmte Menge von Datenkonstruktoren samt ihrer Stelligkeit bereits definiert
ist. Diese Menge ist partitioniert, wobei jede Partition die Datenkonstruktoren eines
Typs festlegt. Z.B. könnten wir annehmen, dass die Konstruktoren True und False eine Partition für den Typ Bool bilden. True und False sind nullstellige Konstruktoren
(ar(False) = 0 = ar(True) = 0), die man daher auch Konstanten nennt. Als weiteres
Beispiel betrachten wir Listen, die durch die Konstante Nil (für die leere Liste) und den
zweistelligen Konstruktor Cons beschrieben werden können. Dies sind nur Beispiele, später werden wir auf die Definition der Datenkonstruktoren noch genauer eingehen.
• case-Ausdrücke, um Datenkonstruktoren zu zerlegen. Hier kommen die Typen wieder
ins Spiel: Die Alternativen in den case-Ausdrücken müssen formal alle Konstruktoren
desselben Typs abdecken. Z.B. wäre case x of {Nil → y, True → z} verboten, da Nil und
True zu unterschiedlichen Typen gehören. Beachte, dass in CHF nur Variablen in Pattern
einer case-Alternative erlaubt sind, d.h. es gibt keine sog. verschachtelten Pattern wie z.B.
(Cons True Nil) (welches genau die Liste [True] matchen würde). Die gezeigte Syntax
erfordert, dass es genau eine Alternative pro Konstruktor des jeweiligen Typs gibt. Wir
werden dies in der Implementierung etwas lockerer handhaben: Fehlende Alternativen
sind (genau wie in Haskell) erlaubt und führen möglicherweise zu einem Laufzeitfehler.
• Monadische Ausdrücke
Wir beschreiben die einzelnen Konstrukte der monadischen Ausdrücke:
• return e verpackt einen beliebigen Ausdruck e in eine monadische Aktion (die nichts tut
außer e zurück zu liefern).
• newMVar e erzeugt eine neue MVar mit Inhalt e. Das Ergebnis ist return y, wobei y der
Name der MVar ist.
• takeMVar e wertet zunächst e solange aus, bis dort der Name einer MVar steht, anschließend wird der Wert aus der MVar gelesen und die MVar leer hinterlassen. Ist die MVar
jedoch vorher schon leer, so wird der Thread solange warten, bis ein anderer Thread die
MVar füllt. Das Ergebnis der Ausführung von takeMVar x ist return e0 , falls die MVar
namens x den Wert e0 beinhaltete.
• putMVar e1 e2 wertet zunächst e1 solange aus, bis dort der Name einer MVar steht. Anschließend wird in die MVar der Ausdruck e1 abgelegt, falls diese leer ist. Ist die MVar
gefüllt, muss der Thread warten, bis ein anderer Thread die MVar entleert. Das Ergebnis
einer putMVar-Aktion ist (return Unit). Hierbei ist Unit ein Datenkonstruktor des Typs
Unit. Dieser besteht gerade einzig und allein aus dem Wert Unit. Unit ist daher ein Nullwert, der zurückgegeben wird, wenn man eigentlich gar nichts zurückgeben will.
D. Sabel, FP-PR, SoSe 2015
11
Stand: 26. März 2015
2 Kurzübersicht über das Projekt
• future e erzeugt eine Future für e, wobei e eine monadische Aktion ist. D.h. es wird ein
nebenläufiger Thread erzeugt, der e ausführt. Der aufrufende Thread erhält sofort nach
Ausführen von future e das Ergebnis return y, wobei y der Name der erzeugten Future für
e ist. Das Besondere daran ist, dass der aufrufende Thread sofort weiterrechnen kann. Erst
wenn dieser den Wert von y benötigt, muss er u.U. warten, bis der nebenläufige Thread
für e seine Berechnung beendet hat. Wie wir später sehen werden, geschieht diese Synchronisation völlig automatisch ohne weiteres Zutun des Programmierers.
• Der „bind“-Operator >>= dient der sequentiellen Ausführung von zwei monadischen
Aktionen. In e1 >>= e2 muss e1 eine monadische Aktion sein und e2 ist eine Funktion die das Ergebnis der monadischen Aktion e1 verwendet, um eine neue monadische Aktion zu erstellen. Z.B. kann man mit >>= die folgende Aktion zusammenbauen:
(takeMVar x) >>= (λy → putMVar y True). Diese liest zunächst aus der MVar namen sx den
Namen einer anderen MVar und schreibt anschließend in diese MVar den Wert True.
Beachte, dass monadische Aktionen nur auf oberster Ebene ausgeführt werden und ansonsten (als Unterausdruck) wie Werte (genauer: genauso wie Konstruktoranwendungen) behandelt
werden.
2.2.2 Typisierung von Ausdrücken
CHF ist eine monomorph typisierte Sprache1 , d.h. es gibt (im Gegensatz) zu Haskell keine Typvariablen in den Typen2 . Das Typsystem unterscheidet zwischen „normalen“ funktionalen Ausdrücken, monadischen Aktionen und Typen der MVars. Monadische Aktionen sind stets vom
Typ IO τ , wobei τ ein beliebiger Typ ist, MVar haben den Typ MVar τ , wobei τ der Typ ist, den
der Ausdruck in der MVar besitzt. Typen können daher anhand der folgenden Syntax geformt
werden:
τ, τi ∈ Type ::= IO τ | T | MVar τ | τ1 → τ2
Hierbei ist T ein Typkonstruktor. Diese werden anhand der vorhanden Datentypen vorgegeben.
Z.B. ist Bool ein solcher Typkonstruktor. Für einen Ausdruck e schreiben wir e :: τ , wenn e den
Typ τ hat.
Die Datenkonstruktoren haben alle einen fest vorgegebenen Typ, wobei rekursive Typen erlaubt sind. Wir werden (ähnlich wie in Haskell) Benutzer-definierte Datentypen erlauben, die
am Anfang des Quelltextes mithilfe einer data-Anweisung definiert werden. Z.B. dürfen wir
schreiben:
data Bool = True | False
1
Im Gegensatz zum Typsystem in (Sabel & Schmidt-Schauß, 2011), ist die hier betrachtete Typisierung noch „monomorpher“.
2
trotzdem werden wir für die Implementierung des Typchecks auch Typvariablen verwenden
Stand: 26. März 2015
12
D. Sabel, FP-PR, SoSe 2015
2.2 Die Sprache CHF
Der Parser wird uns daraus extrahieren, dass der Typ Bool die beiden Datenkonstruktoren True
und False beinhaltet, wobei beide genau den Typ Bool haben. Als Beispiel für einen rekursiven
Typ betrachten wir den Typ ListBool, der Listen von Booleschen Werten darstellen kann3
data ListBool = NilBool | ConsBool Bool ListBool
Der Parser wird dies interpretieren als: Der Typ ListBool beinhaltet die Datenkonstruktoren
NilBool und ConsBool, wobei diese die folgenden Typen haben:
NilBool :: ListBool
ConsBool :: Bool → ListBool → ListBool
Beachte, dass man anhand der Typen der Konstruktoren auch direkt deren Stelligkeit ablesen
kann: NilBool erwartet keine Eingaben und ist daher 0-stellig, ConsBool erwartet zwei Eingaben
und ist daher 2-stellig.
Wir werden das genaue Typsystem später kennenlernen, an dieser Stelle wollen wir lediglich
die Typen einiger Konstrukte angeben und erläutern:
• return hat den Typ τ → IO τ , d.h. das Argument von return kann beliebigen Typs sein
und insgesamt stellt (return e) (für passendes e) eine monadische Aktion vom Typ (IO τ )
dar.
• future hat den Typ IO τ → IO τ , d.h. in (future e) muss e eine monadische Aktion sein.
Diese wird nebenläufig als Future ausgeführt. Insgesamt stellt (future e) eine monadische
Aktion dar, die als Ergebnis den Wert der Futureberechnung liefert.
• takeMVar hat den Typ MVar τ → IO τ , d.h. in (takeMVar e) muss e ein Ausdruck sein, der
zu einem Namen einer MVar auswertet und (takeMVar e) stellt eine monadische Aktion
dar, die als Ergebnis den Inhalt der MVar (vom Typ τ ) liefert.
• putMVar hat den Typ MVar τ → τ → IO Unit, d.h. in (putMVar e1 e2 ) muss e1 ein Ausdruck
sein, der zu einem Namen einer MVar auswertet und (putMVar e1 e2 ) stellt eine monadische Aktion dar, die als Ergebnis den Nullwert Unit liefert (da putMVar nur in die MVar
schreibt, genügt dieses Ergebnis).
• newMVar hat den Typ τ → IO(MVar τ ), was dazu passt, dass (newMVar e) eine monadische
Aktion darstellt, die eine MVar mit Inhalt e erstellt und den Namen der MVar als Ergebnis
liefert.
• >>= hat den Typ IO τ1 → (τ1 → IO τ2 ) → IO τ2 , da das linke Argument von >>= eine
monadische Aktion sein muss, deren Ergebnis vom rechten Argument verwendet wird,
und daraus eine neue monadische Aktion erstellt wird.
3
Beachte: Da wir kein polymorphes Typsystem zur Verfügung haben, können wir keinen Typ für beliebige Listen
definieren.
D. Sabel, FP-PR, SoSe 2015
13
Stand: 26. März 2015
2 Kurzübersicht über das Projekt
Desweiteren ist erwähnswert, dass das Typsystem bestimmte seq-Ausdrücke verbietet: In
seq a b muss a einen funktionalen Typ haben, d.h. der Typ von a darf weder IO τ noch (MVar τ )
sein. Diese Beschränkung stellt u.a. sicher, dass die monadischen Gesetze in CHF gelten (siehe
(Sabel & Schmidt-Schauß, 2011)).
2.2.3 Beispiele
Wir geben noch einige Beispiel-Ausdrücke an, um zu verdeutlichen, wie in CHF programmiert
wird.
Beispiel 2.2.1. Die Identitätsfunktion kann in CHF durch \x -> x implementiert werden. Beachte, dass wir im Quelltext \ statt λ und -> statt → verwenden. Die Funktion
\x -> \y -> x erwartet zwei Argumente und bildet stets auf das erste Argument ab. Der Ausdruck case b of {True -> e1; False -> e2} verhält sich wie Haskell’s if-then-else-Ausdruck:
if b then e1 else e2.
Beispiel 2.2.2. Ein Ausdruck, der die Liste [True, False, False] von booleschen Werten umdreht, kann
in CHF programmiert werden als
letrec
reverse =
\xs ->
case xs of {
NilBool
-> NilBool;
(ConsBool y ys) -> append (reverse ys) (ConsBool y NilBool)
};
append =
\xs -> \ys ->
case xs of {
NilBool
-> ys;
(ConsBool u us) -> ConsBool u (append us ys)
}
in reverse (ConsBool True (ConsBool False (ConsBool False NilBool)))
Vorher müssen natürlich die Typen Bool und ListBool definiert sein.
Stand: 26. März 2015
14
D. Sabel, FP-PR, SoSe 2015
3
Teil 1: Lexen und Parsen
In diesem ersten Teil geht es darum, das CHF-Programm vom Quelltext (als String) in einen
Haskell-Datentyp zu überführen, der Programme (im Grunde als Baum) darstellt. Dieser Prozess ist im Allgemeinen zweistufig: In der ersten Phasen wird die lexikalische Analyse durchgeführt, die Kommentare und Leerzeichen werden aus dem Quelltext entfernt, und syntaktische
Einheiten werden zu sog. Token zusammengefasst. In der zweiten Phase wird die syntaktische
Analyse durchgeführt, die den Tokenstrom des Lexers erhält und anhand einer (kontextfreien)
Grammatik den Parsebaum (bzw. Syntaxbaum) erzeugt.
3.1 Quelltext lexen
Das Format für den Quelltext ist folgendermaßen: Der Quelltext ist in zwei Abschnitte aufgeteilt:
• Im ersten Abschnitt können (u.U. mehrere) Datentyp-Definitionen stehen. Der Abschnitt
darf jedoch auch leer sein. Jede Datentypdefinition wird mit dem Schlüsselwort data eingeleitet und definiert einen Typ. Die Syntax ist allgemein:
data Typname = Konstruktordefinition1 | . . . | Konstruktordefinitionn
wobei eine Konstruktordefinition von der Form
KonstruktorName Argumenttyp1 . . . Argumenttypm
Stand: 26. März 2015
D. Sabel, FP-PR, SoSe 2015
3 Teil 1: Lexen und Parsen
ist. Sowohl KonstruktorName als auch Typname müssen mit einem Großbuchstaben beginnen und dürfen nur Buchstaben enthalten. Weiterhin muss gelten, dass alle Konstruktornamen verschieden sind (dies wird jedoch erst beim Parsen überprüft). Argumenttypen
sind aufgebaut entsprechend der Grammatik:
Typ ::= (Typ -> Typ) | Typname
D.h. Argumenttypen bestehen aus Funktionstypen und Typkonstruktoren. Insgesamt
müssen alle verwendeten Typnamen in den Argumenttypen definiert sein (was vom Parser geprüft wird). Eine Ausnahme ist der Typ Unit, dieser wird durch den Parser automatisch hinzugefügt, mit der Definition
data Unit = Unit
• Im zweiten Abschnitt (nicht optional, sondern dieser muss vorhanden sein), muss ein
CHF-Ausdruck stehen. Der zweite Teil wird mit dem Schlüsselwort expression eingeleitet. Für CHF-Ausdrücke verwenden wird \ statt λ und -> statt →. Ausdrücke dürfen
beliebig viele Klammern ( und ) benutzen, während geschweifte Klammern { und } nur
für die Abgrenzung der case-Alternativen benutzt werden dürfen. Die Einrückung von
Ausdrücken ist im Gegensatz zu Haskell-Quellcode egal, dafür müssen die Semikolons
zwischen letrec-Bindungen und case-Alternativen stets vorhanden sein. Variablennamen müssen mit einem Kleinbuchstaben beginnen, wobei bel. Buchstaben und Zahlen
anschließend folgen dürfen. Konstruktornamen folgen der Konvention in den Typdefinitionen: Sie beginnen mit einem Großbuchstaben gefolgt von bel. anderen Buchstaben. Für
case-Ausdrücke wird im Quelltext kein Index T für den Typ verwendet, sondern einfach
nur case.
Zudem erlauben wir im Quelltext Zeilenkommentare: Diese werden mit -- eingeleitet und bewirken, dass der Rest der Zeile als Kommentar und nicht als Quellcode erkannt wird.
Mögliche Schlüsselwörter im Quelltext sind daher: expression, data, case, of, letrec, in,
seq, newMVar, putMVar, takeMVar, return und future sowie die Symbole \, >>=, ;, =, ->, |, (, ),
{ und }.
Die Ausgabe des Lexers ist eine Liste von Token. Für die Token soll der folgende Typ verwendet werden, der im Modul CHF.Parse.Lexer definiert ist:
> data Token label
>
TokenKeyword
> | TokenSymbol
> | TokenVar
> | TokenCons
Stand: 26. März 2015
=
String
String
String
String
label
label
label
label
16
D. Sabel, FP-PR, SoSe 2015
3.1 Quelltext lexen
>
>
| TokenUnknown Char label
deriving(Eq,Show)
Der Typ Token ist polymorph über der Variable label definiert, und alle verschiedenen Token
können als zweites Argument einen Wert für label speichern. Desweiteren hat der Typ Token
fünf verschiedene Konstruktoren:
• TokenKeyword zeigt ein gefundenes Schlüsselwort im Text an, das erste Argument ist das
Schlüsselwort.
• TokenSymbol zeigt ein gefundenes Symbol im Text an, das erste Argument ist das Symbol
(als String).
• TokenVar zeigt eine gefundene Variable im Text an, das erste Argument ist der Name der
Variablen.
• TokenCons zeigt einen gefundenen Daten- oder Typkonstruktor an, das erste Argument ist
der Name des Konstruktors.
• TokenUnknown wird benutzt um unbekannte Zeichen im Quelltext als Token darzustellen.
Eine Alternative wäre es, direkt eine Fehlermeldung auszugeben, wenn ein solches Zeichen gefunden wird. Wir überlassen die Fehlerausgabe jedoch dem Parser, der anhand
dieses Tokens eine Fehlermeldung ausgeben wird.
Das label-Argument werden wir verwenden, um die Position des Tokens im Quelltext zu speichern. Hierfür benutzen wir ein Paar aus zwei Ganzzahlen, dass die Zeile und die Spalte des
Tokens im Quelltext angibt. Daher ist die Ausgabe des Lexer eine Liste von Elementen vom Typ
TokenType, wobei:
> type TokenType = Token SourceMark
> type SourceMark = (Int,Int)
I
Aufgabe 1. Implementieren Sie im Modul CHF.Parse.Lexer eine Funktion
lexCHF :: String -> [TokenType], welche die lexikalische Analyse für CHF-Quellcode durchführt,
d.h. syntaktische Einheiten erkennt, Leerzeichen und Umbrüche entfernt, Zeilenkommentare entfernt
und als Ausgabe eine Liste von Tokens liefert, wobei die Token mit ihrer Position im Quelltext markiert
sind. Versuchen Sie den Lexer so zu implementieren, dass er die Eingabe genau einmal durchläuft.
Hinweise:
• In der Bibliothek Data.List finden sich einige hilfreiche Funktionen, die Sie für die Implementierung des Lexers verwenden können.
D. Sabel, FP-PR, SoSe 2015
17
Stand: 26. März 2015
3 Teil 1: Lexen und Parsen
• Sie müssen nicht genau eine Funktion implementieren, d.h. Sie können auch weitere Funktionen
als Hilfsfunktionen definieren.
Einige Beispielaufrufe sind:
*CHF.Parse.Lexer> lexCHF "data Bool = True | False expression \\x -> x"
[TokenKeyword "data" (1,1),TokenCons "Bool" (1,6),TokenSymbol "=" (1,11),
TokenCons "True" (1,13),TokenSymbol "|" (1,18),TokenCons "False" (1,20),
TokenKeyword "expression" (1,26),TokenSymbol "\\" (1,37),
TokenVar "x" (1,38),TokenSymbol "->" (1,40),TokenVar "x" (1,43)]
*CHF.Parse.Lexer> lexCHF "expression 1+1"
[TokenKeyword "expression" (1,1),TokenUnknown ’1’ (1,12),
TokenUnknown ’+’ (1,13),TokenUnknown ’1’ (1,14)]
*CHF.Parse.Lexer> lexCHF "data L = L P expression (case x of {True -> (x True)})"
[TokenKeyword "data" (1,1),TokenCons "L" (1,6),TokenSymbol "=" (1,8),
TokenCons "L" (1,10), TokenCons "P" (1,12),TokenKeyword "expression" (1,14),
TokenSymbol "(" (1,25),TokenKeyword "case" (1,26),TokenVar "x" (1,31),
TokenKeyword "of" (1,33),TokenSymbol "{" (1,36),TokenCons "True" (1,37),
TokenSymbol "->" (1,42),TokenSymbol "(" (1,45),TokenVar "x" (1,46),
TokenCons "True" (1,48),TokenSymbol ")" (1,52),TokenSymbol "}" (1,53),
TokenSymbol ")" (1,54)]
*CHF.Parse.Lexer> lexCHF "expression (return True) >>= \\x -> putMVar x True"
[TokenKeyword "expression" (1,1),TokenSymbol "(" (1,12),
TokenKeyword "return" (1,13),TokenCons "True" (1,20),TokenSymbol ")" (1,24),
TokenSymbol ">>=" (1,26),TokenSymbol "\\" (1,30),TokenVar "x" (1,31),
TokenSymbol "->" (1,33),TokenKeyword "putMVar" (1,36),TokenVar "x" (1,44),
TokenCons "True" (1,46)]
Implementieren Sie Funktionen getLabel :: Token label -> label, die den Label eines Tokens
extrahiert und showToken :: Token label -> String, die den zum Token passenden ursprünglichen Text anzeigt. Einige Beipspielaufrufe sind:
*CHF.Parse.Lexer>
(1,38)
*CHF.Parse.Lexer>
"x"
*CHF.Parse.Lexer>
"letrec"
*CHF.Parse.Lexer>
(20,20)
getLabel (TokenVar "x" (1,38))
showToken (TokenVar "x" (1,38))
showToken (TokenKeyword "letrec" (20,20))
getLabel (TokenKeyword "letrec" (20,20))
J
Stand: 26. März 2015
18
D. Sabel, FP-PR, SoSe 2015
3.2 Parsen mit Happy
3.2 Parsen mit Happy
Ziel des Parsens ist es, anhand einer Grammatik die Gültigkeit der Eingaben (Datentypdefinitionen und Ausdruck) zu erkennen und diese als Syntax- bzw. Parsebaum darzustellen. Einen Parser per Hand zu implementieren ist eher kompliziert. Daher bietet es sich
an, einen Parsergenerator zu verwenden. Dieser erstellt anhand einer Parserdefinitionsdatei automatisch einen Parser. In Haskell ist happy1 der wohl bekannteste Parsergenerator. In
der Datei CHF/Parse/Parser.ly befindet sich die Parserdefinition aus der happy die Datei
CHF/Parse/Parser.hs erstellen wird, die das Modul CHF.Parse.Parser darstellt und insbesondere die Funktionen
parseCHF :: String -> ParserOutput
parse
:: [TokenType] -> ParserOutput
exportieren. Die Funktion parse ist der eigentliche Parser, der die Ausgabe des Lexers als Eingabe erhält und ein Parseergebnis liefert (den Typ ParserOutput erläutern wir im Anschluss).
Die Funktion parseCHF verknüpft schon den Parser mit dem Lexer und erwartet daher den
Quelltext als Eingabe. Die Implementierung von parseCHF ist:
> parseCHF inp = parse $ (lexCHF "data Unit = Unit\n") ++ lexCHF inp
Neben dem eigentlich Quelltext wird an dieser Stelle die Datentypdefintion für Unit hinein
„geschmuggelt“.
3.2.1 Ausgabe des Parsers
Wir betrachten nun zunächst die Ausgabe des Parsers, also den Haskelldatentyp, der das Parseergebnis bei erfolgreichem Parsen darstellen soll (diese Definitionen befinden sich in Parser.ly):
> type ParserOutput = ParseTree () ConsName VarName
> data ParseTree a cname vname =
>
ParseTree [TDef cname cname] (Expr a cname vname)
> deriving(Eq,Show)
> type ConsName = (SourceMark,String)
> type VarName = (SourceMark,String)
ParserOutput ist ein ParseTree, wobei ein ParseTree im Grunde ein Paar ist, bestehend aus
einer Liste von Typdefinitionen und einem Audruck (das passt gerade zum Quelltext). Für Konstrukturnamen wird dabei der Typ ConsName verwendet, der neben dem eigentlichen Namen
1
http://haskell.org/happy
D. Sabel, FP-PR, SoSe 2015
19
Stand: 26. März 2015
3 Teil 1: Lexen und Parsen
noch die Markierung im Quelltext (Zeile,Spalte) mit abspeichert, für Variablennamen wird der
Typ VarName verwendet, der neben dem Variablennamen noch die Position im Quelltext speichert. Der Sinn dabei ist, auch später noch nach dem Parsen einigermaßen sinnvolle Fehlermeldungen ausgeben zu können.
Es fehlt noch die Defintion der Typen TDef und Expr. Diese sind im Modul
CHF.CoreL.Expression definiert. Wir erörtern zunächst den Expr-Typ:
> data Expr label cname v =
>
Var v
>
| Lam v (Expr label cname v)
>
| App (Expr label cname v) (Expr label cname v)
>
| Seq (Expr label cname v) (Expr label cname v)
>
| Cons (Either MAction cname) [Expr label cname v]
>
| Case (Expr label cname v) [CAlt label cname v]
>
| Letrec [Binding label cname v] (Expr label cname v)
>
| Label label (Expr label cname v)
> deriving(Eq,Show)
> data MAction = Fork | Return | Take | Put | New | Bind
> deriving(Eq,Show)
> data CAlt label cname v =
> CAlt cname [v] (Expr label cname v)
> deriving(Eq,Show)
> data Binding label cname v =
> v :=: (Expr label cname v)
> deriving(Eq,Show)
Der Datentyp Expr ist polymorph über einer beliebigen Markierung label, dem Typ der Konstruktornamen consname und dem Typ der Variablennamen v definiert. Wir erörtern, wie CHFAusdrücke mithilfe der einzelnen Konstruktoren dargestellt werden.
• Var v dient zur Darstellung einer Variablen, z.B. soll x später durch Var "x", bzw. mit
Positionsmarkierung als Var ((1,1),"x") dargestellt werden.
• Lam v e stellt eine Abstraktion dar, d.h. λx → e wird zu Lam "x" e.
• App e1 e2 stellt eine Anwendung dar, d.h. (e1 e2 ) wird dargestellt durch App e1 e2 .
• Seq e1 e2 stellt einen seq-Ausdruck dar, d.h. seq e1 e2 wird dargestellt durch Seq e1 e2
Stand: 26. März 2015
20
D. Sabel, FP-PR, SoSe 2015
3.2 Parsen mit Happy
• Cons c [args] stellt entweder eine Konstruktorapplikation oder einen (gesättigten)
monadischen Operator dar. Diese Doppelt-Verwendung wird später nützlich sein, da
in der abstrakten Maschine die monadischen Operationen teilweise wie Konstruktoren behandelt werden. Zur Unterscheidung, zwischen Datenkonstruktoren und Monadischen Operatoren wird der Datentyp Either verwendet2 . Normale Konstruktoranwendungen werden durch Right dargestellt, d.h. (ci e1 . . . en ) wird dargestellt durch
Cons (Right "c_i") [e1,...,en], wobei wir am Anfang anstelle von "c_i" den Konstruktornamen zusammen mit seiner Position speichern, also z.B. ((1,4),"c_i").
Monadische Aktionen werden durch Left angewendet auf die entsprechende Aktion dargestellt. Für die Aktionen ist der Datentyp MAction vordefiniert, wobei
Fork gerade future, Return gerade return, Take gerade takeMVar, Put gerade
putMVar, New gerade newMVar und Bind gerade >>= darstellt. Z.B. wird e1 >>= e2
dargestellt als Cons (Left Bind) [e1,e2] und putMVar e1 e2 wird dargestellt als
Cons (Left Put) [e1,e2].
• Case e [alt] stellt gerade einen case-Ausdruck dar, wobei [alt] die Liste der caseAlternativen ist. Diese werden durch den Typ CAlt dargestellt. Eine Alternative
((c x1 . . . xn ) → e) wird daher dargestellt durch CAlt "c" [x1,...,xn] e. Wir betrachten
zwei Beispiele: Der Ausdruck
case e of {True → False; False → True}
wird dargestellt durch (ohne Positionsmarkierungen):
Case e
[CAlt "True" [] (Cons (Right "False") [])
,CAlt "False" [] (Cons (Right "True") [])]
Der Ausdruck
case e of {(ConsBool y ys) → return ys; NilBool → return e}
wird dargestellt durch:
(Case e
[CAlt "ConsBool" ["y","ys"] (Cons (Left Return) ["ys"])
,CAlt "NilBool" [] (Cons (Left Return) [e]])
• Letrec binds e dient zur Darstellung von letrec-Ausdrücken. Hierbei ist binds eine
Liste von Bindungen, wobei eine Bindung durch den Typ Binding implementiert ist, die
2
Dieser ist in Haskell vordefiniert als data Either a b = Left a | Right b
D. Sabel, FP-PR, SoSe 2015
21
Stand: 26. März 2015
3 Teil 1: Lexen und Parsen
einen infix-Konstruktor :=: zur Verfügung stellt. z.B. wird letrec x1 = e1 , . . . , xn =
en in e dargestellt als: (Letrec ["x1" :=: e1, ..., "xn" :=: en] e)
• Label label e dient dazu einen beliebigen Unterausdruck mit einer Markierung zu versehen. Während des Parsens werden wir dieses Konstrukt nicht benutzen, allerdings später während dem Typcheck, um Typen von Unterausdrücken zu speichern.
Die erste Komponente von ParseTree ist eine Liste von Datentypdefinitionen vom Typ TDef,
der Parser übersetzt die data-Anweisungen in diese Liste. Der Typ TDef ist definiert als:
> data TDef tname cname = TDef tname [(cname,Type tname)]
>
> deriving(Eq,Show)
> data Type tname =
>
TC tname
>
| (Type tname) :->: (Type tname)
>
| TVar String
>
| TIO (Type tname)
>
| TMVar (Type tname)
> deriving(Ord,Eq,Show)
TDef ist polymorph über den Namen der Typkonstruktoren tname und den Namen der Datenkonstruktoren cname definiert. Ein Objekt vom Typ TDef stellt genau eine komplette dataAnweisung dar. Das erste Argument ist der definierte Typ, anschließend folgt eine Liste mit
Elementen der Form
(Konstruktorname, Typ des Konstruktors).
Der Typ ist dabei durch den Datentyp Type dargestellt:
• TC tname stellt einen Typkonstruktor dar, z.B wird Bool als TC "Bool" dargestellt.
• t1 :->: t2 stellt einen Funktionstypen dar, d.h. den Typ t1 → t2 .
• TIO t stellt einen IO-Typen dar. z.B, wird IO Bool als TIO (TC "Bool") dargestellt.
• TMVar stellt einen MVar-Typen dar, z.B. wird MVar (Bool -> ListBool) durch
TMVar (TC "Bool" :->: TC "ListBool") dargestellt.
• TVar stellt eine Typvariable dar. Diese können im Quelltext nicht auftreten, werden aber
beim Typcheck benötigt.
Wir geben noch ein vollständiges Beispiel an: Wenn die Eingabe ist
Stand: 26. März 2015
22
D. Sabel, FP-PR, SoSe 2015
3.2 Parsen mit Happy
data Bool = True
| False
data ListBool = NilBool
| ConsBool Bool ListBool
expression
(seq True True)
So liefert der Parser (mit allen Markierungen):
ParseTree
[TDef ((1,6),"Bool")
[(((1,13),"True"),TC ((1,6),"Bool")),
(((2,13),"False"),TC ((1,6),"Bool"))],
TDef ((3,6),"ListBool")
[(((3,17),"NilBool"),TC ((3,6),"ListBool")),
(((4,17),"ConsBool"),
TC ((4,26),"Bool") :->: (TC ((4,31),"ListBool") :->: TC ((3,6),"ListBool")))],
TDef ((1,6),"Unit")
[(((1,13),"Unit"),TC ((1,6),"Unit"))]
]
(Seq
(Cons (Right ((7,6),"True")) [])
(Cons (Right ((7,11),"True")) [])
)
Beachte, dass die Typdefinition für Unit automatisch hinzugefügt wurde.
3.2.2 Die Parserdefinition
Die Parserdefinition ist größtenteils in der Datei Parser.ly vorgegeben, da es ziemlich aufwändig ist, eine Grammatik zu finden, die frei von Konflikten3 ist. Allerdings müssen beim Parsen
noch einige zusätzliche Überprüfungen durchgeführt werden, die noch implementiert werden
müssen.
Wir betrachten zunächst die Tokendefinition des Parsers, diese bildet einzelnen Token auf
„interne Symbole“ ab. Diese internen Symbole werden dann in der eigentlichen kontextfreien
Grammatik verwendet:
>
>
>
>
>
3
%token
var
cons
’|’
’\\’
{
{
{
{
TokenVar _ _ }
TokenCons _ _ }
TokenSymbol
"|" _ }
TokenSymbol
"\\" _ }
Genauer gesagt: eine Grammatik die eindeutig und nicht mehrdeutig ist.
D. Sabel, FP-PR, SoSe 2015
23
Stand: 26. März 2015
3 Teil 1: Lexen und Parsen
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
’>>=’
’;’
’=’
’->’
’(’
’{’
’)’
’}’
’case’
’of’
’letrec’
’in’
’seq’
’newMVar’
’putMVar’
’takeMVar’
’future’
’return’
’data’
’expr’
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
TokenSymbol
TokenSymbol
TokenSymbol
TokenSymbol
TokenSymbol
TokenSymbol
TokenSymbol
TokenSymbol
TokenKeyword
TokenKeyword
TokenKeyword
TokenKeyword
TokenKeyword
TokenKeyword
TokenKeyword
TokenKeyword
TokenKeyword
TokenKeyword
TokenKeyword
TokenKeyword
">>=" _ }
";" _ }
"=" _ }
"->" _ }
"(" _ }
"{" _ }
")" _ }
"}" _ }
"case" _ }
"of" _ }
"letrec" _ }
"in" _ }
"seq" _ }
"newMVar" _ }
"putMVar" _ }
"takeMVar" _ }
"future" _}
"return" _ }
"data" _ }
"expression" _ }
Beachte, dass das TokenUnknown nicht abgebildet wird, da es in der Grammatik nicht vorkommt, und daher immer zu einem Fehler führt.
Anschließend folgt eine Liste der Assoziativitäten und Prioritäten der Konstrukte, wobei sich
die Prioritäten durch die Reihenfolge in der Aufzählung ergeben:
>
>
>
>
>
>
>
>
>
>
>
>
>
%right ’->’
%right ’>>=’
%left ’seq’
%nonassoc ’in’
%nonassoc ’of’
%nonassoc ’case’
%nonassoc ’letrec’
%nonassoc ’future’
%nonassoc ’takeMVar’
%nonassoc ’putMVar’
%nonassoc ’return’
%nonassoc ’newMVar’
%%
Stand: 26. März 2015
24
D. Sabel, FP-PR, SoSe 2015
3.2 Parsen mit Happy
Schließlich folgt die eigentliche Grammatik. Beachte, dass diese “so kompliziert” ist, damit der Programmierer nicht alle Klammern angeben muss, z.B. wird
(\x -> \y -> x y) (\z -> z) True erkannt, obwohl nicht alle Anwendungen explizit
geklammert sind. Das Startsymbol der Grammatik ist SOURCECODE, in geschweiften Klammern
steht hinter jeder Produktion, was mit dem Ergebnis gemacht werden soll. Z.B. wird nach dem
Parsen der Typdefinitionen (Nichtterminal TDEFS), das Ergebnis an die Funktion checkTDEFS
übergeben, um weitere Tests auszuführen.
> SOURCECODE :: {ParserOutput}
> SOURCECODE : EXPR
{ checkParseTree $ ParseTree [] $1 }
>
| TDEFS EXPR { checkParseTree $ ParseTree $1 $2 }
Grammatik f"ur die Datentypdefinitionen:
>
>
>
>
>
>
>
>
>
>
>
>
TDEFS
: TDEFSIT
TDEFSIT : TDEF
| TDEF TDEFSIT
TDEF
: ’data’ cons ’=’ CDEFS
{
{
{
{
CDEFS
{
{
{
{
{
{
CDEF
TYPE
> TK
>
:
|
:
|
:
|
CDEF
CDEF ’|’ CDEFS
cons
cons TYPE
TK
TK TYPE
: cons
| ’(’ TK ’->’ TK ’)’
checkTDEFS $1 }
[$1] }
$1:$2 }
(TDef
(mkCons $2)
(listsToTypes $4 (TC $ mkCons $2))) }
[$1] }
$1:$3 }
(mkCons $1,[]) }
(mkCons $1,$2) }
[$1] }
$1:$2 }
{ TC $ mkCons $1 }
{ $2 :->: $4 }
Grammatik f"ur Ausdr"ucke:
> EXPR
> EXPR
> REXPR
>
>
>
:: {Expr () ConsName VarName}
: ’expr’ REXPR
: ’letrec’ BINDS ’in’ REXPR
| ’\\’ var ’->’ REXPR
| REXPR ’>>=’ REXPR
| AEXPR
{
{
{
{
{
$2 }
checkBinds $4 $2 $1 }
Lam (mkVar $2) $4 }
Cons (Left Bind) [$1,$3] }
$1 }
> AEXPR
>
>
>
:
|
|
|
{
{
{
{
$1 }
Cons (Right (mkCons $1)) $2 }
mkAPP $1 $2}
Cons (Right (mkCons $1)) [] }
IEXPR
cons CONSARGS
IEXPR CONSARGS
cons
D. Sabel, FP-PR, SoSe 2015
25
Stand: 26. März 2015
3 Teil 1: Lexen und Parsen
> BINDS
>
: BIND
| BIND ’;’ BINDS
{ [$1] }
{ $1:$3 }
> BIND
: var ’=’ REXPR
{ (mkVar $1) :=: $3 }
> IEXPR :
>
|
>
>
>
|
>
|
>
>
>
|
>
|
>
>
>
|
>
>
>
|
>
|
>
>
>
|
>
|
>
>
>
|
>
|
>
>
>
|
>
|
>
|
> ALTS :
>
|
> ALT
:
>
>
> PAT
:
>
|
> VARARGS
>
>
>
’seq’ IEXPR IEXPR
’(’ ’seq’ cons cons ’)’
{ Seq $2 $3}
{ Seq
(Cons (Right (mkCons $3)) [])
(Cons (Right (mkCons $4)) [])}
’newMVar’ IEXPR
{ Cons (Left New) [$2] }
’(’ ’newMVar’ cons ’)’
{ Cons
(Left New)
[(Cons (Right (mkCons $3)) [])] }
’putMVar’ IEXPR IEXPR
{ Cons (Left Put) [$2,$3] }
’(’ ’putMVar’ cons IEXPR ’)’
{ Cons
(Left Put)
[(Cons (Right (mkCons $3)) []),$4] }
’(’ ’putMVar’ IEXPR cons’)’
{ Cons
(Left Put)
[$3,(Cons (Right (mkCons $4)) [])] }
’takeMVar’ IEXPR
{ Cons (Left Take) [$2] }
’(’ ’takeMVar’ cons ’)’
{ Cons
(Left Take)
[(Cons (Right (mkCons $3)) [])] }
’return’ IEXPR
{ Cons (Left Return) [$2] }
’(’ ’return’ cons ’)’
{ Cons
(Left Return)
[(Cons (Right (mkCons $3)) [])] }
’future’ IEXPR
{ Cons (Left Fork) [$2] }
’(’ ’future’ cons ’)’
{ Cons
(Left Fork)
[(Cons (Right (mkCons $3)) [])] }
’(’ REXPR ’)’
{ $2 }
var
{ Var (mkVar $1) }
’case’ REXPR ’of’ ’{’ ALTS ’}’ { checkAlts $1 $2 $5 }
ALT
{ [$1] }
ALT ’;’ ALTS
{ $1:$3 }
PAT ’->’ REXPR
{ checkAlt
$2
(CAlt (fst $1) (snd $1) $3) }
cons
{ (mkCons $1,[]) }
’(’ cons VARARGS ’)’
{ (mkCons $2,$3) }
: var
{ [mkVar $1] }
| var VARARGS
{ (mkVar $1):$2}
CONSARGS : IEXPR
| IEXPR CONSARGS
Stand: 26. März 2015
{ [$1] }
{ $1:$2}
26
D. Sabel, FP-PR, SoSe 2015
3.2 Parsen mit Happy
>
>
| cons CONSARGS
| cons
{ Cons (Right $ mkCons $1) []:$2}
{ [Cons (Right $ mkCons $1) []]}
Die Funktion checkTDEFS prüft, ob alle geparsten Typkonstruktoren und Datenkonstruktoren in den data-Definitionen stimmig sind: Typen und Datenkonstruktoren werden nur einmal
definiert, verwendete Datenkonstruktoren und Typen sind auch definiert. Die Konstruktion der
Typen aus der Definition heraus, wird durch die Funktion listToTypes erledigt. Diese Funktionen sind bereits vorgegeben in Parser.ly. Beispiele, für checkTDEFS einen Fehler während
des Parsens generiert, sind z.B.
parseCHF "data ListBool = NilBool | ConsBool Bool ListBool expression NilBool"
*** Exception: type "Bool" not defined
Zeile :1 Spalte: 36
parseCHF "data Bool = True | False data NochmalBool = True | False expression True"
ParseTree *** Exception: data constructor False already used
Zeile: 1 Spalte: 53
Die Funktionen mkCons und mkVar erstellen aus dem Token einen entsprechenden Konstruktor oder eine Variable vom Expr-Typ, wobei diese mit Positionsmarkierungen versehen sind.
Beim Parsen einer case-Alternative, eines case-Ausdrucks und eines letrec-Ausdrucks sind
weitere Tests nötig (die Funktionen checkAlt, checkAlts und checkBinds, diese sollen in der
nächsten Aufgabe implementiert werden.)
I Aufgabe 2. Implementieren Sie im vierten Teil der Parser-Spezifikation in der Datei Parser.ly die
folgenden Funktionen:
• checkAlt :: TokenType
-> CAlt () ConsName VarName
-> CAlt () ConsName VarName
Die das Token einer case-Alternative, und die case-Alternative selbst erhält, und prüft, ob alle
Variablen des Patterns verschieden sind. Ist dies nicht der Fall, so wird mittels error eine Fehlermeldung ausgedruckt, die möglichst genau angibt, wo und warum der Fehler aufgetreten ist (diese
Information finden Sie z.B. im Token). Sind alle Variablen verschieden, so wird die Alternative als
Ausgabe zurück geliefert.
Ein Beispiel ist die Eingabe
data Bool = True | False
data ListBool = NilBool | ConsBool Bool ListBool
expression
case NilBool of {(ConsBool y y) -> True; NilBool -> False}
Wendet man parseCHF auf diese Eingabe an, so sollte durch checkAlt der Fehler bemerkt werden,
dass y doppelt verwendet wird, und eine Fehlermeldung der Form
*** Exception: repeated variable "y"in a pattern of a case alternative before
Zeile: 4 Spalte: 34
D. Sabel, FP-PR, SoSe 2015
27
Stand: 26. März 2015
3 Teil 1: Lexen und Parsen
generiert werden.
• checkAlts :: TokenType
-> (Expr () ConsName VarName)
-> [CAlt () ConsName VarName]
-> (Expr () ConsName VarName)
die das Token zu einem case-Ausdruck, einen Ausdruck e und case-Alternativen alts erhält.
Daraus konstruiert checkAlts den case-Ausdruck case e of alts, wobei vorher geprüft werden
muss, ob doppelte Alternativen (also Alternativen für den gleichen Konstruktor) vorhanden sind.
Ist dies der Fall, so soll mittels error eine aussagekräftige Fehlermeldung ausgegeben werden.
Ein Beispielprogramm ist
data Bool = True | False
expression
case True of {False -> True; False -> True}
Der Aufruf von parseCHF soll mittels checkAlts die folgende Fehlermeldung generieren:
*** Exception: multiple case-alternatives for constructor "False"
Zeile: 3 Spalte: 2
• checkBinds :: Expr () ConsName VarName
-> [Binding () ConsName VarName]
-> TokenType -> Expr () ConsName VarName
die einen Ausdruck e, letrec-Bindungen Env und das Token des zugehörigen letrec-Ausdrucks
erhält und den Ausdruck letrec Env in e erzeugt, wobei zuvor überprüft werden muss, ob alle
Bindungsvariablen der Bindungen verschieden sind. Ist dies der Fall, so soll mit error ein aussagekräftiger Fehler erzeugt werden.
Ein Beispiel ist:
parseCHF "expression letrec x = x; x = y in x"
*** Exception: multiple bindings in letrec expression for variable ’x’
Zeile: 1 Spalte: 12
J
Die Parserdefinition enthält noch einige weitere nützliche Funktionen, wie z.B. happyError,
die immer aufgerufen wird, sobald ein Parse-Fehler auftritt. Wir gehen nicht weiter auf diese
Funktion ein.
Der so vom Parser gelieferte Syntaxbaum ist unter Umständen immer noch nicht syntaktisch
richtig. Dies liegt daran, dass manche syntaktische Bedingung nicht mehr mit einer kontextfreien Grammatik ausgedrückt werden kann. Zwei wesentliche Fehler, die noch nach dem Parsen
in Ausdrücken vorhanden sein können, sind:
Stand: 26. März 2015
28
D. Sabel, FP-PR, SoSe 2015
3.2 Parsen mit Happy
• Im Ausdruck wird ein Konstruktor verwendet, der in den Datentyp-Definitionen nicht
definiert wurde. Betrachte z.B. den Quelltext
expression case True of {True -> False; False -> True}
Die Konstruktoren True und False werden verwendet, obwohl sie gar nicht definiert wurden. Man kann diesen Test jedoch nicht mit der kontextfreien Grammatik des Parser ausdrücken, da die data-Anweisungen selbst Teil der Grammatik sind.
• Im Ausdruck können die Konstruktoren mit falscher Stelligkeit verwendet werden, z.B.
könnte im Ausdruck (True False) stehen, obwohl True ein nullstelliger Konstruktor ist
und daher keine Argumente erwartet
Der zweite Fehler wird im Rahmen des Typchecks im nächsten Teil erkannt. Der erste Fehler
wird in der nächsten Aufgabe bearbeitet:
I Aufgabe 3. Implementieren Sie in der Parser-Definition Parser.ly eine Funktion:
checkParseTree :: ParserOutput -> ParserOutput, die den geparsten Ausdruck rekursiv
durchläuft und für alle dort auftretenden Konstruktoren (auch in den Pattern der case-Alternativen)
prüft, ob die Konstruktornamen in den Typdefinitionen (erste Komponente von ParseTree) definiert
werden. Ist dies der Fall, so wird der ParseTree unverändert zurückgegeben. Anderenfalls soll eine
aussagekräftige Fehlermeldung (mit Position des falschen Namens) generiert werden.
Ein Beispiel ist:
parseCHF "expression True"
*** Exception: Constructor "True" not defined
Zeile: 1 Spalte: 12
J
I Aufgabe 4. Erstellen Sie mit happy den Parser, indem Sie
happy -iinfo.txt Parser.ly
aufrufen. Dies erstellt die Haskell-Quelltext-Datei Parser.hs sowie eine Datei info.txt die Auskunft
über die Zustände des erstellten Shift-Reduce-Parsers gibt. Schauen Sie sich den Inhalt von info.txt
an, um die Wirkungsweise des Parsers (etwas) zu verstehen.
Testen Sie anschließend den Parser mit einigen Eingaben. Welche Ausdrücke können geparst werden?
Welche Klammern kann der Benutzer weglassen, welche nicht?
J
D. Sabel, FP-PR, SoSe 2015
29
Stand: 26. März 2015
4
Teil 2: Type-Check und Transformation in einfachere
Syntax
4.1 Das Typsystem
Für einen CHF-Ausdruck e schreiben wir e :: τ , wenn e den Typ τ hat. In Abbildung 4.1 sind
Typisierungsregeln für CHF angegeben. Hierbei ist Γ eine Typumgebung, die die Typen von
Variablen festhält, d.h. Γ ist eine Menge der Form {x1 :: τ1 , . . . , xn :: τn }. Die Notation Γ ∪ S
meint, dass wir zu Γ neue Typen für neue Variablen hinzufügen, d.h. die Mengen der Variablen
in Γ und S sind stets disjunkt.
Die Notation Γ ` e :: τ bedeutet, dass man den Ausdruck e mit dem Typ τ für die Typumgebung Γ typisieren kann. Die Bruchstrichnotation in den Regeln ist so zu deuten:
Voraussetzung
Konsequenz
D.h. man muss stets die Voraussetzung zeigen, um die Konsequenz schließen zu dürfen.
Umgekehrt kann man daraus auch mehr oder weniger direkt den Algorithmus zum TypeChecking ablesen: Um einen Ausdruck zu typisieren, zerlegt man diesen stückweise anhand
seiner Syntax und baut damit implizit den Herleitungsbaum von unten nach oben auf.
Wir beschreiben kurz die Regeln:
• Die ersten sechs Regeln dienen der Typisierung von monadischen Ausdrücken.
D. Sabel, FP-PR, SoSe 2015
Stand: 26. März 2015
4.1 Das Typsystem
Γ ` e :: τ
Γ ` e1 :: IO τ1 ,
Γ ` return e :: IO τ
Γ ` e :: MVar τ
Γ ` e2 :: τ1 → IO τ2
Γ ` e1 >>= e2 :: IO τ2
Γ ` e1 :: MVar τ,
Γ ` takeMVar e :: IO τ
Γ ` future e :: IO τ
Γ ` e2 :: τ
Γ ` putMVar e1 e2 :: IO ()
∀i : Γ ` ei :: τi und c :: τ1 → . . . → τn → τn+1
Γ ` e :: τ
Γ ` newMVar e :: IO (MVar τ )
Γ ` e1 :: τ1 → τ2 ,
Γ ` (c e1 . . . ear(c) ) :: τn+1
Γ ` e2 :: τ1
Γ ` (e1 e2 ) :: τ2
Γ ` e1 :: τ1 , Γ ` e2 :: τ2
τ1 = τ3 → τ4 oder τ1 = T
Γ ∪ {x :: τ1 } ` e :: τ2
Γ ` (λx → e) :: τ1 → τ2
Γ ` e :: IO τ
Γ ∪ {x :: τ } ` x :: τ
Γ ` (seq e1 e2 ) :: τ2
Γ ` e :: T,
∀i : Γ ∪ {x1,i :: τ1,i , . . . xni ,i :: τni ,i } ` (ci x1,i . . . xni ,i ) :: T,
∀i : Γ ∪ {x1,i :: τ1,i , . . . xni ,i :: τni ,i } ` ei :: τ2
Γ ` (caseT e of(c1 x1,1 . . . xn1 ,1 → e1 ) . . . (cm x1,m . . . xnm ,m → em )) :: τ2
∀i : Γ ∪ {x1 :: τ1 , . . . xn :: τn } ` ei :: τi ,
Γ ∪ {x1 :: τ1 , . . . xn :: τn } ` e :: τ
Γ ` (letrec x1 = e1 , . . . xn = en in e) :: τ
Abbildung 4.1: Typisierungsregeln
• Die siebte Regel dient der Typisierung einer Konstruktoranwendung. Hierbei muss man
den Typ des Konstruktors (also τ1 → . . . → τn ) bereits kennen. Für unseren Typcheck ist
dies kein Problem, da der Parsebaum bereits die Typen der durch die data-Anweisungen
definierten Konstruktoren kennt.
• Die achte Regel dient der Typisierung von Anwendungen.
• Abstraktionen werden mit der neunten Regel typisiert. Will man λx → e typisieren, so
muss man (rekursiv) den Rumpf von e typisieren. In e kann x als freie Variable vorkommen. Deshalb wird in die Typumgebung Γ ein Typ für x hinzugefügt.
• Die zehnte Regel ist ein Axiom (die Voraussetzung ist leer). Sie dient der Typisierung von
Variablen, wobei diese geschieht, indem einfach der entsprechende Typ in der Typumgebung nachgeschaut wird.
• Die elfte Regel dient der Typisierung von seq-Ausdrücken, wobei die beiden Beschränkungen τ1 = τ3 → τ4 oder τ1 = T gerade ausdrücken, dass der Typ τ1 des ersten Arguments von seq kein IO-Typ und auch kein MVar-Typ sein darf.
D. Sabel, FP-PR, SoSe 2015
31
Stand: 26. März 2015
4 Teil 2: Type-Check und Transformation in einfachere Syntax
• Die zwölfte Regel dient der Typisierung von case-Ausdrücken. Wir erläutern die drei Zeilen in der Voraussetzung: Die erste Zeile besagt, dass der T -Index in caseT gerade zum
Typ des Ausdrucks e passen muss und dass e selbst typisierbar sein muss. In der Implementierung werden wir den Abgleich zwischem dem Index und dem Typ von e nicht
durchführen, da wir keinen T -Index in case-Ausdrücken verwenden. Für die zweite und
die dritte Zeile wird Γ jeweils um Typen für die Pattern-Variablen einer case-Alternativen
erweitert und schließlich werden die Pattern (Zeile 2) und die rechten Seiten der Alternativen getypt. Durch Verwendung der gleichen Symbole (Pattern müssen alle den Typ T
erhalten, die rechten Seite müssen alle den Typ τ2 erhalten) wird sichergestellt, dass alle
rechten Seiten den gleichen Typ haben, und dass alle Pattern den selben Typ wie e haben.
• Die letzte Regel dient der Typisierung letrec-Ausdrücken. Hierbei ist die Behandlung der
Variablen zu beachten: Da das letrec rekursiv sein darf, können die Variablen x1 , . . . , xn
in e1 , . . . , en und e auftauchen. Da Γ für die Typisierung all dieser Unterausdrücke um die
gleichen Typannahmen für alle xi erweitert wird, wird sichergestellt, dass jedes xi stets
mit dem gleichen Typ typisiert wird.
Für den eigentlichen Typisierungsalgorithmus wird mit der Annahme Γ = ∅ gestartet, da
wir nur geschlossene Ausdrücke als Programme betrachten. Der Parser prüft allerdings nicht, ob
ein Ausdruck keine freien Variablen enthält. Typisiert man einen Ausdruck der freie Variablen
enthält (z.B. λx → y) beginnend mit Γ = ∅, so stößt man während der Typisierung auf die freie
Variable, d.h. man möchte das Axiom für Variablen anwenden, aber für die Variable befindet
sich kein Eintrag in Γ. In diesem Fall soll der Typchecker eine Fehlermeldung liefern, die die
Position der freien Variablen ausgibt und erläutert, dass es sich um eine freie Variable handelt.
Wir geben einige Beispiele für Typisierungen an.
Beispiel 4.1.1. Wir nehmen an, dass die Datentypen Bool und ListBool wie vorher definiert sind.
Der Ausdruck λx → x lässt sich z.B. mit Bool → Bool typisieren:
x :: Bool ` x :: Bool
∅ ` λx → x :: Bool → Bool
Der Ausdruck λx → x lässt sich ebenso mit ListBool → ListBool typisieren:
x :: ListBool ` x :: ListBool
∅ ` λx → x :: ListBool → ListBool
Der Ausdruck ((λx → λy → x) True False) lässt sich mit Bool typisieren:
Stand: 26. März 2015
32
D. Sabel, FP-PR, SoSe 2015
4.1 Das Typsystem
{x :: Bool, y :: Bool} ` y :: Bool
{x :: Bool} ` (λy → x) :: Bool → Bool
True :: Bool
∅ ` (λx → λy → x) :: Bool → Bool → Bool
∅ ` True :: Bool
∅ ` ((λx → λy → x) True) :: Bool → Bool
False :: Bool
∅ ` False :: Bool
∅ ` ((λx → λy → x) True False) :: Bool
Der Ausdruck ConsBool True NilBool lässt sich mit ListBool typisieren:
True :: Bool
∅ ` True :: Bool
NilBool :: ListBool
∅ ` NilBool :: ListBool und ConsBool :: Bool → ListBool → ListBool
∅ ` (ConsBool True NilBool) :: ListBool
Die Typisierung der Identitätsfunktion λx → x zeigt ein Problem, das entsteht, wenn man die
so formulierten Typisierungsregeln automatisieren will. Man muss den Typ für x raten, um mit
der Voraussetzung ∅ ∪ {x ::?} ` x . . . weiter rechnen zu können. Der Ausweg ist, Typvariablen
– also Variablen, die für Typen stehen – zu benutzen. Man nimmt für den Typ von x zunächst
eine Variable an und löst später die Bedingungen an die Variable, die entstehen können.
Um dies formal zu fassen erweiteren wir die Typregeln, um eine weitere Komponente E:
Hierbei handelt es sich um Gleichungen zwischen Typen (mit Typvariablen). Diese werden bei
der Typisierung aufgesammelt und später gelöst. Wir schreiben Γ ` e :: τ, E und meinen damit:
In der Typumgebung Γ kann für den Ausdruck e der Typ σ(τ ) hergeleitet werden, wenn σ die
Lösung der Gleichungen aus E ist.
Da wir nun Typvariablen erlauben, können wir nicht mehr einfach fordern, dass in einer Voraussetzung ein bestimmter Typ stehen muss, dort kann nun stets auch eine Typvariable entstehen, die noch gelöst werden muss. Betrachte, z.B. die Regel für die Anwendung:
Ohne Typvariablen konnten wir schreiben:
Γ ` e1 :: τ1 → τ2 ,
Γ ` e2 :: τ1
Γ ` (e1 e2 ) :: τ2
Mit Typvariablen kann nun allerdings der Fall aufreteten, dass der Typ von e1 nicht τ1 → τ2 ist,
sondern eine Typvariable ist (die man mit τ1 → τ2 instantiieren dürfte). Dieses Problem wird in
den Typregeln durch Verwendung von Typgleichungen umgangen:
Γ ` e1 :: τ1 , E1 und Γ ` e2 :: τ2 , E2
.
Γ ` (e1 e2 ) :: α, E1 ∪ E2 ∪ {τ1 = τ2 → α}
D. Sabel, FP-PR, SoSe 2015
33
Stand: 26. März 2015
4 Teil 2: Type-Check und Transformation in einfachere Syntax
Die Anforderung, dass e1 einen Funktionstyp haben muss, wird nun mit der neu entstandenen
.
Gleichung τ1 = τ2 → α ausgedrückt.
In Abbildung 4.2 sind die Typregeln mit dieser Erweiterung angegeben.
Zum Beispiel wird die Identitätsfunktion mit diesen Typregeln wie folgt typisiert:
{x :: α} ` x :: α, ∅
∅ ` λx → x :: α → α, ∅
Dies ergibt einen polymorphen Typ für die Identitätsfunktion, den wir nun mit einer beliebigen Grundsubstitution in einen monomorphen Typ umwandeln können. Für dieses Beispiel
entstehen allerdings keine Gleichungen. Wir betrachten als weiteres Beispiel die Typisierung
von ((λx → λy → x) True False):
{x :: α2 , y :: α3 } ` x :: α2 , ∅
{x :: α2 } ` λy → x :: α3 → α2 , ∅
,
∅ ` (λx → λy → x) :: α2 → α3 → α2 , ∅ ∅ ` True :: Bool, ∅
,
.
∅ ` ((λx → λy → x) True) :: α1 , {α2 → α3 → α2 = Bool → α1 } ∅ ` False :: Bool, ∅
.
∅ ` ((λx → λy → x) True False) :: α0 , {α2 → α3 → α2 = Bool → α1 , α1 = Bool → α0 }
Nun müsste man die Gleichungen
.
{α2 → α3 → α2 = Bool → α1 , α1 = Bool → α0 }
lösen. Durch Hinschauen erkennt man, dass
α2
α3
α0
α1
= Bool
= Bool
= Bool
= Bool → Bool
eine Lösung der Gleichungen ist. Wir führen noch die folgende Regel ein, um Typen wirklich
herzuleiten:
Γ ` e :: τ, E
Typherleitung
Γ ` e :: σ(τ ) wenn σ Lösung von E
Für unser Beispiel können wir daher schließen
.
.
∅ ` ((λx → λy → x) True False) :: α0 , {α2 → α3 → α2 = Bool → α1 , α1 = Bool → α0 }
((λx → λy → x) True False) :: Bool
Stand: 26. März 2015
34
D. Sabel, FP-PR, SoSe 2015
4.1 Das Typsystem
Γ ` e :: τ, E
Γ ` e1 :: τ1 , E1 und Γ ` e2 :: τ2 , E2
.
.
Γ ` e1 >>= e2 :: IO α2 , E1 ∪ E2 ∪ {τ1 = IO α, τ2 = α → IO α2 }
Γ ` return e :: IO τ, E
Γ ` e :: τ, E
Γ ` e :: τ, E
.
Γ ` future e :: τ, E ∪ {τ = IO α}
.
Γ ` takeMVar e :: IO α, E ∪ {τ = MVar α}
Γ ` e1 :: τ1 , E1 und Γ ` e2 :: τ2 , E2
.
Γ ` putMVar e1 e2 :: IO (), E1 ∪ E2 ∪ {τ1 = MVar τ2 }
Γ ` e :: τ, E
Γ ` newMVar e :: IO (MVar τ ), E
∀i : Γ ` ei :: τi , Ei
[
.
Γ ` (c e1 . . . ear(c) ) :: τn+1 , Ei ∪ {Typ(c) = τ1 → . . . → τn → τn+1 }
i
Γ ` e1 :: τ1 , E1 und Γ ` e2 :: τ2 , E2
.
Γ ` (e1 e2 ) :: α, E1 ∪ E2 ∪ {τ1 = τ2 → α}
Γ ` e1 :: τ1 , E1 Γ ` e2 :: τ2 , E2
.
.
τ1 = τ3 → τ4 oder τ1 = T
Γ ∪ {x :: α} ` e :: τ, E
Γ ` (λx → e) :: α → τ, E
Γ ∪ {x :: τ } ` x :: τ, ∅
Γ ` (seq e1 e2 ) :: τ2 , E1 ∪ E2
Γ ` e :: τ, E
∀i : Γ ∪ {x1,i :: α1,i , . . . xni ,i :: αni ,i } ` (ci x1,i . . . xni ,i ) :: τi , Ei
∀i : Γ ∪ {x1,i :: α1,i , . . . xni ,i :: αni ,i } ` ei :: τi0 , Ei0


case e of(c1 x1,1 . . . xn1 ,1 → e1 )
[
[
[ .
[ .
 :: α, E ∪ Ei ∪ Ei0 ∪
...
Γ`
α = τi0 ∪ τ = τi
i
i
i
i
(cm x1,m . . . xnm ,m → em )
∀i : Γ ∪ {x1 :: α1 , . . . xn :: αn } ` ei :: τi , Ei
Γ ∪ {x1 :: α1 , . . . xn :: αn } ` e :: τ, E
[
[
.
Γ ` (letrec x1 = e1 , . . . xn = en in e) :: τ, E ∪ Ei ∪ {αi = τi }
i
i
Abbildung 4.2: Typisierungsregeln mit Gleichungen
D. Sabel, FP-PR, SoSe 2015
35
Stand: 26. März 2015
4 Teil 2: Type-Check und Transformation in einfachere Syntax
4.1.1 Gleichungen mit Unifikation lösen
Um einen Typ mit den eben vorgestellten Typregeln herzuleiten, benötigen wir noch ein Verfahren, um die entstandenen Typgleichungen zu lösen. Dies liefert die sogenannte Unifikation.
Sie operiert auf zwei Gleichungsmengen Eg und Eu . Hierbei sind Eu die ungelösten Gleichungen und Eg die gelösten Gleichungen. Der Unifikationsalgorithmus startet mit leerem Eg und
mit den zu lösenden Gleichungen in Eu . Der Algorithmus ist beendet, sobald Eu leer ist, oder
f ail auftritt. Hierbei bedeutet f ail, dass das Gleichungssystem nicht lösbar ist (die Typisierung
daher ebenso fehlschlägt). In jedem Schritt wendet der Algorithmus eine der im folgenden aufgezählten Regeln an. Hierbei bezeichnet E[τ /a], die Gleichungsmenge E in der in allen Gleichungen (in allen Untertermen der linken und rechten Seiten) die Variable α durch den Typ τ
ersetzt wurde.
Die Unifikationsregeln sind im folgenden aufgelistet, wobei α stets eine Typvariable ist und
Stand: 26. März 2015
36
D. Sabel, FP-PR, SoSe 2015
4.1 Das Typsystem
T, T1 , T2 Typkonstruktoren bezeichnen:
• Elim
.
Eg , {α = α} ∪ Eu
Eg , Eu
.
Eg , {α = τ } ∪ Eu
, falls α nicht in τ vorkommt.
• Solve
.
Eg [τ /α] ∪ {α = τ }, Eu [τ /α]
• Occ.Check
.
Eg , {α = τ } ∪ Eu
f ail
.
, falls α in τ vorkommt und τ 6= α
.
Eg , {τ = α} ∪ Eu
, wenn τ keine Typvariable ist
• Orient
.
Eg , {α = τ } ∪ Eu
• Decomp.T
• FailT
.
Eg , {T = T } ∪ Eu
Eg , Eu
.
Eg , {T1 = τ } ∪ Eu
f ail
, falls τ = T2 und T1 6= T2 , τ = IO τ 0 , τ = MVar τ 0 oder τ = τ1 → τ2
.
Eg , {IO τ1 = IO τ2 } ∪ Eu
• Decomp.IO
.
Eg , {τ1 = τ2 } ∪ Eu
• FailIO
.
Eg , {IO τ1 = τ2 } ∪ Eu
f ail
, falls τ2 = T , τ2 = MVar τ 0 oder τ2 = τ10 → τ20
.
Eg , {MVar τ1 = MVar τ2 } ∪ Eu
• Decomp.MVar
.
Eg , {τ1 = τ2 } ∪ Eu
• FailMVar
.
Eg , {MVar τ1 = τ2 } ∪ Eu
f ail
, falls τ2 = T , τ2 = IO τ 0 oder τ2 = τ10 → τ20
.
Eg , {τ1 → τ2 = τ10 → τ20 } ∪ Eu
• Decomp.Fn
.
.
Eg , {τ1 = τ10 , τ2 = τ20 } ∪ Eu
• FailFn
.
Eg , {τ1 → τ2 = τ } ∪ Eu
f ail
, falls τ = T , τ = IO τ 0 oder τ = MVar τ 0
Beispiel 4.1.2. Wir unifzieren die Gleichungen:
.
.
.
.
{b → c = c → a, b = d → MVar Bool, c = Bool → MVar d, e = a → Bool}
D. Sabel, FP-PR, SoSe 2015
37
Stand: 26. März 2015
4 Teil 2: Type-Check und Transformation in einfachere Syntax
Eg = ∅,
.
.
Eu = {b → c = c → a, b = d → MVar Bool,
.
.
c = Bool → MVar d, e = a → Bool}
Eg = ∅,
.
.
.
Eu = {b = c, c = a, b = d → MVar Bool,
.
.
c = Bool → MVar d, e = a → Bool}
.
Eg = {b = c},
.
.
.
.
Eu = {c = a, c = d → MVar Bool, c = Bool → MVar d, e = a → Bool}
.
.
Eg = {b = a, c = a},
.
.
.
Eu = {a = d → MVar Bool, a = Bool → MVar d, e = a → Bool}
.
.
.
Eg = {b = d → MVar Bool, c = d → MVar Bool, a = d → MVar Bool},
.
.
Eu = {d → MVar Bool = Bool → MVar d, e = (d → MVar Bool) → Bool}
.
.
.
Eg = {b = d → MVar Bool, c = d → MVar Bool, a = d → MVar Bool},
.
.
.
Eu = {d = Bool, MVar Bool = MVar d, e = (d → MVar Bool) → Bool}
.
.
.
.
Eg = {b = Bool → MVar Bool, c = Bool → MVar Bool, a = Bool → MVar Bool, d = Bool},
.
.
Eu = {MVar Bool = MVar Bool, e = (Bool → MVar Bool) → Bool}
.
.
.
.
Eg = {b = Bool → MVar Bool, c = Bool → MVar Bool, a = Bool → MVar Bool, d = Bool},
.
.
Eu = {Bool = Bool, e = (Bool → MVar Bool) → Bool}
.
.
.
.
Eg = {b = Bool → MVar Bool, c = Bool → MVar Bool, a = Bool → MVar Bool, d = Bool},
.
Eu = {e = (Bool → MVar Bool) → Bool}
.
.
.
.
Eg = {b = Bool → MVar Bool, c = Bool → MVar Bool, a = Bool → MVar Bool, d = Bool,
.
e = (Bool → MVar Bool) → Bool},
Eu = ∅
Die Substitution (genannt: der Unifikator) lässt sich nun ablesen als
{b 7→ Bool → MVar Bool, c 7→ Bool → MVar Bool,
a 7→ Bool → MVar Bool, d 7→ Bool, e 7→ (Bool → MVar Bool) → Bool},
I Aufgabe 5. Im Modul CHF.SemAna.TypeCheck ist der Typ TypeEquation für Typgleichungen
definiert als Paar von Typen:
> type TypeEquation tname =
(Type tname, Type tname)
Der Typ Type wurde bereits im Modul CHF.CoreL.Language definiert.
Implementieren Sie in diesem Modul die Unifikation auf Typgleichungen: Implementieren Sie eine
Funktion
unify :: [TypeEquation ConsName]
-> Either String [TypeEquation ConsName]
Stand: 26. März 2015
38
D. Sabel, FP-PR, SoSe 2015
4.1 Das Typsystem
die Typgleichungen als Eingabe erhält und die Gleichungen unifiziert und entweder Right loesung
liefert, wobei loesung die gelösten Gleichungen sind, oder Left fehler liefert, wobei fehler ein String
ist, der Auskunft über den bei der Unifikation aufgetretenen Fehler gibt. Zur Implementierung von unify
ist es hilfreich einige Hilfsfunkion zu definieren:
• Eine Variante von unify, die als Eingaben die gelösten und die ungelösten Gleichungen erhält.
• Eine Funktion, die die Substitution E[τ /α] auf Typgleichungen durchführt.
• Eine Funktion, die testet, ob eine Typvariable α in einem Typ τ vorkommt.
J
4.1.2 Implementierung des Typchecks
In der nächsten Aufgabe soll der Typcheck entsprechend den Typisierungsregeln aus Abbildung 4.2 implementiert werden. Die Eingabe des Typcheckers ist ein Ausdruck vom Typ Expr.
Da wir uns zwischendrin die Typen von Unterausdrücken „merken“ müssen, verwenden wir
den Konstruktor Label, um dort die Typen der Unterausdrücke zu speichern, d.h. in einem ein
voll typisierten Ausdruck, ist jede „Stufe“ des Ausdrucks durch ein Label verpackt. Z.B. erhält
der Ausdruck Lam ((1,13),"x") (Var ((1,18),"x")) mit Typen versehen die Form
Label (TVar "t1" :->: TVar "t1")
(Lam ((1,13),"x")
(Label (TVar "t1")
(Var ((1,18),"x"))))
Die Idee des Typisierungsalgorithmus folgt zunächst genau den Typisierungsregeln: Der Ausdruck wird rekursiv abgearbeitet und typisiert, die Gleichungen werden erzeugt und aufgesammelt, anschließend werden die Gleichungen unifiziert und die entstande Lösung wird auf
den markierten Ausdruck angewendet (bzw. eine Fehlermeldung wird erzeugt, falls ein Fehler
auftritt).
Im Modul CHF.SemAna.TypeCheck ist der Typ TypeOfCons definiert als
> type TypeOfCons = [(String, Type ConsName)]
Er stellt die Abbildung (als Liste von Paaren) von Konstruktornamen auf den Typ des Konstruktors dar. Zusätzlich sind bereits zwei Hilfsfunktionen definiert:
• mkTypeOfConsList :: [TDef ConsName ConsName] -> TypeOfCons erstellt aus der TDefListe die TypeOfCons-Liste
D. Sabel, FP-PR, SoSe 2015
39
Stand: 26. März 2015
4 Teil 2: Type-Check und Transformation in einfachere Syntax
• getTypeOfCons :: TypeOfCons -> String -> Maybe (Type ConsName) schaut den
Typ eines Konstruktors in der TypeOfCons-Liste nach. Falls er existiert wird Just t zurück gegeben, wobei t der Typ des Konstruktors ist. Existiert der Konstruktor nicht (sollte
nicht vorkommen), so wird Nothing zurück gegeben. Diese Funktion kann in der Typisierungsregel für Konstruktoranwendungen verwendet werden, um Typ(c) zu berechnen.
I Aufgabe 6. Im Modul CHF.SemAna.TypeCheck ist der Typ TypeEnvironment zur Darstellung von
Γ definiert als:
> type TypeEnvironment = [(String,Type ConsName)]
Er stellt Γ als Liste von Paaren (Variablenname, Typ der Variablen) dar.
Implementieren Sie dort die Funktion typecheck:
> typecheck ::
>
TypeOfCons
->
-> [String]
->
-> TypeEnvironment
->
-> (Expr () ConsName VarName)
->
-> ([String],
>
[TypeEquation ConsName],
>
Expr (Type ConsName) ConsName
Liste der Konstruktortypen
frische Namen
Gamma
Ausdruck
-- verbleibende Namen
-- Gleichungen E
VarName) -- Ausdruck mit Typmarkierungen
die die Typisierungsregeln entsprechend Abbildung 4.2 auf einen Ausdruck (rekursiv) anwendet.
Die Eingaben sind:
• Die TypeOfCons-Liste zum Nachschlagen der Typen der Datenkonstruktoren
• Eine Liste von Strings, die als neue Namen dienen. Diese Namen werden verwendet, um Namen
für neue Typvariablen parat zu haben.
• Der Ausdruck.
Die Ausgabe der Funktion typecheck ist ein Drei-Tupel bestehend aus:
• Den verbleibenden neuen Namen (jene, die doch nicht benutzt wurden)
• Den Gleichungen E
• Dem Ausdruck, der nun mit Typmarkierungen (mithilfe des Label-Konstruktors) versehen ist.
Benutzen Sie hierbei die folgende Variante der Regel zur Typisierung von seq-Ausdrücken:
Γ ` e1 , :: τ1 , E1
Γ ` e2 :: τ2 , E2
Γ ` (seq e1 e2 ) :: τ2 , E1 ∪ E2
Stand: 26. März 2015
40
D. Sabel, FP-PR, SoSe 2015
4.1 Das Typsystem
Im Gegensatz zur Regel in Abbildung 4.2, prüft diese Regel nicht, ob der Typ des ersten Arguments von
seq weder ein IO- noch ein MVar-Typ ist. Diesen Test werden wir erst später durchführen.
Beachten Sie den Fall, dass der Typcheck eine freie Variable entdeckt, und geben Sie in diesem Fall eine
Fehlermeldung mit der Position und dem Namen der freien Variablen aus. Ein Variable ist frei, wenn sie
nicht im Geltungsbereich eines Binders steht. Binder in CHF sind λx, die Pattern in case-Ausdrücken
und die letrec-Bindungen. Die Typisierungsregeln finden freie Variablen dadurch, dass eine Variable x
typisiert werden soll, diese jedoch nicht in der Typumgebung Γ enthalten ist.
J
In der nächsten Aufgabe fügen wir den Typcheck und die Unifikation zu einem Algorithmus
zusammen.
I Aufgabe 7. Implementieren Sie im Modul CHF.SemAna.TypeCheck die Funktion
tCheck :: ParseTree () ConsName VarName
-> Expr (Type ConsName) ConsName VarName
Die Funktion erwartet die Ausgabe des Parsers als Eingabe und führt anschließend den wie folgt den
Typcheck durch:
1. Zunächst wird typecheck für den Ausdruck aufgerufen, wobei mittels mkTypeOfConsList vorher
die TypeOfCons-Liste erstellt werden muss. Für die benötigten neuen Namen, können Sie z.B. die
Liste ["t" ++ show i | i <- [1..]] verwenden.
2. Anschließend werden die erhaltenen Typgleichungen unifiziert (hierfür können Sie unify verwenden).
3. War die Unifikation nicht erfolgreich, so wird der Typcheck mit einer Fehlermeldung abgebrochen.
Anderenfalls wenden Sie den erhaltenen Unifikator auf alle Typmarkierungen des Ausdrucks an,
den Sie nach Schritt 1 erhalten haben.
4. Schließlich durchlaufen Sie den aus dem letzten Schritt erhaltenen Ausdruck erneut rekursiv und
überprüfen, ob jeder seq-Ausdruck richtig getypt ist: Das erste Argument darf keinen IO- oder
MVar-Typen erhalten haben. Wird ein fehlerhafter seq-Ausdruck gefunden, so wird eine Fehlermeldung ausgegeben, anderenfalls wird der typisierte Ausdruck als Ergebnis zurück gegeben.
J
Der so implementierte Typcheck ist noch etwas unkomfortabel bei der Generierung von Fehlermeldungen, da die Fehler erst bei der entgültigen Unifikation festgestellt werden. Um Fehler
früher zu entdecken (und dadurch auch bessere Fehlermeldungen generieren zu können), kann
man folgende Zusatzregel einführen:
Γ ` e :: τ, E
Γ, ` e :: σ(τ ), Eσ , wenn σ Lösung von E
D. Sabel, FP-PR, SoSe 2015
41
Stand: 26. März 2015
4 Teil 2: Type-Check und Transformation in einfachere Syntax
Hierbei ist Eσ das Gleichungssystem E, wobei der Unifikator σ auf alle Gleichungen angewendet wird. Diese Regel erlaubt es, jederzeit zwischendrin schon einmal zu unifizieren.
I Aufgabe 8. Passen Sie die Funktion typecheck so an, dass nach jeder Regelanwendung bereits
einmal unifiziert wird (mit der eben beschriebenen Regel). Tritt bei dieser Unifikation ein Fehler auf, so soll
sofort eine Fehlermeldung ausgegeben werden, die möglichst genau angibt, wo der Typfehler aufgetreten
ist.
J
4.2 Transformation in eine vereinfachte Syntax
Wir sorgen zunächst dafür, dass Ausdrücke die sogenannte „Distinct Variable Convention“ erfüllen. Diese besagt, dass sämtliche Variablennamen unterschiedlich sind, wenn sie zu unterschiedlichen Bindern gehören. Am einfachsten kann man die DVC erfüllen, indem man den
Ausdruck komplett mit neuen Namen von oben nach unten umbenennt.
I Aufgabe 9. Implementieren Sie im Modul CHF.Transformation.Renamer die Funktion
> renameAndClean :: Expr (Type ConsName) ConsName VarName
>
-> [String]
>
-> (Expr () String String, [String])
Diese erhält den typisierten Ausdruck und eine Liste von neuen Variablennamen und
• benennt sämtliche gebundene Namen mit frischen Namen um. Die wesentliche Idee des Algorithmus dabei ist es, sich am Binder (z.B. Lam x e) die Umbenennung von x zu merken, um sie dann
beim Umbenennen von Variablen im Rumpf e nachzuschauen. Zum Merken kann eine Liste von
Paaren der Form (alter Name, neuer Name) verwendet werden, effizienter ist allerdings die Verwendung anderer Datenstrukturen, z.B. eine Map aus der Bibliothek Data.Map
• stellt sämtliche Variablen und Konstruktoren nicht mehr mit ihrer Positionsmarkierung dar, sondern ausschließlich als String.
• die Typinformation komplett wegwirft, indem sämtliche Label entfernt werden.
Die Ausgabe ist das Paar bestehend aus dem umbenannten und vereinfachten Ausdruck sowie der Liste
der noch verbleibenden neuen Namen.
J
4.2.1 Transformation in MExpr
Die abstrakten Maschinen erfordern eine leicht veränderte (bzw. vereinfachte) Syntax innerhalb
der Ausdrücke. Kurz gesagt, erlauben die Maschinen nur Variablen als Argumente in Anwendungen und Konstruktoranwendungen. Auch das zweite Argument von seq darf nur eine Variable sein. Da die monadischen Operatoren zum Teil wie Konstruktoranwendungen behandelt
werden, müssen auch deren Argumente stets Variablen sein.
Stand: 26. März 2015
42
D. Sabel, FP-PR, SoSe 2015
4.2 Transformation in eine vereinfachte Syntax
Formal ist die Syntax von Maschinenausdrücken:
e, ei ∈ Expr M ::= x | me | (λx → e) | (e x) | (c x1 . . . xar(c) ) | (seq e1 x)
| (caseT e of {((cT,1 x1 . . . xar(cT,1 ) ) → eT,1 );
...
((cT,|T | x1 . . . xar(cT,|T | ) ) → eT,|T | ))}
| (letrec x1 = e1 ; . . . ; xn = en in e) wobei n ≥ 1
me ∈ MExprM ::= (return x) | (x1 >>= x2 ) | (future x)
| (takeMVar x) | (newMVar x) | (putMVar x1 x2 )
Die Transformation von ursprünglichen Ausdrücken in Maschinenausdrücke ist relativ einfach
zu bewerkstelligen, indem man neue letrec-Ausdrücke einführt. Sei e ein Ausdruck, dann bezeichnen wir mit JeK den Maschinenausdruck zu e. Die wesentlichen Regeln zur Übersetzung
sind:
• J(e1 e2 )K = letrec x = Je2 K in (Je1 K x), wobei x eine neue Variable ist.
• J(c e1 . . . en )K = letrec x1 = Je1 K, . . . , xn = Jen K in (c x1 . . . xn ), wobei x1 , . . . , xn neue
Variablen sind.
• Jseq e1 e2 K = letrec x = Je2 K in (seq Je2 K x), wobei x eine neue Variable ist.
• Jreturn eK = letrec x = JeK in return x, wobei x eine neue Variable ist.
• Je1 >>= e2 K = letrec x1 = Je1 K, x2 = Je2 K in x1 >>= x2 , wobei x1 , x2 neue Variablen sind.
• Jfuture eK = letrec x = JeK in future x, wobei x eine neue Variable ist.
• JtakeMVar eK = letrec x = JeK in takeMVar x, wobei x eine neue Variable ist.
• JnewMVar eK = letrec x = JeK in newMVar x, wobei x eine neue Variable ist.
• JputMVar e1 e2 K = letrec x1 = Je1 K, x2 = Je2 K in putMVar x1 x2 , wobei x1 , x2 neue Variablen sind.
Für alle anderen Konstrukte wird J·K homomorph über die Termstruktur gezogen:
• JxK = x
• Jλx → eK = λx.JeK
• Jletrec x1 = e1 , . . . , xn = en in eK
= letrec x1 = Je1 K, . . . , xn = Jen K in JeK
D. Sabel, FP-PR, SoSe 2015
43
Stand: 26. März 2015
4 Teil 2: Type-Check und Transformation in einfachere Syntax
• JcaseT e of {pat1 → e1 ; . . . ; pat|T | → eT }K
= caseT JeK of {pat1 → Je1 K; . . . ; pat|T | → JeT K}
Im Modul CHF.CoreL.MachineLanguage ist der Datentyp MExpr für Maschinenausdrücke definiert als:
> data MExpr cname v =
>
VarM v
>
| LamM v (MExpr cname v)
>
| AppM (MExpr cname v) v
>
| SeqM (MExpr cname v) v
>
| ConsM (Either MAction cname) [v]
>
| CaseM (MExpr cname v) [MCAlt cname v]
>
| LetrecM [MBinding cname v] (MExpr cname v)
> deriving(Eq,Show)
> data MCAlt cname v =
> CAltM cname [v] (MExpr cname v)
> deriving(Eq,Show)
> data MBinding cname v =
> v := (MExpr cname v)
> deriving(Eq,Show)
I Aufgabe 10. Definieren Sie im Modul CHF.Transformation.Core2Machine die Funktion
> normalize :: [String]
>
-> Expr () String String
>
-> ([String], MExpr String String)
die als Eingaben eine Liste von neuen Namen und einen Ausdruck erhält und diesen entsprechend der
Übersetzuung J·K in einen Maschinenausdruck konvertiert und das Paar bestehend aus den verbleibenden
frischen Namen und dem Maschinenausdruck liefert.
J
I Aufgabe 11. Definieren Sie im Modul CHF.Transformation.Core2Machine eine Funktion
toMachineExpr :: String -> MExpr String String, die ein Quellprogramm erhält, die lexikalische und die syntaktische Analyse durchführt, anschließend den Typcheck durchführt und nach der Umbenennung den Maschinenausdruck liefert. Für die Liste der frischen Namen können Sie hierbei z.B. die
Liste ["_x" ++ show i | i <- [1..]] verwenden.
J
Stand: 26. März 2015
44
D. Sabel, FP-PR, SoSe 2015
5
Teil 3: Abstrakte Maschinen
5.1 Die Abstrakte Maschine Mark 1 zur Ausführung funktionaler CHF-Programme
Eine einfache abstrakte Maschine zur call-by-need Auswertung von letrec-Sprachen ist die
Mark 1 aus (Sestoft, 1997). Die in (Sestoft, 1997) beschriebenen Maschinen passen zu unserer Sprache bis auf die nebenläufige Auswertung und den monadischen Operationen. Deshalb
werden wir später die Mark 1-Maschine zunächst um monadische Berechnungen (IO-Mark 1)
und anschließend um nebenläufige Auswertung (Concurrent Mark 1) erweitern. Die Mark 1Maschine, die wir zunächst betrachten, behandelt alle monadischen Operationen genauso wie
Konstruktoranwendungen. Da keine case-Ausdrücke existieren, die solche „Konstruktoranwendungen“ (wie z.B. takeMVar x) zerlegen können, ist diese Behandlung sehr einfach. Auch
die Nichtexistenz der Nebenläufigkeit passt dazu: Ein Ausdruck future e wird wie eine Konstruktoranwendung behandelt und daher ist es einfach nicht möglich, auf der Mark 1-Maschine
einen neuen Thread zu erzeugen.
Wir benötigen noch die Definition der Werte, welche jene Ausdrücke sind, zu denen wir unsere Programme auswerten möchten. Für die Mark 1-Maschine erklären wir auch die monadischen Operationen zu Werten:
Definition 5.1.1. Ein MExpr-Ausdruck ist ein Mark 1-Wert, wenn er eine Abstraktion, eine Konstruktorapplikation, oder ein monadischer Ausdruck ist. Ein MExpr-Ausdruck ist in WHNF (weak head normal
form, schwache Kopfnormalform), wenn er eine der folgenden Formen besitzt:
• \x → e, oder
Stand: 26. März 2015
D. Sabel, FP-PR, SoSe 2015
5 Teil 3: Abstrakte Maschinen
• (c y1 . . . yar(c) ), wobei c ein Konstruktor, oder ein monadischer Operator ist.
• letrec x1 = e1 , . . . , xn = en in v, wobei v ein Mark 1-Wert ist.
Der Zustand der Mark 1-Maschine ist ein 3-Tupel (H, e, S). Hierbei gilt:
• H ist ein Heap. Ein Heap ist eine Menge von Heapbindungen. Eine Heapbindung p 7→ e
besteht aus einer Heapvariablen p und einem Ausdruck e. Für jede Heapvariable darf nur
eine Bindung im Heap vorkommen, d.h. der Heap ist eine Abbildung von Heapvariablen
· 2 für die disjunkte Vereinigung der Heaps H1 und H2 .
auf Ausdrücke. Wir schreiben H1 ∪H
• e ist der aktuell auszuwertende Ausdruck. Wir nennen diese Komponente auch Control,
da sie die Auswertung (d.h. den Ablauf der Maschine) steuert.
• S ein Stack ist. Auf dem Stack merkt man sich den Teil des Ausdrucks, den man erst später
auswerten möchte. Will man z.B. eine Applikation (e x) auswerten, so merkt man sich
zunächst x auf dem Stack, wertet e (zu einer Abstraktion) aus, und schaut danach auf
dem Stack nach, wie man weiter machen muss (x als Argument im Rumpf der Abstraktion
einsetzen). Elemente im Stack können sein:
– #app (x) zum Merken des Arguments x einer Applikation;
– #seq (x) zum Merken des zweiten Argumentes eines seq-Ausdrucks;
– #case (alts) zum Merken der Alternativen eines case-Ausdrucks;
– #heap (x) zum Merken eines zu aktualisierenden Heapeintrags.
Abbildung 5.1 zeigt die Übergangsrelation
(auch Maschinentransition genannt) der
gibt an, wie man aus einem Zustand den darauf folgenden Zustand
Mark 1-Maschine, d.h.
berechnet. Dies ist eine operationale Semantik, genauer eine sog. small-step Semantik, da die Regeln
die Auswertung in vielen kleinen Schritten festlegen.
Die ersten Regeln (pushApp), (pushSeq), (pushAlts) dienen dazu, die richtige Position zum
„weitermachen“ zu finden: Für eine Anwendung und einen seq-Ausdruck wird in das erste
Argument gesprungen, beim case-Ausdruck rutscht der zu unterscheidende Ausdruck in den
Fokus. Bei allen drei Regeln, wird sich auf dem Stack gemerkt, wie später weiter gemacht werden muss: Für die Anwendung wird das Argument (eine Variable) auf den Stack gelegt, für
seq-Ausdrücke wird das zweite Argument auf dem Stack gesichert und für case-Ausdrücke
werden die Alternativen auf dem Stack abgelegt.
Die nächsten vier Regeln (takeApp), (takeSeq), (branch) und (nobranch) sind genau dafür
da, dass eine mit den ersten drei Regeln begonnene Unterauswertung beendet ist: Für eine Anwendung muss diese Unterauswertung mit einer Abstraktion enden, anschließend wird das
Argument vom Stack genommen und eine β-Reduktion durchgeführt, indem das Argument
im Rumpf der Abstraktion den formalen Parameter ersetzt (Regel (takeApp)). Für die seqAuswertung genügt es, dass das erste Argument zu einem Wert, d.h. zu einer Abstraktion, zu
M1
M1
Stand: 26. März 2015
46
D. Sabel, FP-PR, SoSe 2015
5.1 Die Abstrakte Maschine Mark 1 zur Ausführung funktionaler CHF-Programme
(pushApp)
(H, (e x), S)
(pushSeq)
(H, (seq e x), S)
(pushAlts)
(H, (case e of alts), S)
(takeApp)
(H, λx → e, #app (y) : S)
(takeSeq)
(H, v, #seq (y) : S)
(H, y, S),
falls v ein Mark 1-Wert ist
(branch)
(H, (c x1 . . . xn ), #case (. . . (c y1 . . . yn ) → e; . . .) : S)
(H, e[xi /yi ]ni=1 , S)
M1
(H, e, #app (x) : S)
M1
(H, e, #seq (x) : S)
M1
(H, e, #case (alts) : S)
M1
(H, e[y/x], S)
M1
M1
(nobranch)
(enter)
(update)
(mkBinds)
Laufzeitfehler,
(H, (c x1 . . . xn ), #case (alts) : S)
falls alts keine Alternative für Konstruktor c enthält
M1
· 7→ e}, y, S)
(H∪{y
M1
(H, e, #heap (y) : S)
· 7→ v}, v, S)
(H∪{y
(H, v, #heap (y) : S)
falls v ein Mark 1-Wert ist
M1
(H, letrec x1 = e1 ; . . . ; xn = en in e, S)
· 1 7→ e1 [yi /xi ]ni=1 , . . . , yn 7→ en [yi /xi ]ni=1 }, e[yi /xi ]ni=1 , S)
(H∪{y
wobei yi frische Variablen sind
M1
(blackhole)
(H, y, S) → (H, y, S), falls keine Bindung für y im Heap
Abbildung 5.1: Zustandübergangsregeln der Mark 1
einer Konstruktoranwendung oder einem monadischen Ausdruck ausgewertet wurde. In diesem Fall wird das zweite Argument vom Stack genommen und als Resultat des seq-Ausdrucks
weiter verwendet (Regel (takeSeq)). Für case-Ausdrücke muss der zu unterscheidende Ausdruck zu einer Konstruktoranwendung ausgewertet werden. Ist dies geschehen, so werden die
case-Alternativen vom Stack genommen und die passende Alternative wird gewählt, wobei
die aktuellen Paramter die formalen Parameter (die Patternvariablen) ersetzen, hierbei meint
e[xi /yi ]ni=1 die Ersetzung aller Variablen y1 , . . . , yn in e durch die Variablen x1 , . . . , xn (Regel
(branch)). Existiert keine passende Alternative, so wird ein Laufzeitfehler gemeldet (Regel (nobranch)).
Die Regeln (update) und (enter) werden benutzt, um die Auswertung von Heap-gebundenen
Variablen zu steuern: Wird der Wert einer solchen Variablen benötigt, so wird die entsprechende
Bindung vom Heap entnommen und auf dem Stack wird sich gemerkt, welche Variable gerade
ausgewertet wird (Regel (enter)). Ist die Auswertung erfolgreich beendet (d.h. mit einem Wert
(Abstraktion, Konstruktoranwendung oder monadischer Ausdruck), so wird die entsprechende
aktualisierte Bindung in den Heap zurück geschrieben und die entsprechende Markierung vom
Stack entfernt (Regel (update)).
D. Sabel, FP-PR, SoSe 2015
47
Stand: 26. März 2015
5 Teil 3: Abstrakte Maschinen
Die vorletzte Regel (die Regel (mkBinds)) verarbeitet einen letrec-Ausdruck: Sämtliche
letrec-Bindungen werden in den Heap geschoben, wobei diese (wie auch der „in“-Ausdruck)
mit neuen Namen umbenannt werden.
Schließlich gibt es noch die Regel (blackhole), die angewendet wird, falls eine Variable ausgewertet werden soll, die nicht im Heap vorhanden ist. Dieser Fall tritt z.B. auf, wenn eine rekursive letrec-Bindung sich selbst referenziert, z.B. letrec x = x in x: Die ersten zwei Schritt
der Auswertung sind: (∅, letrec x = x in x, [])
({p 7→ p}, p, [])
(∅, p, [#heap (p)]). Jetzt ist
Control die Variable p, die jedoch nicht im Heap liegt. Die Regel (blackhole) kann nun beliebig
(∅, p, [#heap (p)])
(∅, p, [#heap (p)])
. . ., d.h. die
oft angwendet werden. (∅, p, [#heap (p)])
Maschine ist in einer Endlosschleife.
M1
M1
M1
M1
M1
Beachte, dass ungetypte Fälle nicht auftreten können (z.B. der Ausdruck ist eine Abstraktion,
aber auf dem Stack liegt ein #case (alts)-Eintrag als oberstes Element), da der Typcheck diese
Ausdrücke aussortiert.
Es fehlen jetzt noch Angaben, mit welchem Zustand man beginnt und wann man aufhört:
Sei e ein MExpr-Ausdruck, so startet die Maschine mit dem Zustand (∅, e, []), d.h. mit leerem
Heap und leerem Stack. Die Maschine stoppt, wenn keine Regel mehr anwendbar ist. In diesem
Fall ist der aktuelle Ausdruck (Control) ein Mark 1-Wert und der Stack ist leer, genauer:
Definition 5.1.2. Ein Zustand der Mark 1-Maschine ist ein Endzustand, wenn er von der Form
(H, v, []) ist, wobei v ein Wert ist.
Diese Definition passt gerade zu den WHNFs, wobei das äußere letrec durch den Heap H
dargestellt ist.
Wir betrachten zunächst zwei Beispielausführungen der Maschine:
Beispiel 5.1.3. Der Ausdruck letrec x = (λy → y) z, z = True in x wird auf der Mark 1-Maschine
Stand: 26. März 2015
48
D. Sabel, FP-PR, SoSe 2015
5.1 Die Abstrakte Maschine Mark 1 zur Ausführung funktionaler CHF-Programme
wie folgt ausgewertet:
Heap
∅
Control
Stack
letrec
[]
x = (λy → y) z;
z = True
in x
Regel
{p1 7→ (λy → y) p2 , p1
p2 7→ True}
[]
(mkBinds)
M1
{p2 7→ True}
(λy → y) p2
[#heap (p1 )]
(enter)
M1
{p2 7→ True}
(λy → y)
[#app (p2 ), #heap (p1 )]
(pushApp)
M1
{p2 7→ True}
p2
[#heap (p1 )]
(takeApp)
M1
{}
True
[#heap (p2 ), #heap (p1 )] (enter)
M1
{p2 7→ True}
True
[#heap (p1 )]
(update)
{p2 7→ True,
p1 7→ True}
True
[]
(update)
M1
M1
Beispiel 5.1.4. Der Ausdruck
letrec i = Cons x1 i; x1 = True
in case i of {
(Cons x xs) → xs;
Nil → Nil}
D. Sabel, FP-PR, SoSe 2015
49
Stand: 26. März 2015
5 Teil 3: Abstrakte Maschinen
wird auf der Mark 1-Maschine wie folgt ausgewertet:
Heap
∅
M1
M1
Control
Stack
letrec
[]
i = Cons x1 i,
x1 = True
in
case i of {
(Cons x xs) → xs;
Nil → Nil}
Regel
{p1 7→ Cons p2 p1 , case p1 of {
p2 7→ True}
(Cons x xs) → xs;
Nil → Nil}
[]
(mkBinds)
{p1 7→ Cons p2 p1 , p1
x xs) → xs;
)]
[#case ((Cons
Nil → Nil
(pushAlts)
[#heap (p1 ),
(enter)
p2 7→ True}
M1
{p2 7→ True}
Cons p2 p1
x xs) → xs;
#case ((Cons
)]
Nil → Nil
M1
{p1 7→ Cons p2 p1 , Cons p2 p1
x xs) → xs;
)]
[#case ((Cons
Nil → Nil
(update)
{p1 7→ Cons p2 p1 , p1
p2 7→ True}
[]
(branch)
{p1 7→ True}
[#heap (p1 )]
(enter)
[]
(update)
p2 7→ True}
M1
M1
M1
Cons p2 p1
{p1 7→ Cons p2 p1 , Cons p2 p1
p2 7→ True}
5.1.1 Implementierung
Zur Implementierung der Maschine werden wir nun genauer untersuchen, welche Datenstrukturen und welche Operationen benötigt werden.
Für die Darstellung der Maschinenausdrücke definieren wir als Abkürzungen:
> type Mark1Expr = MExpr Mark1Cons Mark1Var
> type Mark1Var = String
> type Mark1Cons = String
Wir benötigen drei Datenstrukturen:
Stand: 26. März 2015
50
D. Sabel, FP-PR, SoSe 2015
5.1 Die Abstrakte Maschine Mark 1 zur Ausführung funktionaler CHF-Programme
Control: Hier ist die Datenstruktur bereits durch Mark1Expr implementiert, allerdings brauchen wir eine zusätzliche Operation: In den Regeln (takeApp), (mkBinds) und
(branch) werden Variablen durch Variablen substituiert. D.h. es fehlt eine Funktion substitute :: Mark1Expr -> Mark1Var -> Mark1Var -> Mark1Expr, die einen Ausdruck und zwei Variablen erhält und im Ausdruck die erste durch die zweite Variable
ersetzt.
Heap: Heapeinträge sind Paare vom Typ (String,Mark1Expr). Da der Heap eine Abbildung
von Variablen auf Ausdrücke ist, bietet es sich an den Datentyp Map aus der Standardbibliothek Data.Map zu verwenden, d.h. wir könnten den Typ Heap definieren als
type Heap = Map Mark1Var Mark1Expr
Wir benötigen die folgenden Operationen auf dem Heap:
• Für die Regeln (enter) und (blackhole):
lookupHeap : Mark1Var -> Heap -> Maybe (Mark1Var,Heap), die eine Heapvariable und einen Heap erhält, und
– falls eine Bindung für die Heapvariable existiert, die Bindung aus dem Heap entfernt und das Paar bestehend aus dem Ausdruck und dem modifizierten Heap
liefert,
– Nothing liefert, falls keine Bindung existiert.
• Für die Regeln (update) und (mkBinds):
insertHeap : Mark1Var -> Mark1Expr -> Heap -> Heap, die eine Variable, einen
Ausdruck und einen Heap erhält und eine neue Bindung auf dem Heap anlegt, und
schließlich den geänderten Heap zurückgibt.
• emptyHeap :: Heap zum Erstellen eines leeren Heaps.
Stack: Stackelemente können Variablen aus einer Applikation #app (x), Variablen aus dem
Heap #heap (x), case-Alternativen #case (alts), oder rechte Ausdrücke von seq-Ausdrücken
#seq (x) sein. D.h. die Elemente könnten durch den Datentypen
data StackElem
=
|
|
|
RetApp Mark1Var
RetHeap Mark1Var
RetCase [MCAlt Mark1Cons Mark1Var]
RetSeq Mark1Var
repräsentiert werden. Der Stack selbst kann dann durch eine Liste von StackElemElementen dargestellt werden, d.h.
D. Sabel, FP-PR, SoSe 2015
51
Stand: 26. März 2015
5 Teil 3: Abstrakte Maschinen
type Stack = [StackElem]
Operationen auf dem Stack sind:
• Für
die
Regeln
(pushApp),
(pushSeq),
(pushAlts)
und
(enter):
push :: StackElem -> Stack -> Stack, welche ein Stack-Element oben auf
den Stack legt.
• Für die Regeln (takeApp), (takeAlts), (takeSeq) und (update):
pop :: Stack -> (StackElem,Stack), welche das oberste Element des Stacks entnimmt und das Paar bestehend aus dem obersten Stackelement und dem verbleibenden Stack liefert.
• isEmptyStack :: Stack -> Bool, die prüft, ob der Stack leer ist.
• emptyStack :: Stack, die einen neuen leeren Stack anlegt.
Zusätzlich werden Operationen auf dem StackElem-Typ benötigt.
Wir könnten nun direkt loslegen und die Datenstrukturen wie eben analysiert implementieren. Allerdings benötigen die anderen Maschinen auch andere Elemente im Stack und im Heap.
Deswegen werden diese Datenstrukturen in der folgenden Aufgabe polymorph über den Elementtypen definiert und implementiert.
I Aufgabe 12. Das Modul CHF.AbsM.Heap definiert einen Datentypen für den Heap, der polymorph
über den Heapvariablen und den rechten Seiten der Bindungen ist, d.h.
> import qualified Data.Map as Map
> type Heap p e =
(Map.Map) p e
Implementieren Sie im gleichen Modul die folgenden Operationen auf dem Datentypen
• emptyHeap :: Heap p e zum Erstellen eines leeren Heaps.
• lookupHeap :: p -> Heap p e -> Maybe (e, Heap p e), die wie oben beschrieben im
Heap nach dem Eintrag für p sucht und die rechte Seite der Bindung, sowie den modifizierten
Heap liefert. Eventuell müssen Sie an den Typ p weitere Typklassen-Anforderungen stellen.
• insertHeap :: p -> e -> Heap p e -> Heap p e, die wie oben beschrieben eine Heapvariable vom Typ p und eine rechte Seite vom Typ e sowie einen Heap erhält und die entsprechende
neue Bindung im Heap anlegt und schließlich den modifizierten Heap zurück liefert. Auch hier ist
es möglich, dass an p weitere Typklassen-Anforderungen gestellt werden müssen.
J
Stand: 26. März 2015
52
D. Sabel, FP-PR, SoSe 2015
5.1 Die Abstrakte Maschine Mark 1 zur Ausführung funktionaler CHF-Programme
I Aufgabe 13. Implementieren Sie im Modul CHF.AbsM.Stack einen Datentypen für den Stack, der
polymorph über den Einträgen im Stack ist
type Stack a
= ...
Implementieren Sie im gleichen Modul die folgenden Operationen auf dem Stack:
• emptyStack :: Stack a zum Erzeugen eines leeren Stacks.
• isEmptyStack :: Stack a -> Bool, die prüft ob der Stack leer ist.
• push :: a -> Stack a -> Stack a, die ein Element und einen Stack erhält und das Element
oben auf den Stack legt.
• pop :: Stack a -> (a, Stack a), die das oberste Element vom Stack nimmt und das Paar
bestehend aus diesem Element und dem Reststack zurück gibt.
J
I Aufgabe 14. Im Modul CHF.AbsM.Stack.StackElem ist der gleichnamige Datentyp definiert als
> data StackElem v a = RetApp v | RetHeap v
> deriving (Eq,Show)
| RetCase a | RetSeq v
D.h. Stack-Elemente sind polymorph über den Variablen v und den Alternativen a. Sie können entweder
Variablen aus einer Anwendung, Variablen aus dem Heap, Variablen von seq-Ausdrücken oder caseAlternativen sein. Implementieren Sie die folgenden Funktionen in diesem Modul:
• isRetApp :: StackElem v a
RetApp ... ist.
-> Bool, die testet, ob ein Stack-Element von der Form
• isRetHeap :: StackElem v a
RetHeap ... ist.
-> Bool, die testet, ob ein Stack-Element von der Form
• isRetCase :: StackElem v a
RetCase ... ist.
-> Bool, die testet, ob ein Stack-Element von der Form
• isRetSeq :: StackElem v a
RetSeq ... ist.
-> Bool, die testet, ob ein Stack-Element von der Form
• mkRetHeap :: v -> StackElem v a, die aus einer Variablen ein RetHeap-Objekt erstellt.
• mkRetApp :: v -> StackElem v a, die aus einer Variablen ein RetApp-Objekt erstellt.
• mkRetCase :: a -> StackElem v a, die aus case-Alternativen ein RetCase-Objekt erstellt.
• mkRetSeq :: v -> StackElem v a, die ein RetSeq-Objekt erstellt.
D. Sabel, FP-PR, SoSe 2015
53
Stand: 26. März 2015
5 Teil 3: Abstrakte Maschinen
• fromVar :: StackElem v a
Variable extrahiert.
-> v, die aus einem RetApp-, RetSeq-, oder RetHeap-Objekt die
• fromRetCase :: StackElem v a
trahiert.
-> a, die aus einem RetCase-Objekt die Alternativen ex-
J
I Aufgabe 15. Implementieren Sie im Modul CHF.CoreL.MachineLanguage eine Funktion
> substitute :: Mark1Expr -> Mark1Var -> Mark1Var -> Mark1Expr
die einen Ausdruck und zwei Variablen erhält und alle Vorkommen der ersten Variablen durch die zweite
J
ersetzt.
Nachdem nun alle notwendigen Datenstrukturen und Operationen vorhanden sind, können
wir die Mark 1-Maschine implementieren.
Im Modul CHF.AbsM.Mark1 wird der Datentyp für den Zustand der Mark 1 definiert (unter
Benutzung der Record-Syntax (siehe (Sabel, 2015)):
> data Mark1State =
> Mark1State {
>
heap :: Heap Mark1Var Mark1Expr ,
>
control :: Mark1Expr,
>
stack :: (Stack (StackElem Mark1Var [MCAlt Mark1Cons Mark1Var]))
> }
Der Zustand besteht somit aus den 3 Komponenten:
• dem Heap vom Typ (Heap Mark1Var Mark1Expr), d.h. einer Abbildung von Variablen auf
Mark1Expr-Ausdrücke;
• Control, als Ausdruck vom Typ Mark1Expr;
• dem Stack vom Typ
Stack (StackElem Mark1Var [MCAlt Mark1Cons Mark1Var]))
D.h. für die Variablen setzen wir den Typ Mark1Var und für die Alternativen den Typ
[MCAlt Mark1Cons Mark1Var] ein.
I Aufgabe 16. Implementieren Sie im Modul LFPC.AbsM.Mark1 die folgenden Funktionen:
• startState :: Mark1Expr -> Mark1State, die einen Mark1Expr-Ausdruck erhält und den
Startzustand der Maschine für diesen Ausdruck berechnet.
Stand: 26. März 2015
54
D. Sabel, FP-PR, SoSe 2015
5.2 Die IO-Mark 1
• nextState :: Mark1State -> [Mark1Var] -> (Mark1State,[Mark1Var]), die einen Zustand der Mark 1 sowie eine Liste (neuer) Variablen erhält, und entsprechend der Übergangsrelation den Folgezustand berechnet und zusätzlich die nicht benutzten neuen Variablen zurück
gibt.
• finalState :: Mark1State -> [Mark1Var] -> (Mark1State, [Mark1Var]), die einen
Zustand der Mark 1 sowie eine Liste (neuer) Variablen erhält, und entsprechend den Endzustand
der Maschine berechnet und zusätzlich die nicht benutzten neuen Variablen zurück gibt.
• exec :: Mark1Expr -> [Mark1Var] -> String, die einen Ausdruck vom Typ Mark1Expr
und eine Liste neuer Variablennamen erhält, anschließend die Mark 1-Maschine für diesen Ausdruck ausführt und schließlich das Ergebnis als String wie folgt ausdruckt:
– Für Abstraktionen soll einfach der Text <<<Function>>> gedruckt werden
– Für Konstruktoranwendungen (c x1 . . . xn ) soll der Konstruktor c gedruckt werden, anschließend sollen rekursiv x1 bis xn mit der Maschine ausgewertet und deren Wert gedruckt werden. Dafür müssen Sie die Maschine erneut anwerfen: Wenn (H, (c x1 , . . . , xn ), []) der Endzustand war, so soll zunächst (H, x1 , []) zur Berechnung des Strings für x1 angewendet werden, der dadurch veränderte Heap soll benutzt werden, um x2 auszuwerten usw.
Beachten Sie: Die Hintereinander-Auswertung von (H, x1 , []), . . . (H, xn , []) ist eher als
falsch anzusehen, da sie den gleichen Heap H verdoppelt und dadurch Doppelauswertungen ermöglicht.
– Monadische Operationen sollen wie Konstruktoranwendungen beim Drucken behandelt werden.
J
I Aufgabe 17. Implementieren Sie im Modul CHF.Run eine Funktion runMark1, die ein CHFProgramm erwartet, dann das Programm lext, parst, der semantischen Analyse unterzieht, in eine
Mark1Expr umwandelt, schließlich auf der Mark 1-Maschine laufen lässt und das Ergebnis als String
zurück liefert.
Für die neuen Variablen, die Sie brauchen werden, verwenden Sie interne Variablen, z.B. die Liste
["_internal" ++ show x | x <- [1..]].
J
5.2 Die IO-Mark 1
In diesem Abschnitt werden wir die Mark 1-Maschine erweitern, sodass monadische Operationen, wie das Anlegen und der Zugriff auf MVars, ausgeführt werden können. Die Operation
future kann diese Maschine noch nicht verarbeiten, da wir erst im nächsten Abschnitt nebenläufige Threads hinzufügen werden.
D. Sabel, FP-PR, SoSe 2015
55
Stand: 26. März 2015
5 Teil 3: Abstrakte Maschinen
Um mit der Mark 1-Maschine auch die monadischen Aktionen ausführen zu können, muss
der Maschinenzustand zunächst um eine weitere Komponente erweitert werden: Die Speicherzellen, d.h. die MVars, müssen zusätzlich gespeichert werden. Die Auswertung folgt nun der
Idee: Werte zunächst wie auf der Mark 1 Maschine aus. Wenn der Mark 1-Endzustand eine monadische Aktion ist, dann führe diese durch:
• Die Operation newMVar x legt eine neue MVar mit Inhalt x an, und gibt als Resultat
return y, wobei y der Name der MVar ist.
• Die Operation takeMVar y liest den Wert aus der MVar namens y aus, entleert die MVar
und gibt return v zurück, wenn v der Inhalt der MVar war.
• Die Operation putMVar y z schreibt den Wert z in die MVar y und liefert return Unit
zurück.
• Die Operation e1 >>= e2 führt zunächst e1 , bis diese von der Form return x ist, anschließend wird das erste monadische Gesetz angwendet um fortzufahren: return x >>= e2 wird
in die Anwendung (e2 x) überführt.
Damit dies richtig funktioniert, muss man sich dafür wieder die Rücksprung-Operationen
merken (z.B. e2 im obigen Beispiel für die >>= -Operation). Dieses Merken wird durch einen weiteren Stack realisiert – den IO-Stack. Auch die Operatoren takeMVar und putMVar haben noch ein
Problem: Zur Ausführung muss das jeweils erste Argument wirklich eine MVar-Referenz sein,
damit man die zugehörige MVar kennt. Auch dieses Problem wird der IO-Stack lösen. Insgesamt
definieren wir daher:
Definition 5.2.1. Der Zustand der IO-Mark 1-Maschine ist ein 5-Tupel (H, M, e, S, I) wobei:
• H ist ein Heap, der eine Abbildung von Variablen auf Ausdrücke darstellt.
• M ist eine Menge von MVars. Auch hier kann man von einer Abbildung von Variablen auf Ausdrücken sprechen, wobei es Variablen gibt, die auf „leer“ abgebildet werden. Wir notieren eine MVar
namens x mit Inhalt y als x m y und eine leere MVar namens x als x m •.
• e ist der aktuell auszuwertende Ausdruck.
• S ist der Mark 1-Stack
• I ist der IO-Stack. Dieser kann als Elemente enthalten:
– #take : Damit wird sich gemerkt, dass eine takeMVar-Operation noch ausgeführt werden
muss.
– #put (x): Es wird sich gemerkt, dass eine putMVar-Operation durchgeführt werden muss, die
x in die MVar schreiben will.
Stand: 26. März 2015
56
D. Sabel, FP-PR, SoSe 2015
5.2 Die IO-Mark 1
– # >>= (y): Es muss noch eine >>= -Operation durchgeführt werden, deren zweites Argument y
ist.
Für einen Ausdruck e startet die Maschine mit leerem Heap, ohne MVars und mit leeren
Stacks. Ein Endzustand ist erreicht, wenn die beiden Stacks leer sind und der auszuwertende
Ausdruck von der Form (return x), eine Abstraktion, eine Konstruktoranwendung oder der
Name einer MVar ist.
Definition 5.2.2. Für einen Ausdruck e ist der Startzustand der IO-Mark 1-Maschine das 5-Tupel
(∅, ∅, e, [], []). Ein Zustand der IO-Mark 1-Maschine ist ein Endzustand, wenn er von der Form
(H, M, return x, [], []), oder von der Form (H, M, v, [], []) ist, wobei v eine Abstraktion, eine Konstruktoranwendung oder der Name einer MVar ist.
Die Übergangsregeln (die Relation
geben. Wir gehen kurz auf diese ein:
IM1
) der IO-Mark 1-Maschine sind in Abbildung 5.2 ange-
• Die ersten neun Regeln sind direkt von der Mark 1-Maschine übernommen, und auf das
5-Tupel angepasst, wobei die neuen Komponenten – MVars und IO-Stack – keine Rolle
spielen
• Die (update)-Regel ist leicht angepasst, für den Fall, dass eine Variable ausgewertet werden soll, die den Namen einer MVar darstellt, in diesem Fall wird die Variable einfach
zurück in den Heap geschrieben.
• Sämtliche anderen Regeln (außer (blackhole)) werden nur ausgeführt, wenn der normale
Stack leer ist.
• Die Regel (newMVar) führt eine newMVar-Operation durch: Es wird eine neue MVar mit
entsprechendem Inhalt erzeugt und return y ist der neue auszuwertende Ausdruck, wobei y der Name der erzeugten MVar ist.
• Die Regel (takeMVar) führt eine takeMVar-Operation durch: Hierfür muss der aktuell auzuwertende Ausdruck der Name einer (gefüllten) MVar sein und der IO-Stack enthält als
oberstes Element einen #take -Eintrag.
• Die Regel (emptyTake) ist das Gegenstück zu (takeMVar), für den Fall, dass die MVar leer
ist. In diesem Fall blockiert die Berechnung eigentlich. Der Einfachheit halber geben wir an
dieser Stelle einfach den vorherigen Zustand zurück. Da die IO-Mark 1 noch keine nebenläufigen Threads unterstützt, bedeutet dies, dass sich die Maschine in einer Endlosschleife
befindet.
• Die Regeln (putMVar) und (fullPut) dienen zur Verarbeitung einer putMVar-Operation:
Diese liegt bereits auf dem IO-Stack und der aktuelle Ausdruck ist der Name einer MVar.
D. Sabel, FP-PR, SoSe 2015
57
Stand: 26. März 2015
5 Teil 3: Abstrakte Maschinen
Ist diese leer, so kann die Operation durchgeführt werden (die MVar wird gefüllt, und
return Unit ist das Resultat). Ist die MVar voll, so wird blockiert, d.h. der aktuelle Zustand ist auch der Nachfolgezustand.
• Die Regeln (pushTake) und (pushPut) werden durchgeführt, wenn die Maschine zum ersten Mal eine takeMVar- bzw. eine putMVar- Operation sieht: In diesem Fall muss das jeweils
erste Argument ausgewertet werden, bis es ein Name einer MVar ist. Um sich den Rücksprung zu merken, wird ein entsprechendes Element auf den IO-Stack gelegt.
• Die Regel (pushBind) wird durchgeführt, um in einem >>= -Operator zunächst die linke Aktion durchzuführen (auf dem IO-Stack wird sich gemerkt, dass man danach nach
rechts springen muss). Ist die Auswertung der linken Aktion beendet, so ist der aktuelle
Ausdruck von der Form return x, in diesem Fall wird die (lunit)-Regel verwendet, um
die Auswertung des >>= -Operators zu vollenden.
• Schließlich gibt es noch die (blackhole)-Regel, die Anwendung findet, wenn eine Variable
ausgewertet werden soll, die weder im Heap vorkommt, noch eine MVar repräsentiert.
Wir betrachten ein Beispiel:
Beispiel 5.2.3. Wir werten den Ausdruck
letrec x1 = True, x2 = (newMVar x1 ); x3 = (λx.takeMVar x) in x2 >>= x3
auf der IO-Mark 1 aus:
(∅, ∅, letrec . . . in x2 >>= x3 , [], [])





7 True,
p1 →

mkBinds 

, ∅, p2 >>= p3 , [], []
7 (newMVar p1 ),
 p2 →




p3 7→ (λx.takeMVar x)
IM1





p1 →
7 True,


pushBind 

, ∅, p2 , [], [# >>= (p3 )]
7 (newMVar p1 ),
 p2 →




p3 7→ (λx.takeMVar x)
IM1
enter
IM1
Stand: 26. März 2015
(
)
!
p1 →
7 True,
, ∅, (newMVar p1 ), [#heap (p2 )], [# >>= (p3 )]
p3 →
7 (λx.takeMVar x)
58
D. Sabel, FP-PR, SoSe 2015
5.2 Die IO-Mark 1
(pushApp)
(H, M, (e x), S, I)
(pushSeq)
(H, M, (seq e x), S, I)
(pushAlts)
(H, M, (case e of alts), S, I)
(takeApp)
(H, M, λx → e, #app (y) : S, I)
(takeSeq)
(H, M, v, #seq (y) : S, I)
(H, M, y, S, I),
falls v ein Mark 1-Wert ist
(branch)
(H, M, (c x1 . . . xn ), #case (. . . (c y1 . . . yn ) → e; . . .) : S, I)
(H, M, e[xi /yi ]ni=1 , S, I)
IM1
(H, M, e, #app (x) : S, I)
IM1
(H, M, e, #seq (x) : S, I)
IM1
(H, M, e, #case (alts) : S, I)
IM1
(H, M, e[y/x], S, I)
IM1
IM1
(H, M, (c x1 . . . xn ), #case (alts) : S, I)
Laufzeitfehler,
falls alts keine Alternative für Konstruktor c enthält
(nobranch)
IM1
· 7→ e}, M, y, S, I)
(H∪{y
(enter)
(mkBinds)
IM1
IM1
(H, M, e, #heap (y) : S, I)
(H, M, letrec x1 = e1 ; . . . ; xn = en in e, S, I)
· 1 7→ e1 [yi /xi ]ni=1 , . . . , yn 7→ en [yi /xi ]ni=1 }, M, e[yi /xi ]ni=1 , S, I)
(H∪{y
wobei yi frische Variablen sind
(update)
(newMVar)
· 7→ v}, M, v, S, I)
(H∪{y
(H, M, v, #heap (y) : S, I)
falls v ein Mark 1-Wert ist;
oder v m z oder v m • in M enthalten ist
IM1
· m x}, return y, [], I)
(H, M, newMVar x, [], I)
(H, M∪{y
wobei y eine neue Variable ist
IM1
(takeMVar)
· m y}, x, [], #take : I)
(H, M∪{x
IM1
· m •}, return y, [], I)
(H, M∪{x
(emptyTake)
· m •}, x, [], #take : I)
(H, M∪{x
IM1
· m •}, x, [], #take : I)
(H, M∪{x
(putMVar)
· m •}, x, [], #put (y) : I)
(H, M∪{x
· m y}, return Unit, [], I)
(H, M∪{x
IM1
(fullPut)
· m z}, x, [], #put (y) : I)
(H, M∪{x
· m z}, x, [], #put (y) : I)
(H, M∪{x
IM1
(pushTake)
(H, M, takeMVar x, [], I)
(pushPut)
(H, M, putMVar x y, [], I)
(pushBind)
(H, M, x >>= y, [], I)
(lunit)
(H, M, return x, [], # >>= (y) : I)
(blackhole)
(H, M, x, S, I)
(H, M, x, S, I)
falls x weder im Heap H noch in M vorkommt.
IM1
IM1
IM1
(H, M, x, [], #take : I)
(H, M, x, [], #put (y) : I)
(H, M, x, [], # >>= (y) : I)
IM1
(H, M, (y x), [], I)
IM1
Abbildung 5.2: Zustandübergangsregeln der IO-Mark 1
D. Sabel, FP-PR, SoSe 2015
59
Stand: 26. März 2015
5 Teil 3: Abstrakte Maschinen





p1 →
7 True,


update 

, ∅, (newMVar p1 ), [], [# >>= (p3 )]
7 (newMVar p1 ),
 p2 →




p3 7→ (λx.takeMVar x)
IM1





p
7
→
True,
1


newMVar 

, {q m p1 }, (return q), [], [# >>= (p3 )]
7 (newMVar p1 ),
 p2 →




p3 7→ (λx.takeMVar x)
IM1





p
7
→
True,
1


lunit 

, {q m p1 }, (p3 q), [], []
7 (newMVar p1 ),
 p2 →




p3 7→ (λx.takeMVar x)
IM1





p1 →
7 True,


pushApp 

, {q m p1 }, p3 , [#app (q)], []
7 (newMVar p1 ),
 p2 →




p3 7→ (λx.takeMVar x)
IM1
enter
IM1
(
)
!
p1 →
7 True,
, {q m p1 }, (λx.takeMVar x), [#heap (p3 ), #app (q)], []
p2 →
7 (newMVar p1 )





p1 →
7 True,


update 

, {q m p1 }, (λx.takeMVar x), [#app (q)], []
7 (newMVar p1 ),
 p2 →




p3 7→ (λx.takeMVar x)
IM1





p1 →
7 True,


takeApp 

, {q m p1 }, takeMVar q, [], []
7 (newMVar p1 ),
 p2 →




p3 7→ (λx.takeMVar x)
IM1





p1 →
7 True,


pushTake 

, {q m p1 }, q, [], [#take ]
7 (newMVar p1 ),
 p2 →




p3 7→ (λx.takeMVar x)
IM1





7 True,
p1 →

takeMVar 

, {q m •}, (return p1 ), [], []
7 (newMVar p1 ),
 p2 →




p3 7→ (λx.takeMVar x)
IM1
Stand: 26. März 2015
60
D. Sabel, FP-PR, SoSe 2015
5.2 Die IO-Mark 1
5.2.1 Implementierung
Durch die Implementierung der Mark 1-Maschine können wir einiges wiederverwenden: Der
Heap und der Stack können direkt verwendet werden. Für den IO-Stack, genügt es, ein passendes Modul für die Elemente des IO-Stacks zu implementieren:
I Aufgabe 18. Im Modul CHF.AbsM.Stack.IOStackElem ist der Datentyp für Elemente des IOStacks definiert als:
> data IOStackElem v = RetTake | RetPut v | RetBind v
>
deriving (Eq,Show)
Implementieren Sie dort die Funktionen:
• isRetTake :: IOStackElem v -> Bool, die ein IOStackElem-Element erhält und prüft, ob
dieses ein RetTake-Element ist.
• isRetPut :: IOStackElem v -> Bool, die ein IOStackElem-Element erhält und prüft, ob dieses ein RetPut-Element ist.
• isRetBind :: IOStackElem v -> Bool, die ein IOStackElem-Element erhält und prüft, ob
dieses ein RetBind-Element ist.
• mkRetTake :: IOStackElem v, die ein RetTake-Element erstellt.
• mkRetPut :: v -> IOStackElem v, die eine Variable erhält und daraus ein RetPut-Element
erstellt.
• mkRetBind :: v -> IOStackElem v, die eine Variable erhält und daraus ein RetBind-Element
erstellt.
• fromVarIOStack :: IOStackElem v -> v, die ein RetPut- oder RetBind-Element erhält und
die Variable heraus extrahiert.
J
Für die Menge der MVars verwenden wir ebenfalls den Heap-Datentyp, wobei die rechten
Seiten der Bindungen vom Typ Maybe Mark1Var sind, d.h. entweder eine Variable (dargestellt
als Just var) oder Nothing, was gerade bedeutet, dass die MVar leer ist.
Insgesamt definieren wir daher den Zustand der IO-Mark 1-Maschine im Modul
CHF.AbsM.IOMark1 als:
> data IOMark1State = IOMark1State
>
{heap :: Heap Mark1Var Mark1Expr ,
>
mvars :: Heap Mark1Var (Maybe Mark1Var),
D. Sabel, FP-PR, SoSe 2015
61
Stand: 26. März 2015
5 Teil 3: Abstrakte Maschinen
>
>
>
> }
control :: Mark1Expr,
stack :: (Stack (StackElem Mark1Var [MCAlt Mark1Cons Mark1Var])),
ioStack :: Stack (IOStackElem Mark1Var)
I Aufgabe 19. Implementieren Sie im Modul CHF.AbsM.IOMark1:
• Eine Funktion startState :: Mark1Expr -> IOMark1State, die für einen Ausdruck den
Startzustand der IO-Mark 1-Maschine berechnet.
• Eine Funktion
nextState :: IOMark1State
-> [Mark1Var]
-> (IOMark1State,[Mark1Var])
die als Eingaben einen Zustand der IO-Mark 1-Maschine und eine Liste von neuen Variablen erhält
und den entsprechenden Nachfolgezustand sowie die verbleibenden Variablen liefert, indem sie eine
der Transitionen aus Abbildung 5.2 auf den Eingabezustand anwendet.
• Eine Funktion
finalState :: IOMark1State
-> [Mark1Var]
-> (IOMark1State, [Mark1Var])
die für einen IO-Mark 1-Zustand und eine Liste neuer Variablen, versucht den Endzustand der
Maschine zu berechnen (und die Liste der nicht benötigten Variablen ebenfalls zurück liefert).
J
Eine Funktion exec :: Mark1Expr -> [Mark1Var] -> String, die für einen Ausdruck und
eine Liste neuer Variablen das Ergebnis der Auswertung als String berechnet, wird im Modul
CHF.AbsM.IOMark1 bereit gestellt. Beim Ausdrucken ist darauf zu achten, dass tieferliegende
monadische Aktionen nicht durch das Ausdrucken ausgeführt werden. Der Trick, den die Funktion exec hierfür verwendet, besteht darin, zunächst die IO-Mark 1-Maschine laufen zu lassen,
aber anschließend für die Argumentauswertung die Mark 1-Maschine zu verwenden.
I Aufgabe 20. Implementieren Sie im Modul CHF.Run eine Funktion runIOMark1, die ein CHFProgramm erwartet, dann das Programm lext, parst, der semantischen Analyse unterzieht, in eine
Mark1Expr umwandelt, schließlich auf der IO-Mark 1-Maschine laufen lässt und das Ergebnis als String
zurück liefert.
J
Stand: 26. März 2015
62
D. Sabel, FP-PR, SoSe 2015
5.3 Die Nebenläufige Maschine – Concurrent Mark 1
5.3 Die Nebenläufige Maschine – Concurrent Mark 1
Für die nebenläufige Maschine Concurrent Mark 1 können wir große Teile der IO-Mark 1Maschine wiederverwenden, da sie nahezu perfekt dazu passt einen einzelnen Thread auszuwerten. Im Grunde besteht der Schritt von der IO-Mark 1 zur Concurrent Mark 1 aus den
folgenden Teilschritten:
• Statt einem Ausdruck werden nun mehrere Ausdrücken(in Form einer Menge von
Threads) nebenläufig ausgewertet. Den Heap H und die MVars M teilen sich dabei alle
Threads. Jeder Thread hat jedoch: einen Ausdruck, einen eigenen Stack und einen eigenen
IO-Stack
• Es gibt einen ausgezeichneten Thread: Wenn dieser Thread terminiert, dann terminiert
das gesamte Programm. Um dies zu verwalten, werden wir den Threads Namen geben.
Dies ist auch nötig, damit die Threads zu Futures werden, die (von anderen Threads)
referenziert werden können. Der ausgzeichnete Thread erhält den Namen _main_.
• Wenn ein Thread (sagen wir namens y) seine Auswertung beendet hat (also der aktuelle
Thread die Form (return x) besitzt und beide Stacks leer sind), dann wird der Thread
entfernt und das Ergebnis wird als normale Heap-Bindung in den Heap eingefügt (also
die Bindung y 7→ x). Diese Regel wird für alle Threads außer dem ausgezeichneten Thread
verwendet.
• Die Regel für das Erzeugen einer Future (die future-Operation) muss hinzugefügt werden.
• Die Auswertung ist interleaved: Es wird ein Thread ausgewählt, dieser darf einen Transitionschritt machen, anschließend wird erneut gewählt usw.
• Für die Wahl des nächsten Threads, sollte eine Art von Fairness implementiert werden,
sodass jeder Thread nach endlich vielen Schritten der gesamten Maschine auch selbst ein
eigenen Schritt gemacht hat. Hierfür werden wir die Threads mit Ressourcen markieren
(soviele Schritte darf der Thread noch ausführen, bis der nächste an der Reihe ist), und
zudem eine Queue der Threads verwalten.
Formal definieren wir:
Definition 5.3.1. Eine Future (ein Thread) der Concurrent Mark 1-Maschine ist ein 4-Tupel (x, e, S, I)
wobei
• x ein Variablenname ist. Wir nennen x den Namen der Future, bzw. sagen wir manchmal auch
einfach „Future x“.
• e ist der durch die Future auszuwertende Ausdruck (Control).
D. Sabel, FP-PR, SoSe 2015
63
Stand: 26. März 2015
5 Teil 3: Abstrakte Maschinen
• S ist ein Stack.
• I ist ein IO-Stack.
Der Zustand der Concurrent Mark 1-Maschine ist ein 4-Tupel (H, M, T , Q), wobei
• H ist ein Heap.
• M ist eine Menge von MVars.
• T ist eine Menge von Futures.
• Q ist eine FIFO-Queue, die die Reihenfolge der Futures verwaltet und für jede Future aus T eine
Ressource (eine natürliche Zahl) enthält.
Ein Zustand ist gültig genau dann, wenn
• alle Namen der Futures in T verschieden sind, und
• T enthält eine Future namens _main_
Im Modul CHF.AbsM.CMark1 ist der Datentyp für Threads der Concurrent Mark 1-Maschine
definiert als
> data CMark1Thread = CMark1Thread {
>
name :: Mark1Var,
>
control :: Mark1Expr,
>
stack :: (Stack (StackElem Mark1Var [MCAlt Mark1Cons Mark1Var])),
>
ioStack :: Stack (IOStackElem Mark1Var)
>
}
Der Datentyp für den Zustand der Maschine ist definiert als
> data CMark1State = CMark1State {
>
heap :: Heap Mark1Var Mark1Expr,
>
mvars :: Heap Mark1Var (Maybe Mark1Var),
>
threads :: Heap Mark1Var CMark1Thread,
>
queue :: [(Int,Mark1Var)]
>
}
Für die Menge der Threads wird hier erneut die Heap-Datenstruktur benutzt, damit man einen
bestimmten Thread effizient in der Menge der Threads suchen kann. Aals Schlüssel für die
Einträge werden dabei die Namen der Threads benutzt. Die Queue wird dargestellt als Liste
von Paaren, wobei ein Paar gerade aus einer Ressource (ganze Zahl) und dem Namen der Future
besteht.
Stand: 26. März 2015
64
D. Sabel, FP-PR, SoSe 2015
5.3 Die Nebenläufige Maschine – Concurrent Mark 1
Definition 5.3.2. Sei e ein Ausdruck, dann ist der Startzustand der Concurrent Mark 1-Maschine
(∅, ∅, T , Q), wobei
• T = {(_main_, e, [], [])}
• Q enthält nur einen Eintrag: (1,_main_)
Ein Zustand der Concurrent Mark 1-Maschine ist ein Endzustand, wenn der Thread namens _main_
von der Form (_main_, return x, [], []) oder von der Form (_main_, v, [], []) ist, wobei v eine Abstraktion,
eine Konstruktoranwendung, oder der Name einer MVar ist.
In Abbildung 5.3 wird die Übergangsrelation
für die Concurrent Mark 1-Maschine definiert. Dabei darf stets derjenige Thread einen Schritt machen, der in der Queue Q ganz vorne
steht. Im Wesentlichen sind hierbei drei Fälle zu unterscheiden:
CM1
• Die Regel (unIO) wird verwendet, wenn eine nebenläufige Future ihre Berechnung beendet hat. In diesem Fall wird der Thread entfernt und eine Bindung im Heap für das
Resultat erzeugt.
• Die Regel (future) wird angewendet, wenn ein neuer Thread mithilfe einer futureOperation erzeugt werden soll. Hierbei wird eine neue Future angelegt: Es entsteht ein
neuer Thread, der hinten mit einer zufällig gezogenen Ressource in die Queue Q eingefügt wird. Der erzeugende Thread erhält den Namen der Future als Rückgabe.
• Die Regel (liftIOM) liftet die Regeln der IO-Mark 1-Maschine für die Concurrent Mark 1Maschine: Für den ausgewählten Thread wird eine IO-Mark 1-Transition angewendet.
Die letzten beiden Regeln passen dabei die Ressourcen des auswertenden Threads an: War die
Ressource echt größer 1, so wird sie um 1 erniedrigt, der Thread verbleibt jedoch vorne in der
Queue. War die Ressource 1, so muss sich der Thread ganz hinten in die Queue einreihen. Er
erhält dabei eine neue Ressource, die zufällig aus den natürlichen Zahlen gewählt wird. Diese
Ressourcen-Verfahren entspricht gerade dem Round-Robin-Verfahren mit „Zeitscheiben“: Ein
Thread darf eine zeitlang rechnen, wird dann inaktiv und erhält erst dann wieder Rechenzeit,
wenn alle anderen Threads rechnen durften. Man kann sich überlegen, dass diese Strategie fair
in der Hinsicht ist, dass jeder Thread nach endlich vielen Schritten rechnen darf. Eine Verbesserung wäre es, zusätzlich FIFO-Queues für jede MVar einzuführen, damit jeder Thread (der es
möchte) auch nach endlich vielen Schritt auf eine MVar zugreifen darf (wenn dies überhaupt
möglich ist).
I Aufgabe 21. Implementieren Sie im Modul CHF.AbsM.CMark1:
• Eine Funktion startState :: Mark1Expr -> CMark1State, die einen Ausdruck erhält und
den Startzustand der Concurrent Mark 1-Maschine für diesen Ausdruck berechnet.
D. Sabel, FP-PR, SoSe 2015
65
Stand: 26. März 2015
5 Teil 3: Abstrakte Maschinen
·
(H, M, T ∪{(x,
(return y), [], [])}, (n, x) : Q)
·
(H∪{x
7→ y}, M, T , Q)
x 6= _main_
(unIO)
CM1
·
(H, M, T ∪{(x,
(future y), [], I)}, (n, x) : Q)
(fork)
·
(H, M, T∪{(x,
(return z), [], I), (z, y, [], [])}, Q0 )
(n − 1, x) : Q++[(m, z)] falls n > 1
wobei Q0 :=
Q++[(m, z), (n0 , x)]
falls n = 1.
Hierbei sind m, n0 zufällig gewählte natürliche Zahlen;
z ist eine neue Variable.
CM1
(liftIOM)
·
(H, M, T ∪{(x,
e, S, I)}, (n, x) : Q)
0
·
(H , M0 , T ∪{(x,
e0 , S 0 , I 0 )}, Q0 )
CM1
(H0 , M0 , e0 , S 0 , I 0 )
falls (H, M, e, S, I)
0
wobei Q := (n − 1, x) : Q falls n > 1 und Q0 := Q++[(n0 , x)]
falls n = 1. Hierbei ist n0 eine zufällig gewählte natürliche Zahl.
Die Regel darf nur dann angewendet werden, wenn weder (fork)
noch (unIO) anwendbar sind.
IM1
Abbildung 5.3: Zustandübergangsregeln der Concurrent Mark 1
• Eine Funktion setSchedule :: CMark1State -> Int -> CMark1State, die einen Zustand
der Concurrent Mark 1-Maschine und eine Zahl m erhält (für den schon ein Schritt gemacht wurde) und das Scheduling nach dem Schritt durchführt:
– Der erste Eintrag der Queue Q wird betrachtet. Sei dieser (n, x)
– Falls n > 1 ist, so ändere den Eintrag ab auf (n − 1, x)
– Falls n = 1 ist, so setze den Eintrag auf (m, x) und verschiebe m von der Front der Queue
an die letzte Position in der Queue.
• Eine Funktion
nextState :: CMark1State
-> [Mark1Var]
-> [Int]
-> (CMark1State, [Mark1Var],[Int])
die einen Concurrent Mark 1-Zustand, eine Liste neuer Variablennamen und eine Liste von Zahlen
(für die Ressourcen) erhält, den Nachfolgezustand der Concurrent Mark 1-Maschine berechnet und
das 3-Tupel bestehend aus
– Nachfolgezustand
Stand: 26. März 2015
66
D. Sabel, FP-PR, SoSe 2015
5.3 Die Nebenläufige Maschine – Concurrent Mark 1
– verbleibende neue Namen
– verbleibende (nicht verwendete) Ressourcenzahlen
liefert.
• Eine Funktion
finalState :: CMark1State
-> [Mark1Var]
-> [Int]
-> (CMark1State, [Mark1Var],[Int])
die einen Concurrent Mark 1-Zustand, eine Liste neuer Variablennamen und eine Liste von Zahlen
(für die Ressourcen) erhält, den Endzustand der Concurrent Mark 1-Maschine berechnet und das
3-Tupel bestehend aus
– Endzustand
– verbleibende neue Namen
– verbleibende (nicht verwendete) Ressourcenzahlen
liefert.
J
Im Modul CHF.AbsM.CMark1 ist die Funktion
exec :: Mark1Expr -> [Mark1Var] -> [Int] -> String
bereits vorgegeben, die das Ergebnis der Concurrent Mark 1-Maschine ausdruckt. Hierbei geht
sie wie folgt vor: Monadische Unterausdrücke des _main_-Threads werden nicht mehr ausgeführt, sondern gedruckt (dieser Thread wird wie in der Mark 1-Maschine behandelt), nebenläufige Threads dürfen beim Drucken weiter Aktionen ausführen und werden daher wie in der
IO-Mark 1-Maschine behandelt.
I Aufgabe 22. Implementieren Sie im Modul CHF.Run eine Funktion runCMark1, die ein CHFProgramm erwartet, dann das Programm lext, parst, der semantischen Analyse unterzieht, in eine
Mark1Expr umwandelt, schließlich auf der Concurrent Mark 1-Maschine laufen lässt und das Ergebnis als String zurück liefert.
Testen Sie verschiedene Implementierungen für die Wahl der Liste von Ressourcen. Beispielsweise
könnte man die Liste (repeat 1) verwenden (allerdings sind die Zahlen dann nicht zufällig). Eine andere Möglichkeit besteht darin mit den Funktionen aus der Bibliothek System.Random eine Liste von
Pseudo-Zufallszahlen zu erzeugen.
D. Sabel, FP-PR, SoSe 2015
67
Stand: 26. März 2015
5 Teil 3: Abstrakte Maschinen
Implementieren Sie im Modul CHF.Run eine Funktion runCM1 :: String -> IO (), die wie
runCMark1 ein Eingabeprogramm analysiert und auf der Concurrent Mark 1-Maschine laufen lässt und
das Ergebnis ausdruckt. Sorgen Sie für einen nichtdeterministischen Ablauf, indem Sie die Ressourcenzahlen (zufällig) in Abhängigkeit von der Systemzeit erzeugen. Verwenden Sie hierfür die Bibliothek
J
System.CPUTime.
5.4 Der Interpreter
I Aufgabe 23. Implementieren Sie in der Datei Main.hs ein Modul Main, dass einen interaktiven
Interpreter für CHF bereitstellt. Hierbei soll ähnlich wie im GHCi agiert werden können. Die Minimalanforderungen sind:
• CHF-Ausdrücke können eingeben und ausgewertet werden
• Sind die Ausdrücke syntaktisch oder typfalsch, wird eine Fehlermeldung ausgegeben, der Interpreter wird aber nicht beendet.
• Datentypdefinitionen können mit :define Typdefinition hinzugefügt werden, d.h. der Interpreter „merkt“ sich, welche Datentypen eingegeben wurden.
• Mit :quit kann der Interpreter verlassen werden.
• Ein Quellprogramm kann aus einer Datei geladen und ausgeführt werden.
Hinweise:
• Zum Speichern der aktuellen Datentypdefinition empfiehlt es sich eine IORef aus dem Modul
Data.IORef zu verwenden.
• Zum Abfangen der Fehler sind die Funktionen try (oder auch catch) und evaluate aus der
Bibliothek Control.Exception hilfreich.
Compilieren Sie den Interpreter und testen Sie ihn ausgiebig.
Stand: 26. März 2015
68
J
D. Sabel, FP-PR, SoSe 2015
Literatur
Bird, R. S. (1998). Introduction to Functional Programming Using Haskell. Prentice-Hall.
Chakravarty, M. M. T. & Keller, G. C. (2004). Einführung in die Programmierung mit Haskell.
Pearson Studium.
Hudak, P., Peterson, J., & Fasel, J. H. (2000). A Gentle Introduction to Haskell. Online
verfügbar unter http://haskell.org/tutorial/.
Jones, S. P., editor (2003). Haskell 98 Language and Libraries. Cambridge University Press.
Auch online verfügbar unter http://haskell.org/definition.
Lipovača, M. (2011). Learn You a Haskell for Great Good! A Beginner’s Guide. No Starch Press,
first edition. Online verfügbar unter http://learnyouahaskell.com/.
O’Sullivan, B., Stewart, D., & Goerzen, J. (2008). Real World Haskell. O’Reilly Media, Inc.
Webseite zum Buch: http://book.realworldhaskell.org/read/.
Sabel, D. (2015). Begleitmaterial zum Praktikum „Funktionale Programmierung.
http://www.ki.informatik.uni-frankfurt.de/lehre/SS2015/FP-PR/.
Sabel, D. & Schmidt-Schauß, M. (2011). A contextual semantics for concurrent Haskell
with futures. Frank report 44, Institut für Informatik. Fachbereich Informatik und Mathematik. J. W. Goethe-Universität Frankfurt am Main. http://www.ki.informatik.unifrankfurt.de/papers/frank/frank-44.pdf.
Schmidt-Schauß, M. (2014). Skript zur
grammierung
2“
(Sommersemester
frankfurt.de/∼prg2/SS2014/index.html.
Vorlesung
2014).
„Grundlagen der Prohttp://www.informatik.uni-
Sestoft, P. (1997). Deriving a lazy abstract machine. J. Funct. Programming, 7(3):231–264.
Thompson, S. (1999). Haskell – The Craft of Functional Programming. Addison-Wesley.
und David Sabel, M. S.-S. (2014). Skript zur Vorlesung „Einführung in die Funktionale Programmierung“ (Wintersemester 2014 / 2015). http://www.ki.informatik.unifrankfurt.de/lehre/WS2014/EFP/.
Stand: 26. März 2015
D. Sabel, FP-PR, SoSe 2015
Herunterladen