Funktionale Programmierung mit Haskell Prof. Dr. Hans J. Schneider Lehrstuhl für Programmiersprachen und Programmiermethodik Friedrich-Alexander-Universität Erlangen-Nürnberg Sommersemester 2011 I. Die Sprache Haskell II. Fallstudien 8. Suchbäume und beliebige Graphen 9. Lambda-Interpretierer 10. Syntaxanalyse 11. Algebraische Spezifikation 12. Semantik operationeller Sprachen 13. Kategorien in Haskell 14. Compilieren funktionaler Programme c Hans J. Schneider 2011
Motivation Philip Wadler: The essence of functional programming (Principles of Programming Languages, 1992) Rein funktional oder nicht? Pure functional languages (Haskell, Miranda) offer the power of lazy evaluation and the simplicity of equational reasoning. Impure functional languages (ML, Scheme) offer a tempting spread of features such as state, exception handling, or continuations. Wadlers Entscheidung hängt davon ab, wie leicht ein Programm geändert werden kann: Pure languages ease change by making manifest the data upon which each operation depends. But, sometimes, a seemingly small change may require a program [... ] to be extensively restructured. Funktionale Programmierung mit Haskell 9.1
Beispiel: Interpretierer für den Lambda-Kalkül Wadlers Beispiel beschränkt sich auf Addition, Funktionsdefinition und Funktionsanwendung. Definition der zu interpretierenden Terme: data Term = Var Name Con Int Add Term Term Lam Name Term App Term Term Weitere Operationen können problemlos eingebaut werden. Die Beschränkung auf den Typ Int erspart die Typprüfung. Die Interpretation ist wesentlich einfacher als die für zuweisungsorientierte Sprachen. (Siehe später!) Funktionale Programmierung mit Haskell 9.2
Kern eines Interpretierers Kern eines Interpretierers: Typ: interp (Var x) e = lookup x e interp (Con i) e = Num i interp (Add u v) e = add (interp u e) (interp v e) interp (Lam x v) e = Fun (\a -> interp v ((x,a):e)) interp (App t u) e = apply (interp t e) (interp u e) interp Wir benötigen noch: :: Term -> Environment -> Value die Basisfunktionen add und apply, die Definitionen von Term, Environment, und Value, kümmern uns darum aber erst in der Fassung mit monadischen Klassen. Funktionale Programmierung mit Haskell 9.3
Wadlers Varianten des Interpretierers To add error handling to it, I need to modify the result type to include error values, and at each recursive call to check for and handle errors appropriately. exception handling To add an execution count to it, I need to modify the result type to include such a count, and modify each recursive call to pass around such counts appropriately. global variable To add an output instruction to it, I need to modify the result type to include an output list, and to modify each recursive call to pass around this list appropriately. side effect Wadlers Vorschlag mit monadischen Klassen: All that is required is to redefine the monad and to make a few local changes. Funktionale Programmierung mit Haskell 9.4
Modifikation der Basiswerte Das Beispiel wird auf ganzzahlige Werte und Funktionen darauf beschränkt: data Value = Wrong Num Int Fun (Value -> M Value) Der Typkonstruktor M erlaubt, das eigentliche (rechnerische) Ergebnis um Zusatzinformationen zu erweitern. Die Werte müssen ausdruckbar sein: instance Show Value where show Wrong = "<wrong>" show (Num i) = show i show (Fun f) = "<function>" Das Ausdrucken der M-Werte muss abhängig von deren Definition geregelt werden. Hinweis: In Wadlers Arbeit werden unterschiedliche Konstruktoren verwandt. Funktionale Programmierung mit Haskell 9.5
Standardversion Value benötigt keine zusätzliche Information (Wadler-Variante 0): newtype M a = M a instance Show a => Show (M a) where show (M a) = show a Allgemeine Definition der monadischen Interpretiererklasse: class InterpMonad m where unit :: a -> m a -- entspricht return bind :: m a -> (a -> m b) -> m b -- entspricht >>= fail :: String -> m a instance InterpMonad M where unit a = M a (M a) bind k = k a fail s = error s -- wird nicht aufgerufen Wadler definiert für jede Version eine eigene Klasse und muss dann auch die Methoden unterschiedlich benennen. Funktionale Programmierung mit Haskell 9.6
Grundlegende Funktionen Operationen auf den Werten: Funktionsanwendung: -- apply :: Value -> Value -> M Value apply (Fun k) a = k a apply notfun a = unit Wrong Arithmetische Operationen: -- add :: Value -> Value -> M Value add (Num i) (Num j) = unit (Num (i+j)) add a b = unit Wrong Umgebung: type Environment = [(Name, Value)] type Name = String -- lookup :: Name -> Environment -> M Value lookup x [] = unit Wrong lookup x ((y,b):e) = if x==y then unit b else lookup x e Funktionale Programmierung mit Haskell 9.7
Interpretation der Terme Die Interpretation eines Termes liefert, abhängig von der Umgebung einen monadischen Wert: -- interp :: Term -> Environment -> M Value Interpretation der Atome: interp (Var x) e = lookup x e interp (Con i) e = unit (Num i) interp (Lam x v) e = unit (Fun (\a -> interp v ((x,a):e))) Interpretation der vordefinierten Operationen: interp (Add u v) e = interp u e bind (\x -> interp v e bind (\y -> add x y)) Interpretation der Funktionsanwendung: interp (App t u) e = interp t e bind (\f -> interp u e bind (\x -> apply f x)) Funktionale Programmierung mit Haskell 9.8
Erinnerung unit verwandelt Value in M Value: unit :: a -> m a unit a = M a interp (Con i) e = unit (Num i) Was macht bind? bind :: m a -> (a -> m b) -> m b (M y) bind k = k y bind gibt einen M Value an eine Funktion Value -> M Value weiter, die einen neuen M Value berechnet. interp (Add u v) e = interp u e bind (\x -> interp v e bind (\y -> add x y)) Funktionale Programmierung mit Haskell 9.9
Beispiele Testfunktion: -- test :: Term -> String test t = show (interp t []) Testbeispiele: twice = (Lam "f" (Lam "x" (App (Var "f") (App (Var "f") (Var "x"))))) increm = (Lam "x" (Add (Var "x") (Con 1))) term12 = (App (App twice increm) (Con 5)) Main> test term12 "7" Main> test increm "<function>" Main> test (App increm (Con 10)) "11" Beispiel mit Fehler: Main> test (App (App twice (Con 3)) (Con 5)) "<wrong>" Funktionale Programmierung mit Haskell 9.10
Interpretierer mit Fehlermeldungen Wir unterscheiden in M Value zwischen erfolgreichen und fehlerbehafteten Berechnungen: data M a = Suc a Err String Fehler werden bis zum Ende der Berechnung durchgereicht: instance InterpMonad M where unit a = Suc a (Suc a) bind k = k a (Err s) bind k = Err s fail s = Err s und schließlich ausgedruckt: instance Show a => Show (M a) where show (Suc a) = "Success: " ++ show a show (Err s) = "Error: " ++ s Man könnte auch auf die Meldung Success verzichten. Funktionale Programmierung mit Haskell 9.11
Notwendige Änderungen interp muss gar nicht geändert werden. lookup muss an einer Stelle geändert werden: lookup x [] = fail("unbound variable: " ++ x) lookup x ((y,b):e) = if x==y then unit b else lookup x e apply muss an einer Stelle geändert werden: apply (Fun k) a = k a apply notfun a = fail("should be function: " ++ show notfun) add muss an einer Stelle geändert werden: add (Num i) (Num j) = unit (Num (i+j)) add a b = fail("should be numbers: " ++ show a ++ ", " ++ show b) Man könnte bei add noch eine Fallunterscheidung bezüglich des nicht korrekten Summanden einfügen. Funktionale Programmierung mit Haskell 9.12
Beispiele term12 = (App (App twice increm) (Con 5)) terme1 = (Add (Var "x") (Con 10)) terme2 = (Add (Con 10) term9) terme4 = (App (App twice (Con 3)) (Con 5)) terme5 = (App (App twice (Con 3)) (Var "x")) Korrektes Beispiel: Main> test term12 "Success: 7" Inkorrekte Beispiele: Main> test terme4 "Error: should be function: 3" Main> test terme1 "Error: unbound variable: x" Main> test terme2 "Error: should be numbers: 10, <function>" Main> test terme5 "Error: unbound variable: x" Funktionale Programmierung mit Haskell 9.13
Interpretierer mit Reduktionszähler Das Zählen der Reduktionen (Anwendung eines λ-ausdruckes auf ein Argument) wird hier als Beispiel für das Mitführen von Zuständen und ihrer Transformation betrachtet. Eine Zustandstransformation geht von einem Zustand aus und liefert einen neuen Zustand, zusammen mit einem Wert: newtype M a = M (State -> (a, State)) Gegenüber der Fassung von Wadler ist hier der Konstruktor M eingefügt. Für das Zählerbeispiel genügt: type State = Int tick = M (\s -> ((), s+1)) Am Ende sollen das Ergebnis und der akkumulierte Zähler ausgedruckt werden: instance (Show a) => Show (M a) where show (M f) = let (a, s) = f 0 in "Value: " ++ show a ++ " Count: " ++ show s Funktionale Programmierung mit Haskell 9.14
Einbettung in die monadische Klasse unit liefert den gegebenen Wert, ohne den Zustand zu ändern.: instance InterpMonad M where unit a = M (\s -> (a, s)) bind :: M a -> (a -> M b) -> M b nimmt eine Zustandstransformation M m und eine Funktion k und transformiert den Anfangszustand s0 mit m zu einem Paar (a,s1): (M m) bind k = M (\s0 -> let (a, s1) = m s0 k erzeugt durch Anwendung auf diesen Wert a eine neue Zustandstransformation M m : (M m ) = k a An diese wird der Zwischenzustand s1 weitergegeben: in m s1 ) fail s = error s -- wird nicht aufgerufen Funktionale Programmierung mit Haskell 9.15
Einfügen des Zählers State ist in dieser Variante ein Zähler: newtype M a = M (State -> (a, State)) tick = M (\s -> ((), s+1)) Wir müssen die Zähloperation in die Interpretationen von add und apply einfügen. add: add (Num i) (Num j) = tick bind (\() -> unit (Num (i+j))) add a b = unit Wrong apply apply (Fun k) a = tick bind (\() -> k a) apply notfun a = unit Wrong Funktionale Programmierung mit Haskell 9.16
Simulation von add add (Num i) (Num j) = tick bind (\() -> unit (Num (i+j))) = M (\s -> ((), s+1)) bind (\() -> unit (Num (i+j))) = M (\s0 -> let (a, s1) = (\s -> ((), s+1) s0 = ((), s0+1) a = () s1 = s0+1 (M m ) = (\() -> unit (Num (i+j))) a = unit (Num (i+j)) = M (\s -> (Num (i+j), s)) m = (\s -> (Num (i+j), s)) in (m s1) = (\s -> (Num (i+j), s)) (s0+1) = (Num (i+j), s0+1) Funktionale Programmierung mit Haskell 9.17
Beispiele term1 = (Add (Con 10) (Con 11)) term4 = (Lam "x" (Add (Var "x") (Var "x"))) term5 = (App term4 term1) Interpretation mit der ursprünglichen Version: *Main> test term5 "42" Interpretation mit der Zähler-Version: *Main> test term5 "Value: 42 Count: 3" Fehlerhafte Beispiele: terme1 = (Add (Var "x") (Con 10)) terme4 = (App (App twice (Con 3)) (Con 5)) *Main> test terme1 "Value: <wrong> Count: 0" *Main> test terme4 "Value: <wrong> Count: 2" Funktionale Programmierung mit Haskell 9.18
Call-by-name-Interpretierer Die bisherigen Lösungen simulieren das Prinzip call-by-value : Das Argument ist ein Wert. Fun:: Value -> M Value Haskell macht das natürlich lazy! Bei Auswertung mit call-by-name muss die Auswertung des Arguments zurückgestellt werden. Das Argument bleibt eine Berechnung: Fun:: M Value -> M Value Notwendige Änderung des Datentyps Value: data Value = Wrong Num Int Fun (M Value -> M Value) Variablen sind jetzt an Berechnungen gebunden: type Environment = [(Name, M Value)] Deshalb genügt in lookup jetzt b statt unit b: lookup x [] = unit Wrong lookup x ((y,b):e) = if x==y then b else lookup x e Funktionale Programmierung mit Haskell 9.19
Änderungen in den Berechnungen Die Auswertung von Konstanten und der Addition ändert sich nicht. Die Änderung bei der Auswertung der Variablen ist bereits in lookup berücksichtigt. Die Auswertung der Lambda-Ausdrücke sieht zwar gleich aus: interp (Lam x v) e = unit (Fun (\a -> interp v ((x,a):e))) interp (Lam x v) e = unit (Fun (\m -> interp v ((x,m):e))) Der Parameter ist jetzt aber vom Typ M Value, nicht mehr vom Typ Value. Die wesentliche Änderung passiert in der Auswertung der Funktionsapplikation: Die Funktion wird ausgewertet, nicht aber das Argument. interp (App t u) e = interp t e bind (\f -> apply f (interp u e)) In diesen Interpretierer können wie zuvor Fehlermeldungen und/oder ein Reduktionszähler durch Anpassung des Typs State eingebaut werden. Funktionale Programmierung mit Haskell 9.20
Zusammenfassung des Call-by-name-Interpretierers Interpretierer: interp :: Term -> Environment -> M Value interp (Var x) e = lookup x e interp (Con i) e = unit (Num i) interp (Add u v) e = interp u e bind (\a -> interp v e bind (\b -> add a b)) interp (Lam x v) e = unit (Fun (\m -> interp v ((x,m):e))) interp (App t u) e = interp t e bind (\f -> apply f (interp u e)) Wadler: An advantage of the monadic style is that the types make clear where effects occur. Thus, one can distinguish call-by-value from call-by-name simply by examining the types. Funktionale Programmierung mit Haskell 9.21
Vergleich Wir betrachten folgendes Beispiel: term1 = (Add (Con 10) (Con 11)) term4 = (Lam "x" (Add (Var "x") (Var "x"))) term5 = (App term4 term1) Ergebnis bei Verwendung des Call-by-name-Interpretierers: *Main> test term5 "Value: 42 Count: 4" Ergebnis bei Verwendung des Call-by-value-Interpretierers: *Main> test term5 "Value: 42 Count: 3" Bei Verwendung des Call-by-value-Interpretierers wird term1 nur einmal ausgewertet. Funktionale Programmierung mit Haskell 9.22
Zustandstransformationen Monadische Klassen können benutzt werden, um Programme zu formulieren, die interne Zustände benutzen: data State s a = ST (s -> (a, s)) (M.P. Jones: Lect. Notes Comp. Sc. 925, 1995) Einbettung in die vordefinierte Klasse Monad: instance Monad (State s) where return x = ST (\s -> (x,s)) -- entspricht unserem unit m >>= f = ST (\s -> let ST m = m (x, s1) = m s ST f = f x in f s1 ) -- entspricht unserem bind Passender Funktor: instance Functor (State s) where fun f (ST st) = ST (\s -> let (x, s ) = st s in (f x, s ) ) Funktionale Programmierung mit Haskell 9.23