Technische Universität Braunschweig Institut für Programmierung und Reaktive Systeme Dr. Werner Struckmann 8. Dezember 2011 Programmieren für Fortgeschrittene 4. Übungsblatt Das Thema dieses Übungsblatts sind rekursive Datentypen. Auf diesem Blatt starten wir mit der Implementierung eines Interpreters einer einfachen Sprache: PCF (Programming Computable Functions). Diese orientiert sich in der Syntax an üblichen semantischen Definitionen von Programmiersprachen durch Fixpunktterme. Sie kann als Erweiterung des getypten Lambda-Kalküls gesehen werden. Die Syntax von PCF ist durch folgende BNF-Grammatik gegeben: 1 e ::= x | n | true | false | succ | pred | iszero | ( e ) 2 | if e then e else e 3 | e e 4 | \ x -> e 5 | fix x -> e Dabei steht x für einen Bezeichner, n für eine natürliche Zahl (mit 0), true und false sind boolesche Literale. succ, pred sind die üblichen successor und predecessor Funktionen auf N, während iszero auf Gleichheit mit 0 prüft. Weiterhin können Ausdrücke geklammert werden. Um Berechnungen zu ermöglichen, existiert das normale if-then-else- Konstrukt und die Applikation von Funktionen e e. Zuletzt können mit \x -> e und fix x -> e Variablen eingeführt werden. Dabei entspricht das erste Konstrukt einer Funktion mit einer Variablen x und einem Ausdruck e, in dem x verwendet werden darf. Das zweite Konstrukt ist der Fixpunkt-Operator oder Rekursion. Die Verwendung wird später behandelt. Folgende Regeln werden benutzt, um Ausdrücke eindeutig auswerten zu können: • Funktionsapplikation ist links-assoziativ, d. h. f g h wird als (f g) h ausgewertet. • Funktionsapplikation bindet am stärksten, d. h. z. B. \f -> f 0 wird als \f -> (f 0) ausgewertet. Zu dieser kontextfreien Grammatik existiert ein Syntaxbaum. Die zugehörigen Datentypen sind die folgenden: 1 2 3 4 5 6 7 8 9 10 type Id = String data BuiltInFunction = Succ | Pred | IsZero deriving Eq data Value = NumVal Int | BoolVal Bool deriving Eq data Term = Var Id | Const Value | BuiltIn BuiltInFunction 11 12 13 14 15 | If Term Term Term | Lambda Id Term | App Term Term | Fix Id Term deriving Eq Diese entsprechen der Grammatik. Für die folgenden Aufgaben werden vorgefertigte Dateien bereitgestellt, die vordefinierte Funktionen und insbesondere vordefinierte Testfälle enthalten. Darin ist auch eine Cabal-Beschreibung enthalten. Diese kann zum Testen genutzt werden. Dazu müssen in dem Verzeichnis die folgenden Befehle ausgeführt werden: 1 2 3 cabal configure -- enable - tests cabal build cabal test Ein Aufruf sollte Anfangs zur Ausgabe aller Fehler führen. Am Ende dieses Blattes sollte lediglich noch die Ausgabe Test suite test-pcf: PASS erscheinen. Die Implementierung befindet sich in dem Ordner lib/PCF. Die Testfälle finden Sie in der Datei test/test-pcf.hs. Dort können Sie auch sehen, was erfüllt sein müsste. Das Arbeitstier der Implementierung ist die Funktion eval :: Term -> Either PcfError Term, die einen gegebenen Term auswertet oder einen Fehler liefert. In den folgenden Aufgaben wird diese Auswertungsfunktion schrittweise implementiert. Die benötigten Regeln werden hier zur Verfügung gestellt. Zunächst sollten Sie sich mit dem Typ Either vertraut machen. Dieser ist definiert durch 1 data Either a b = Left a | Right b Wenn Either zur Fehlerbehandlung verwendet wird, wird der Fehler per Konvention in dem Left-Konstruktor hinterlegt, während das Ergebnis in Right liegt. Aufgabe 10: Schreiben Sie die Funktion natSum von Blatt 3 um, sodass Left statt error verwendet wird, d.h. die Funktion erhält die Signatur natSum :: Int -> Either String Int. Schreiben Sie anschließend eine Funktion natSumList :: [Int] -> Either String Int, die für jeden Eintrag in der gegebenen Liste natSum aufruft und die Ergebnisse aller Aufrufe addiert. Sie können dafür foldl benutzen. Im Folgenden werden wir ein Regelsystem angeben, das die Semantik der Sprache definiert. Wir nutzen dabei eine Art der Darstellung, die als „natural semantics“ bezeichnet wird (vergleiche Kahn, Natural Semantics). Die Regeln bestehen dabei aus Bewertungen („judgements“) der Form e ⇒ v. Dies bedeutet, dass e zum Wert v ausgewertet wird. Die Regeln werden dabei soweit angewendet, bis keine weitere Auswertung mehr möglich ist oder eine Auswertung nichts mehr ändert. –2– Axiome: n⇒n true ⇒ true und f alse ⇒ f alse succ ⇒ succ, pred ⇒ pred und iszero ⇒ iszero (1) (2) (3) Regeln: b ⇒ true e1 ⇒ v (4) if b then e1 else e2 ⇒ v Oben stehen die Hypothesen und unten die Konklusion. Das heißt. falls b zu true ausgewertet wird und e1 zum Wert v, dann kann der Ausdruck if b then e1 else e2 zu v ausgewertet werden. Analog für den else-Zweig: b ⇒ f alse e2 ⇒ v if b then e1 else e2 ⇒ v Die Auswertung der vordefinierten Funktionen ist wie folgt definiert: e1 ⇒ succ e2 ⇒ n e1 e2 ⇒ n + 1 e1 ⇒ pred e2 ⇒ 0 e1 ⇒ pred e2 ⇒ n + 1 e1 e2 ⇒ 0 e1 e2 ⇒ n e1 ⇒ iszero e2 ⇒ 0 e1 ⇒ iszero e2 ⇒ n + 1 e1 e2 ⇒ true e1 e2 ⇒ f alse Aufgabe 11: (5) (6) (7) (8) Implementieren Sie die Regeln (1) bis (8) durch die Funktion eval :: Term -> Either PcfError Term. Dabei sollten Sie die Auswertung der Regeln (6) bis (8) in die Funktion Term -> Term -> Either PcfError Term auslagern. Für alle anderen Konstrukte kann die vorgegebene Implementierung, die einen Fehler zurückgibt beibehalten werden. Beachten Sie, dass im Fehlerfall die Auswertung abgebrochen und der Fehler weitergereicht werden soll (siehe vorige Aufgabe). Danach sollten Sie die Testfälle check1 bis check6 abdecken. Um eine Funktion definiert durch \x -> e o. Ä. auszuwerten, muss ein Verfahren zur Übergabe von Parametern definiert werden. Wir wählen hier die textuelle Ersetzung. Wir werden dabei die Notation e[x := t] verwenden, um die Ersetzung von x durch t in e zu bezeichnen. Zum Beispiel ergibt (succ x)[x := 1] den Ausdruck succ 1. Hierbei darf in e der Bezeichner x nur dann ersetzt werden, wenn x frei vorkommt, d. h. nicht durch \x -> oder fix x -> gebunden ist. Beispiel: ((\x -> succ x) (pred x))[x := 3] ergibt ((\x -> succ x) (pred 3)). Aufgabe 12: Implementieren Sie die Funktion subst :: Term -> Id -> Term -> Term, die in einem Term e, alle freien Vorkommen eines Bezeichners x durch einen Term t ersetzt. d. h. e[x := t] berechnet. Nach der Implementierung sollten auch die Testfälle check7 und check8 erfüllt sein. –3– Als nächstes wollen wir Lambda-Terme auswerten. Dazu werden diese zunächst wie die eingebauten Funktionen zu sich selber ausgewertet: λx → e ⇒ λx → e (9) Die eigentliche Berechnung kommt durch die Applikation auf ein Argument zustande. Wir benutzen call-by-value, d. h. zuerst wird das Argument ausgewertet, danach in dem Rumpf des Lambda-Ausdrucks ersetzt und dann die Ersetzung ausgewertet. e1 ⇒ λx → e e2 ⇒ v1 e1 e2 ⇒ v e[x := v1 ] ⇒ v (10) Beispiel: (\x -> succ x) (succ 0). Hier wird zuerst succ 0 zu 1 ausgewertet. Dann wird x im Rumpf succ x ersetzt und zu 2 ausgewertet. Beachten Sie, dass für Bezeichner keine Auswertungsregel existiert. Diese werden lediglich durch die obige Ersetzung belegt. Sollte bei der Auswertung eines Ausdrucks also ein Bezeichner auftreten, so ist dieser ungebunden und muss zu einem Fehler führen. Aufgabe 13: Benutzen Sie die Funktion subst, um eval so zu erweitern, dass Funktionsapplikationen nach Regel (10) ausgewertet wird. Dies sollte sollte dafür sorgen, dass Ihre Implementierung die Testfälle check7b und check8b erfüllt. Das letzte Konstrukt der Sprache, das noch umgesetzt werden muss, ist die Fixpunktbestimmung bzw. Rekursion. Diese ist interessanterweise sehr einfach: e[x := (fix x → e)] ⇒ v (fix x → e) ⇒ v (11) Was passiert hier? Wenn wir einen Ausdruck e gegeben haben, dann ersetzen wir das Vorkommen der Fixpunktvariablen durch den Ausdruck inklusive des Rekursionsschritt. Hier noch ein Beispiel für ein Fixpunktkonstrukt, an dem man sich die Funktionsweise klar machen sollte (Berechnung der Summe zweier Zahlen): 1 fix sum -> \ x -> \ y -> 2 if iszero x then y else sum ( pred x ) ( succ y ) Dies entspricht dem Haskellkonstrukt 1 sum x y = if iszero x then y else sum ( x - 1) ( y + 1) Aufgabe 14: Implementieren Sie abschließend die Fixpunktbestimmung in eval nach obiger Regel. Ihre Implementierung sollte nun alle Testfälle erfüllen. –4–