Funktionale Programmierung mit Haskell Prof. Dr. Hans J. Schneider Lehrstuhl für Programmiersprachen und Programmiermethodik Friedrich-Alexander-Universität Erlangen-Nürnberg Sommersemester 2010 I. Die Sprache Haskell II. Ergänzungen 7. Sprachergänzungen 8. Ergänzungen zu binären Bäumen 9. Syntaxanalyse 10. Algebraische Spezifikation 11. Semantik von Programmiersprachen 12. Compilieren funktionaler Programme c Hans J. Schneider 2010
Methoden der Semantikdefinition Operationelle Methode: Transitionssystem (abstrakter Automat) δ : Z Z partielle Überführungsfunktion F Z Endzustände (Bedeutung) Semantik: f : P rograms Data Z Axiomatische Methode: Zustandsraum Prädikate über dem Zustandsraum Bedeutung von Anweisung(sfolg)en: Prädikattransformationen Semantik: Axiome über Prädikattransformationen Denotationelle Methode: Zustandsmenge Funktionen, die Zustände auf Zustände abbilden Funktionen, die elementaren Anweisungen entsprechen Funktionen (höherer Ordnung), die zusammengesetzten Anweisungen entsprechen Zur Vorbereitung betrachten wir die Simulation der Wertzuweisung in Haskell. Funktionale Programmierung mit Haskell 11.1
Simulation der Wertzuweisung Wenn das Konzept der funktionalen Programmierung universell ist, muss man die Wertzuweisung simulieren können. Wir betrachten den Speicher als eine Funktion, die zu jeder Adresse den Wert liefert: type Store a = Int -> a Wegen der Typbindung brauchen wir für jeden Datentyp eine eigene Funktion. Alternative: Wir betten alle benötigten Datentypen in eine gemeinsame Oberklasse ein. Zuweisung eines Wertes liefert bei nachfolgenden Zugriffen auf die Funktion ein anderes Ergebnis, wenn genau nach dieser Speicherstelle gefragt wird: assign :: Store a -> Int -> a -> Store a assign s i v i = if i == i then v else s(i ) Funktionale Programmierung mit Haskell 11.2
Simulation der Wertzuweisung (II) assign :: Store a -> Int -> a -> Store a assign s i v i = if i == i then v else s(i ) Beispiele: null_vector i = 0 s1 = assign (assign (null_vector) 1 0.5) 2 0.25 s2 = assign s1 3 (-1.5) s3 = assign s1 1 1.0 Main> s1 1 0.5 Main> s1 2 0.25 Main> s1 5 0.0 Main> s3 1 1.0 Main> s3 2 0.25 Funktionale Programmierung mit Haskell 11.3
Denotationelle Semantik in Haskell Jeder syntaktische Bereich wird in einen semantischen Bereich abgebildet. Syntaktischer Bereich = Menge von Zeichenketten Semantischer Bereich = Menge von Funktionen Beispielsprache besteht nur aus Deklarationen von Konstanten Formale Beschreibung der Semantik von Ausdrücken und Deklarationen Rolle der Umgebung (Was ist wo wie definiert?) Unmittelbare Übersetzung in Haskell Was ändert sich bei der Berücksichtigung von Variablen und Wertzuweisungen? Funktionale Programmierung mit Haskell 11.4
Semantische Bereiche Die denotationelle Semantik basiert auf Funktionen, deren Definitionsund Wertebereiche festgelegt werden müssen. Basisbereiche: Bool = { Bool, T, F } Num = { Num, 0, 1, 2, 3,...} Gleichheitstest: e = D e liefert Bool, falls e = D e = D. Bedingungen: Seien e 1 Bool, e 2, e 3 D. Dann ist if e 1 then e 2 else e 3 = D e 2 e 3 falls e 1 = Bool falls e 1 = T falls e 1 = F Im Haskell-Programm verwenden wir statt Num den vorhandenen Bereich Int. Funktionale Programmierung mit Haskell 11.5
Zusammengesetzte semantische Bereiche Funktionsbereiche: D 1 D 2 Produktbereiche: D 1 D 2... D n D 1 D 2... D n verschmilzt Tupel, die mindestens ein Di enthalten (smash-produkt). Summenbereiche: D 1 + D 2 +... + D n disjunkte Vereinigung mit neuem D D 1 D 2... D n verschmilzt die Di zu einem D Im Folgenden benutzte spezielle Bereiche: Val = Bool Num ValO = Val {, } Env = (Ident ValO) Funktionale Programmierung mit Haskell 11.6
Eine Beispielsprache Wir betrachten eine einfache Programmiersprache, die nur Deklarationen der Form const i = expr enthält. Beispiel: let const i = 1, j = 2, k = i, l = i+3; in l+k; Interessierender semantischer Bereich: Env = Ident ValO Semantische Funktionen: E : expr (Env ValO) CD : const decl (Env Env) Funktionale Programmierung mit Haskell 11.7
Semantik der Ausdrücke Bei Ausdrücken mit dyadischen Operatoren wird vorausgesetzt, dass beide Operanden einen definierten Wert haben: E [[e 1 op e 2 ]] = λe. OP [[op]](smash(e [[e 1 ]]e, E [[e 2 ]]e)) Bei bedingten Ausdrücken muss der boolesche Ausdruck definiert sein, ebenso der von diesem ausgewählte: E [[if e 1 then e 2 else e 3 ]] = λe.(λt. if t then E [[e 2 ]]e else E [[e 3 ]]e)(e [[e 1 ]]e) Der Wert eines Bezeichners hängt von der Umgebung ab, die durch die Deklarationen aufgebaut wurde: E [[i]] = λe. bound i e bound : Ident Env ValO bound = λi.λe.[id Val, λx. ](e(i)) Funktionale Programmierung mit Haskell 11.8
Semantik der Deklarationen Eine Deklaration fügt einen neuen Bezeichner ein oder definiert ihn um: CD [[const i = expr]] = λe. binding i (E [[expr]] e) binding : Ident ValO Env = Ident (ValO (Ident ValO)) binding = λi.λv.λi. if i = i then in 1 (v) else in 2 ( ) Eine nachfolgende Deklaration ändert die bisherige Umgebung: CD [[d 1 ; d 2 ]] = λe.(λe 1. overlay(cd [[d 2 ]](overlay(e 1, e)), e 1 ))(CD [[d 1 ]]e) overlay : Env Env Env overlay = λ(e, e ).λi.[id Val, λx.e (i)](e(i)) Geschachtelte Ausdrücke hängen auch von der äußeren Umgebung ab: E [[let d in expr]] = λe. E [[expr]](overlay(cd [[d]]e, e)) Funktionale Programmierung mit Haskell 11.9
Syntaktische Bereiche in Haskell Syntaktische Bereiche sind auf eine abstrakte Syntax reduziert, betrachten also nicht konkrete Schreibweisen wie Semikolon usw. Wir betrachten zu Testzwecken nur den Deklarationsteil: type Prog = DeclList Abstrakte Syntax der Konstantendeklarationen: type DeclList = [Decl] type Decl = (Ident, Expr) Abstrakte Syntax der arithmetischen Ausdrücke: data Expr = Number Int Const Ident Dyadic Operator Expr Expr data Operator = Plus Minus Times type Ident = [Char] Funktionale Programmierung mit Haskell 11.10
Semantische Bereiche in Haskell Umgebung: type Env = Ident -> Val_O Definierte und undefinierte Werte: data Val_O = Defined Val Undefined O_Dom deriving (Eq, Show) data O_Dom = Bottom Top deriving (Eq, Show) Top bezeichnet nichtdeklarierte Bezeichner, Bottom die deklarierten Bezeichner, denen kein definierter Wert zugewiesen ist. Fallunterscheidung bei der disjunktiven Vereinigung: is_defined(defined _) = True is_defined(undefined _) = False Vereinfachung: type Val = Int Funktionale Programmierung mit Haskell 11.11
Verarbeitung der Ausdrücke in Haskell Der Wert eines Ausdruckes hängt von der Umgebung ab, in der er ausgewertet wird: eval_expr :: Expr -> Env -> Val_O Mustervergleich liefert die verschiedenen Fälle: Der Wert einer Zahl ist durch diese gegeben: eval_expr (Number x) e = Defined x Der Wert eines Bezeichners ist durch die in der Umgebung gültige Deklaration gegeben: eval_expr (Const i) e = bound i e Operationen sind auszuwerten: eval_expr (Dyadic op e1 e2) e = eval_dyadic op (eval_expr e1 e) (eval_expr e2 e) Funktionale Programmierung mit Haskell 11.12
Verarbeitung der Operationen in Haskell Operationen werden nur ausgewertet, wenn beide Operanden definiert sind: eval_dyadic :: Operator -> Val_O -> Val_O -> Val_O eval_dyadic op (Defined x) (Defined y) = Defined (eval_op op x y) eval_dyadic op (Undefined x) y = Undefined Bottom eval_dyadic op x (Undefined y) = Undefined Bottom Beschränkung auf den einfachsten Fall von Operatoren: eval_op :: Operator -> Int -> Int -> Int eval_op Plus = (+) eval_op Minus = (-) eval_op Times = (*) Funktionale Programmierung mit Haskell 11.13
Verarbeitung der Deklarationen in Haskell Deklarationsliste: eval_decl_list :: DeclList -> Env -> Env eval_decl_list [] e = e eval_decl_list (d1:d2) e = overlay (eval_decl_list d2 (overlay e e)) e where e = eval_decl d1 e CD [[d 1 ; d 2 ]] = λe.(λe 1. overlay(cd [[d 2 ]](overlay(e 1, e)), e 1 ))(CD [[d 1 ]]e) Einzelne Deklaration: eval_decl (i,ex) e = binding i (eval_expr ex e) Gesamtprogramm : eval_prog :: Prog -> Env eval_prog d = (eval_decl_list d) empty_env Funktionale Programmierung mit Haskell 11.14
Verwaltung der Umgebung in Haskell Zugriff auf eine Bindung: bound :: Ident -> Env -> Val_O bound i e = if is_defined(e(i)) then e(i) else Undefined Bottom Veränderung der Bindungen: binding :: Ident -> Val_O -> Ident -> Val_O {- Env -> Env -} binding i v i = if i == i then v else Undefined Top Überlagern von Gültigkeitsbereichen: overlay :: Env -> Env -> Env overlay e e i = if is_defined(e(i)) then e(i) else e (i) Funktionale Programmierung mit Haskell 11.15
Beispiel Initialisierung: empty_env :: Env empty_env i = Undefined Top Testbeispiel (nur der Aufbau der Umgebung): bsp1 = eval_prog [("i",number 1),("j",Number 2), ("k", Const "i"), ("l", Dyadic Plus (Const "i") (Number 3))] Abfrage: Semantik> bsp1 "i" Defined 1 Semantik> bsp1 "l" Defined 4 Semantik> bsp1 "m" Undefined Top Funktionale Programmierung mit Haskell 11.16
Einfache Variablen Einfache Variablen werden durch Speicherplätze repräsentiert. Loc ist der Bereich der Speicherplätze. SV ist der Bereich der speicherbaren Werte. ST ist der Bereich der Speicherzustände. ST = Loc (SV {, }) ( bedeutet frei; bedeutet reserviert, aber nicht initialisiert.) Anpassung der Umgebungsdefinition: Env = Ident (SV Loc) Zugriffsfunktionen auf den Speicher: stored : Loc ST SV reserv : Loc ST ST store : Loc SV ST ST Funktionale Programmierung mit Haskell 11.17
Änderung bei der Auswertung von Bezeichnern Umgebungsdefinition: Env = Ident (SV Loc) Auswertung auf der rechten Seite einer Anweisung: RE : expr Env ST SV Auf der rechten Seite benötigen wir in jedem Fall einen Wert! RE [[id]] = λe.λs. if e(id) SV then e(id) else s(e(id)) Auswertung auf der linken Seite einer Anweisung: LE : expr Env ST Loc Auf der linken Seite benötigen wir in jedem Fall einen Speicherplatz! LE [[id]] = λe.λs. if e(id) SV then else e(id) Funktionale Programmierung mit Haskell 11.18
Verwaltung der Umgebung Operationen auf der Umgebung: empty : ST reserv : Loc ST ST store : Loc SV ST ST stored : Loc ST SV Reservierung von Speicherplätzen: empty = λl. reserv = λl.λs.(λl. if l = l then else s(l )) Zugriff auf Speicherplätze: store = λl.λv.λs.(λl. if l = l then v else s(l )) stored = λl.λs.[id SV, λx. ]s(l) Beachte: s(l) ist entweder in SV oder in {, }. Außerdem braucht man eine Funktion, die einen unbenutzten Speicherplatz bereitstellt. Funktionale Programmierung mit Haskell 11.19
Anweisungen Syntaktischer Bereich: stat ::= id := expr stat; stat if expr then stat if expr then stat else stat Semantische Funktion: S : Stat Env ST ST Sequenzen und Bedingungen: S [[s 1 ; s 2 ]] = λe. S [[s 2 ]]e S [[s 1 ]]e S [[if b then s 1 ]] = λe.λs. if RE [[b]]e s then S [[s 1 ]]e s else s S [[if b then s 1 else s 2 ]] = λe.λs. if RE [[b]]e s then S [[s 1 ]]e s else S [[s 2 ]]e s Funktionale Programmierung mit Haskell 11.20
Hinweis auf Ergänzungen Variablendeklarationen verändern Env und ST: VD : var decl Env ST (Env ST) VD [[var i]] = λe.λs.(λl.(binding i l e, reserv l s)(new loc)) CD und VD müssen zusammengefasst werden zu D. Eine Wertzuweisung verändert nur ST: S [[id := expr]] = λe.λs.(store (LE [[id]] e s) (RE [[expr]] e s) s) (Hier fehlt der Test, ob die linke Seite definiert ist.) Blöcke: BL : block Env ST (Env ST) BL [[decls; stats]] = λe.λs.(λz.(e, S [[stats]] pr 1 (z) pr 2 (z))(d [[decls]] e s) ) Funktionale Programmierung mit Haskell 11.21