Funktionale Programmierung mit Haskell Prof. Dr. Hans J. Schneider Lehrstuhl für Programmiersprachen und Programmiermethodik Friedrich-Alexander-Universität Erlangen-Nürnberg Sommersemester 2012 I. Die Sprache Haskell 1. Einführung in Haskell 2. Funktionen als Basis der Programmierung 3. Algebraische Typen 4. Grundlagen des Typsystems 5. Strukturierung durch Typklassen 6. Verbergen von Information 7. Sprachergänzungen II. Fallstudien c Hans J. Schneider 2012
Literate programming Literate Programming kombiniert Programmcode und ausführliche Dokumentation. Programmcode beginnt jeweils mit der ersten Zeile nach einer Zeile, die mit \begin{code} beginnt. Programmcode endet jeweils unmittelbar vor der Zeile, die mit \end{code} beginnt. Alle anderen Zeilen werden vom Haskell-Interpretierer (Compiler) nicht beachtet. Diese anderen Zeilen können beispielsweise LaTeX-Zeilen sein. Der Bericht über die Implementierung der Graphtransformationen mit Haskell (siehe Internetseite zu den Graphtransformationen) ist so geschrieben. Bei Bedarf kann die code-umgebung durch Kopieren der verbatim- Umgebung definiert werden. Funktionale Programmierung mit Haskell 7.1
Übersicht Haskell wertet Parameter erst aus, wenn sie benötigt werden, und nur soweit, wie sie benötigt werden (Lazy evaluation). Dies ermöglicht (potentiell) unendlich lange Listen. Es kann sinnvoll sein, aus Effizienzgründen ausdrücklich die strikte Auswertung zu verwenden. Lazy evaluation ist auch beim Mustervergleich möglich: lazy patterns. Die Unifikation wird nur teilweise durchgeführt. Einige Anmerkungen zur Ein- und Ausgabe. Funktionale Programmierung mit Haskell 7.2
Unendliche Listen Die rekursive Implementierung der Fibonacci-Zahlen ist sehr aufwendig: fib 0 = 1 fib 1 = 1 fib n = fib (n-2) + fib (n-1) Direkter Zugriff auf die Liste bereits berechneter Zahlen: fib 0 = 1 fib 1 = 1 fib n = flist!!(n-1) + flist!!(n-2) where flist = map fib [0..] map fib ordnet jeder Komponenten einer Liste ihre Fibonacci-Zahl zu. [0..] bezeichnet die unendliche Liste [0,1,2,3,4,...] Unendliche Listen sind möglich, weil Haskell Parameter erst und soweit auswertet, wie sie wirklich benötigt werden. Funktionale Programmierung mit Haskell 7.3
Sieb des Erathostenes Man braucht nicht zu wissen, wie lang eine Liste wird! Sieb des Erathostenes: primes = sieve([2..]) where sieve(p:x) = p : sieve([n n<-x, n mod p > 0]) Auch auf unendliche Listen können alle Listenoperationen angewandt werden: prime_number(i) = primes!! (i-1) Main> prime_number 150 863 Main> initial(15, primes) [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47] Funktionale Programmierung mit Haskell 7.4
Grenzwerte Grenzwerte sind ebenfalls ein Beispiel, wo man nicht im vorhinein weiß, wie weit die Liste geht. Definition: limes(a:b:x) = if a == b then a else limes(b:x) limes(other) = error "incorrect use of limes" Bemerkung: other ist ein beliebig gewählter Bezeichner. Beispiel: f(x) = x 2 z = 0 Newtonsche Iteration: f(x i+1 ) = x i f(x i) f (x i ) = 1 ) 2 (x i + zxi wurzel z = limes (iterate f 1) where f(x) = (x + z/x)/2.0 Main> wurzel 3 1.73205080756888 Da bei Gleitpunktzahlen Rundungsfehler auftreten, ist der Gleichheitstest etwas gefährlich. (siehe Übung) Funktionale Programmierung mit Haskell 7.5
Funktionen als Objekte Funktionen sind Objekte wie alle anderen und können entsprechend verwendet werden, z.b. als Operanden spezieller Funktionen oder als Komponenten in Datenstrukturen. Komposition von Funktionen: f1 x = 2*x f2 x = x+2 f3 = f1. f2 Liste von Funktionen: pow 0 n = 1 pow i n = n * (pow (i-1) n) powers = [ pow i i <- [0..] ] In der angewandten Mathematik spielen Folgen von Funktionen etwa bei der Approximation eine Rolle. Funktionale Programmierung mit Haskell 7.6
Funktoren I Wir kennen bereits Funktionen höherer Ordnung, die Funktionen auf alle Elemente einer Liste anwenden, z.b.: map f [] = [] map f (x:xs) = f x : map f xs Im Standardvorspann definiert: class Functor s where fmap :: (a -> b) -> s a -> s b Damit können wir eine Funktion auf die Elemente einer beliebigen Struktur anwenden, z.b. eines binären Baumes: instance Functor StandBinTree where fmap f EmptyBinTree = EmptyBinTree fmap f (Node(x, l, r)) = Node(f x, (fmap f l), (fmap f r)) Main> t2 (87 (123 (471) (200)) (256 () (317))) Main> fmap (+1) t2 (88 (124 (472) (201)) (257 () (318))) Funktionale Programmierung mit Haskell 7.7
Funktoren II fmap wendet eine Funktion auf alle Elemente einer Struktur an. class Functor s where fmap :: (a -> b) -> s a -> s b Bei rekursiv aufgebauten Strukturen macht es auch Sinn, eine Funktion auf Teilstrukturen anzuwenden. class RecFunctor s where rmap :: (s a -> b) -> s a -> s b instance RecFunctor StandBinTree where rmap f EmptyBinTree = EmptyBinTree rmap f (Node(x, l, r)) = Node(f (Node(x,l,r)), (rmap f l), (rmap f r)) Main> t2 (87 (123 (471) (200)) (256 () (317))) Main> rmap height t2 (3 (2 (1) (1)) (2 () (1))) Main> rmap balance t2 (0 (0 (0) (0)) (1 () (0))) Funktionale Programmierung mit Haskell 7.8
Lazy Patterns: Beispiel Struktur eines Client-Server-Systems: requests Client Server responses initialize Der Server verarbeitet die Anfragen des Client und liefert für jede Anfrage eine Antwort: server (req:reqs) = process req : server reqs Der Client konstruiert aus dem Anfangswert und den Antworten des Servers eine Ergebnisliste: client initialize ~(resp:resps) = initialize : client (next resp) resps ~(resp:resps) verhindert die Auswertung von resps. next bearbeitet die Antwort vor der Weitergabe. Quelle: A Gentle Introduction to Haskell 98 Funktionale Programmierung mit Haskell 7.9
Lazy Patterns: Beispiel (II) Struktur eines Client-Server-Systems: requests Client Server responses initialize Einfaches Beispiel einer Verarbeitung: initialize = 0 next resp = resp process req = req + 1 Kommunikation der Komponenten: requests = client initialize responses responses = server requests Beispiellauf: Main> take 10 requests [0,1,2,3,4,5,6,7,8,9] Funktionale Programmierung mit Haskell 7.10
Lazy Patterns: Semantik A lazy pattern has the form ~pat. Lazy patterns are irrefutable: matching a value v against ~pat always succeeds. If an identifier in pat is later used on the right-hand side, it will be bound to that portion of the value that would result if v were to successfully match pat, and otherwise. Beispiel: Aufruf: client initialize responses Definition: client initialize ~(resp:resps) = initialize : client (next resp) resps Ohne ~ ergibt sich in unserem Beispiel. Funktionale Programmierung mit Haskell 7.11
Strikte Parameterauswertung Die Funktion seq erreicht, dass ein Parameter stets ausgewertet wird. Ihre Definition entspricht: seq b = seq a b = b, if a Beispiel: take :: Integer -> [Integer] -> [Integer] take 0 _ = [] take _ [] = [] take n (x:xs) = x : take (n-1) xs bot = bot take 0 bot funktioniert. seq (take 0) bot funktioniert nicht! Funktionale Programmierung mit Haskell 7.12
Die Standardklasse Show The Show class is used to convert values to strings (Report App. A.2). class Show a where showsprec :: Int -> a -> ShowS show :: a -> String showlist :: [a] -> ShowS Beschreibung der Funktionen: showsprec and showlist return a String-to-String function, to allow constant-time concatenation of its results using function composition. A specialised variant, show, is also provided, which uses precedence context zero, and returns an ordinary String. The method showlist is provided to allow the programmer to give a specialised way of showing lists of values. Aus der Betrachtung der vordefinierten Klassen kann man einiges lernen. Funktionale Programmierung mit Haskell 7.13
Default-Methoden in der Standardklasse Show ShowS ist ein Funktionstyp: type ShowS = String -> String Die Klasse Show: class Show a where showsprec :: Int -> a -> ShowS show :: a -> String showlist :: [a] -> ShowS showsprec _ x s = show x ++ s show x = showsprec 0 x "" Es genügt, entweder show zu definieren oder showsprec. Weiter gibt es: shows = showsprec 0 Funktionale Programmierung mit Haskell 7.14
Beispiele zu ShowS showchar und showstring sind Curry-Funktionen: type ShowS = String -> String showchar showstring :: Char -> ShowS :: String -> ShowS showchar a ist eine Funktion, die das Zeichen a an den Anfang einer Zeichenkette stellt: showchar = (:) Main> showchar a "bcd" "abcd" Entsprechend setzt showstring eine Zeichenkette voran: showstring = (++) Main> showstring "ab" "cde" "abcde" Funktionale Programmierung mit Haskell 7.15
Traditionelle Implementierung von showlist Ohne Curry-Funktion würde man das folgendermaßen machen: showlist :: (Show a) => [a] -> String showlist [] = "[]" showlist (x:xs) = "[" ++ show x ++ showl xs where showl [] = "]" showl (x:xs) = "," ++ show x ++ showl xs Die Auflösung der Rekursion führt zu fortgesetzten Konkatenationsoperationen: showlist [1,2,3] = "[" ++ show 1 ++ showl [2,3] = "[1" ++ "," ++ show 2 ++ showl [3] = "[1,2" ++ "," ++ show 3 ++ showl [] = "[1,2,3" ++ "]" = "[1,2,3]" und damit zu quadratischer Laufzeit! (Aus Gründen der Übersichtlichkeit sind die weiter links stehenden Konkatenationsoperationen zusammengefasst, obwohl ++ rechtsassoziativ ist.) Funktionale Programmierung mit Haskell 7.16
Standardimplementierung von showlist Implementierung im Standardvorspann: showlist :: [a] -> (String -> String) showlist [] = showstring "[]" showlist (x:xs) = showchar [. shows x. showl xs where showl [] = showchar ] showl (x:xs) = showchar,. shows x. showl xs Arbeitsweise der Standardimplementierung: showlist [1,2,3] "" = showchar [. shows 1. showl [2,3] "" = showchar [. shows 1. showchar,. shows 2. showl [3] "" = showchar [. shows 1. showchar,. shows 2. showchar,. shows 3. showl [] "" Da die Funktionskomposition rechtsassoziativ ist, läuft das auf eine fortgesetzte Anwendung des cons-operators hinaus! Funktionale Programmierung mit Haskell 7.17
Monadische Klassen Die erste Begegnung mit monadischen Klassen ist schwierig, aber das IO-System von Haskell basiert darauf. Monad An entity or elementary being thought of as a microcosmos or ultimate unit (Webster). Mathematically, monads are governed by set of laws that should hold for the monadic operations (A gentle introduction...). Functor (haben wir schon gesehen) Die Klasse Functor definiert nur eine einzige Funktion: fmap. fmap wendet eine Operation auf alle Elemente innerhalb eines Containers an und liefert einen Container von gleicher Gestalt zurück. Gesetze für fmap: fmap id = id fmap (f. g) = fmap f. fmap g Funktionale Programmierung mit Haskell 7.18
Klasse Monad Definition im Vorspann: class Monad m where (>>=) :: m a -> (a -> m b) -> m b (>>) :: m a -> m b -> m b return :: a -> m a fail :: String -> m a return fügt einen Wert vom Typ a in eine Monade über a ein. >> verknüpft zwei Monaden, bei >>= ist die Verknüpfung parametrisiert. m >> k = m >>= \_ -> k fail s = error s Die Listen sind eine Instanz von Monad: (>>=):: [a] -> (a -> [b]) -> [b] m >>= k = concat (map k m) Main> "abc" >>= (\x -> [succ x]) "bcd" Main> zip [1,2,3] [4,5,6] >>= (\(x,y) -> [x*y]) [4,10,18] Funktionale Programmierung mit Haskell 7.19
Do-Ausdrücke (I) Do-Ausdrücke sind eine suggestive Syntax für Ketten monadischer Operationen. Beispiel: Main> zip [1,2,3] [4,5,6] >>= (\(x,y) -> [x*y]) [4,10,18] Main> do {(x,y) <- zip [1,2,3] [4,5,6]; [x*y] } [4,10,18] Den Linkspfeil <- kennen wir von den Zermelo-Fraenkel-Ausdrücken: Main> [ x*y (x,y) <- zip [1,2,3] [4,5,6] ] [4,10,18] Definition der do-ausdrücke: do {e} = e do {e;stmts} = e >> do {stmts} do {p <- e; stmts} = let ok p = do {stmts} ok _ = fail "..." in e >>= ok ok ist eine (von p abhängige) Funktion. Funktionale Programmierung mit Haskell 7.20
Do-Ausdrücke (II) Beispiel für >>: Main> do {putstr "abc"; putstr "\n"} abc Main> putstr "abc" >> putstr "\n" abc Beispiel aus dem Standardvorspann: getline :: IO String getline = do c <- getchar if c == \n then return "" else do s <- getline return (c:s) getchar :: IO Char -- primitive Funktion Die IO-Funktionen im Standardvorspann sind in diesem Stil definiert. Funktionale Programmierung mit Haskell 7.21
Anhang: Gesetze für Monaden Instances of Monad should satisfy the following laws: return a >>= k = k a m >>= return = m m >>= (\x -> k x >>= h) = (m >>= k) >>= h (Assoziativgesetz) Instances of both Monad and Functor should additionally satisfy the law: fmap f xs = xs >>= return. f Beispiel: Listen f e = f(e) (return. f) e = [f(e)] xs >>= return. f = [f(e) e <- xs ] = map f xs Funktionale Programmierung mit Haskell 7.22
Anhang: Die Standardklasse Read The Read class is used to convert values from strings (Report 7.1 / A.2): class Read a where readsprec :: Int -> ReadS a readlist :: ReadS [a] Die Standardtypen sind Instanzen von Read, z.b. instance Read Float where readsprec p = readfloat Main> :info readfloat readfloat :: RealFrac a => ReadS a Anmerkung: RealFrac umfasst standardmäßig Float und Double. Unbequem: type ReadS a = String -> [(a,string)] Main> readfloat "1.6 3.6 7.8" [(1.6," 3.6 7.8")] Am bequemsten ist es, Beispiele in den Dateien selbst zu speichern. Funktionale Programmierung mit Haskell 7.23
Anhang: Hilfsfunktionen für Read Funktion, die eine Eingabezeichenkette in eine Liste von Gleitpunktzahlen umwandelt: Main> readfloatlist "1.6 3.4 9.0" [1.6,3.4,9.0] Definition: readfloatlist "" = [] :: [Float] readfloatlist s = head:(readfloatlist rest) where [(head, rest)] = readfloat (removeblanks s) removeblanks [] = [] removeblanks ( :rest) = removeblanks rest removeblanks other = other Der Datentyp ReadS erlaubt jedoch die Kombination verschiedener Typen in der Eingabezeichenkette. Vorsicht: In den neueren Hugs-Versionen (ab März 2005) sind Funktionen wie beispielsweise readfloat nicht mehr automatisch verfügbar. Lösung: import Numeric Funktionale Programmierung mit Haskell 7.24