Programmieren in Haskell Programmiermethodik Peter Steffen Universität Bielefeld Technische Fakultät 16.01.2009 1 Programmieren in Haskell Bisherige Themen Was soll wiederholt werden? Bedienung von hugs oder ghc Grundlegende Haskell-Funktionen Syntax und Semantik von Haskell Datentypen Typklassen Musik Lokale Definitionen mit where und let Programmieren mit Listen foldr und Kolleginnen Arrays Hash-Tabellen Ein- und Ausgabe Abstrakte Datentypen 2 Programmieren in Haskell Spezifikation Aus dem Schülerduden Informatik: Präzise Darstellung und Beschreibung der Eigenschaften, der Verhaltensweisen, des Zusammenwirkens und des Aufbaus von Modellen, Programmen oder Systemen, wobei meist von Details abstrahiert wird und konkrete Implementierungen keine Rolle spielen. Die Spezifikation stellt im Idealfall eine vollständig formale, von Implementierungen unabhängige Beschreibung des Verhaltens eines zu erstellenden Systems dar. Das Ziel einer Spezifikation liegt erstens in einem klaren Verständnis des Problems, seiner besonderen Eigenschaften und seiner Lösungen. Zweitens dient sie als Vergleichsmaß dafür, ob ein System oder Programm den gewünschten Anforderungen genügt. 3 Programmieren in Haskell Spezifikation des Sortierproblems Informell: Gesucht ist eine Funktion sort, die eine gegebene Liste von Elementen aufsteigend anordnet. Beispiele: sort sort sort sort 4 [8, 3, 5, 3, 6, 1] "hello world" ["Bein", "Anfall", "Anna"] [(1, 7), (1, 3), (2, 2)] ⇒ ⇒ ⇒ ⇒ [1, 3, 3, 5, 6, 8] " dehllloorw" ["Anfall", "Anna", "Bein"] [(1, 3), (1, 7), (2, 2)] Programmieren in Haskell Spezifikation der Sortierfunktion sort :: (Ord a) => [a] -> OrdList a ordered ordered ordered ordered :: (Ord a) [] [a] (a1:a2:as) => [a] -> Bool = True = True = a1 <= a2 && ordered (a2:as) Spezifikation 1: Für alle Listen x :: [τ ] muß gelten: ordered (sort x) = True . Reicht das? Nein: sort xs = [] 5 erfüllt ebenfalls die Spezifikation Programmieren in Haskell (1) Spezifikation der Sortierfunktion sort :: (Ord a) => [a] -> OrdList a ordered ordered ordered ordered :: (Ord a) [] [a] (a1:a2:as) => [a] -> Bool = True = True = a1 <= a2 && ordered (a2:as) Spezifikation 1: Für alle Listen x :: [τ ] muß gelten: ordered (sort x) = True . Reicht das? Nein: sort xs = [] 5 erfüllt ebenfalls die Spezifikation Programmieren in Haskell (1) Spezifikation der Sortierfunktion sort :: (Ord a) => [a] -> OrdList a ordered ordered ordered ordered :: (Ord a) [] [a] (a1:a2:as) => [a] -> Bool = True = True = a1 <= a2 && ordered (a2:as) Spezifikation 1: Für alle Listen x :: [τ ] muß gelten: ordered (sort x) = True . Reicht das? Nein: sort xs = [] 5 erfüllt ebenfalls die Spezifikation Programmieren in Haskell (1) Spezifikation der Sortierfunktion sort :: (Ord a) => [a] -> OrdList a ordered ordered ordered ordered :: (Ord a) [] [a] (a1:a2:as) => [a] -> Bool = True = True = a1 <= a2 && ordered (a2:as) Spezifikation 1: Für alle Listen x :: [τ ] muß gelten: ordered (sort x) = True . Reicht das? Nein: sort xs = [] 5 erfüllt ebenfalls die Spezifikation Programmieren in Haskell (1) Multimengen ∅ die leere Multimenge, *a+ die einelementige Multimenge, die genau ein Vorkommen von a enthält, x ] y die Vereinigung der Elemente von x und y ; das +“ im ” Vereinigungszeichen deutet an, dass sich die Vorkommen in x und y akkumulieren. ∅]x x ]∅ x ]y (x ] y ) ] z 6 = = = = x x y ]x x ] (y ] z) Programmieren in Haskell (2) (3) (4) (5) Multimengen bag :: [a] -> Bag a bag [] = ∅ bag (a:as) = *a+ ] bag as Eine Liste x enthält alle Elemente von y , falls bag x = bag y . In diesem Fall heißt x Permutation von y . Spezifikation 2: Für alle Listen x :: [τ ] muß gelten: ordered (sort x) = True ∧ bag (sort x) = bag x . (6) Dadurch ist sort als mathematische Funktion, nicht aber als Programm, eindeutig bestimmt. 7 Programmieren in Haskell Abstrakter Datentyp Multimenge module Bag (Bag, emptyBag, bagEmpty, inBag, addBag, delBag, appendBag, headBag, tailBag) where import List emptyBag bagEmpty inBag addBag delBag appendBag headBag tailBag 8 :: :: :: :: :: :: :: :: Bag Bag (Eq (Eq (Eq Bag Bag Bag a a -> Bool a) => a -> a) => a -> a) => a -> a -> Bag a a -> a a -> Bag a Bag a -> Bool Bag a -> Bag a Bag a -> Bag a -> Bag a Programmieren in Haskell Abstrakter Datentyp Multimenge emptyBag erzeugt eine neue Multimenge bagEmpty überprüft, ob eine Multimenge leer ist inBag überprüft, ob ein Element in der Multimenge enthalten ist addBag fügt ein Element einer Multimenge hinzu delBag löscht ein Element aus einer Multimenge appendBag vereinigt zwei Multimengen headBag gibt das erste Element aus der Multimenge aus tailBag entfernt das erste Element aus der Multimenge 9 Programmieren in Haskell Implementierung emptyBag = Bag [] bagEmpty (Bag []) = True bagEmpty _ = False inBag x (Bag xs) = elem x xs addBag x (Bag xs) = Bag (x:xs) delBag x (Bag xs) = Bag (filter (/= x) xs) appendBag (Bag xs) (Bag ys) = Bag (xs ++ ys) headBag (Bag []) = error "headBag on empty bag" headBag (Bag (x:xs)) = x tailBag (Bag []) = error "tailBag on empty bag" tailBag (Bag (x:xs)) = Bag xs instance (Eq a, Ord a) => Eq (Bag a) where (Bag xs) == (Bag ys) = sort xs == sort ys 10 Programmieren in Haskell Strukturelle Rekursion auf Listen length :: [a] -> Int length [] = 0 length (a:as) = 1 + length as -- vordefiniert Die Funktion length folgt dem Schema der strukurellen Rekursion. Für jeden Konstruktor des Datentyps, [] und (:), gibt es eine Gleichung. Der Konstruktor (:) ist rekursiv im zweiten Argument, über dieses Argument erfolgt der rekursive Aufruf. Man sagt, die Funktion ist strukturell rekursiv definiert. 11 Programmieren in Haskell Strukturelle Rekursion auf Listen Rekursionsbasis: [] Rekursionsschritt: (a:as) Schema der strukturellen Rekursion auf Listen: f :: [σ] -> τ f [] = e1 f (a : as) = e2 where s = f as Dabei sind e1 und e2 Ausdrücke vom Typ τ und e2 darf die Variablen a, as und s (nicht aber f ) enthalten. Mit s wird gerade die Lösung für as bezeichnet. Tritt s nur einmal in e2 auf, kann man natürlich für s auch direkt f s einsetzen. 12 Programmieren in Haskell Sortieren durch Einfügen insertionSort :: (Ord a) => [a] -> OrdList a insertionSort [] = e1 insertionSort (a : as) = e2 where s = insertionSort as insertionSort :: (Ord a) => [a] -> OrdList a insertionSort [] = [] insertionSort (a:as) = insert a s where s = insertionSort as 13 Programmieren in Haskell Sortieren durch Einfügen insertionSort :: (Ord a) => [a] -> OrdList a insertionSort [] = e1 insertionSort (a : as) = e2 where s = insertionSort as insertionSort :: (Ord a) => [a] -> OrdList a insertionSort [] = [] insertionSort (a:as) = insert a s where s = insertionSort as 13 Programmieren in Haskell Erweitertes Rekursionsschema Erweitertes Rekursionsschema: g :: σ1 -> [σ2 ] -> τ g i [] = e1 g i (a : as) = e2 where s = g e3 as wobei e1 die Variable i, e2 die Variablen i, a, as und s und e3 die Variablen i, a und as enthalten darf. insert :: (Ord a) => a -> OrdList a -> OrdList a insert a [] = e1 insert a (a’ : as) = e2 where s = insert e3 as 14 Programmieren in Haskell Insert insert a [] insert a (a’ : as) | a <= a’ | otherwise where s = [a] = e21 = e22 = insert e3 as insert :: (Ord a) => a -> [a] -> [a] insert a [] = [a] insert a (a’:as) | a <= a’ = a:a’:as | otherwise = a’:insert a as 15 Programmieren in Haskell Strukturelle Rekursion auf Bäumen data Tree a = Nil | Leaf a | Br (Tree a) (Tree a) deriving Show Rekursionsbasis (Nil) Das Problem wird für den leeren Baum gelöst. Rekursionsbasis (Leaf a) Das Problem wird für das Blatt Leaf a gelöst. Rekursionsschritt (Br l r) Um das Problem für den Baum Br l r zu lösen, werden rekursiv Lösungen für l und r bestimmt, die zu einer Lösung für Br l r erweitert werden. 16 Programmieren in Haskell Strukturelle Rekursion auf Bäumen Schema der strukturellen Rekursion auf Bäumen: f :: Tree σ -> τ f Nil = e1 f (Leaf a) = e2 f (Br l r) = e3 where sl = f l sr = f r e3 darf dabei l, r, sl und sr enthalten, nicht aber f . 17 Programmieren in Haskell Funktionen auf Bäumen size berechnet die Anzahl der Blätter eines Baumes: size size size size :: Tree a -> Integer Nil = 1 (Leaf _) = 1 (Br l r) = size l + size r depth berechnet die Tiefe des Baumes, d.h. die Länge des längsten Pfades von der Wurzel bis zu einem Blatt: depth depth depth depth 18 :: Tree a -> Integer Nil = 0 (Leaf _) = 0 (Br l r) = max (depth l) (depth r) + 1 Programmieren in Haskell Das allgemeine Rekursionsschema data T a1 . . . am = C1 t11 . . . t1n1 | ... | Cr tr 1 . . . trnr Wir unterscheiden zwei Arten von Argumenten: rekursive (d.h. tij ist gleich T a1 . . . am ) und nicht-rekursive. Seien li1 ,. . . , lipi mit 1 6 li1 < li2 < · · · < lipi 6 ni die Positionen, an denen der Konstruktor Ci rekursiv ist 19 Programmieren in Haskell Strukturelle Rekursion Allgemeines Schema der strukturellen Rekursion: f :: T σ1 . . . σm -> τ f (C1 x11 . . . x1n1 ) = e1 where s11 = f x1l11 ... s1p1 = f x1l1p1 ... f (Cr xr 1 . . . xrnr ) = er where sr 1 = f xrlr 1 ... srpr = f xrlrpr Der Ausdruck ei darf die Variablen xi1 , . . . , xini und die Variablen si1 , . . . , sipi enthalten. Ist pi = 0, so spricht man von einer Rekursionsbasis, sonst von einem Rekursionsschritt. 20 Programmieren in Haskell Verstärkung der Rekursion Wir wollen eine Funktion entwickeln, die eine Liste von Elementen umkehrt. Wenn wir das Schema der strukturellen Rekursion anwenden, ergibt sich folgende Implementierung: reverse’’ :: [a] -> [a] -- vordefiniert reverse’’ [] = [] reverse’’ (a:as) = reverse’’ as ++ [a] Problem: In jedem Rekursionsschritt wird von der Funktion ++ die komplette bereits umgedrehte Liste durchlaufen. Die Laufzeit von reverse’’ ist somit in O(n2 ). 21 Programmieren in Haskell Verstärkung der Rekursion Wir wollen eine bessere Implementierung von reverse systematisch herleiten. Idee: Wir programmieren eine Funktion, die ein schwierigeres Problem löst als verlangt, aber die es uns erlaubt, den Rekursionsschritt besser zu bewältigen. Diese Technik nennt man Verstärkung der ” Rekursion” oder Programmieren durch Einbettung”. ” Für reverse bedeutet dies: Im Rekursionsschritt müssen wir die Restliste umdrehen und an das Ergebnis eine Liste anhängen. Idee: Wir entwickeln eine Funktion, die beide Aufgaben gleichzeitig löst: Spezifikation: reel :: [a] -> [a] -> [a] reel x y = reverse x ++ y Aus dieser Spezifikation können wir die Definition von reel systematisch ableiten. 22 Programmieren in Haskell Verstärkung der Rekursion Rekursionsbasis (x = []): reel [] y = reverse [] ++ y (Spezifikation) = [] ++ y (Def. reverse) = y (Def. (++)) 23 Programmieren in Haskell Verstärkung der Rekursion Rekursionsschritt (x = a:as): = = = = = 24 reel (a:as) y reverse (a:as) ++ y (reverse as ++ [a]) ++ y reverse as ++ ([a] ++ y) reverse as ++ (a:y) reel as (a:y) (Spezifikation) (Def. reverse) (Ass. (++)) (Def. (++)) (Spezifikation) Programmieren in Haskell Verstärkung der Rekursion reel :: [a] -> [a] -> [a] reel [] y = y reel (a:as) y = reel as (a:y) reverse xs = reverse xs ++ [] (Def. (++)) = (reel xs []) (Spezifikation) reverse’’’ :: [a] -> [a] reverse’’’ as = reel as [] 25 Programmieren in Haskell Verstärkung der Rekursion reel :: [a] -> [a] -> [a] reel [] y = y reel (a:as) y = reel as (a:y) reverse xs = reverse xs ++ [] (Def. (++)) = (reel xs []) (Spezifikation) reverse’’’ :: [a] -> [a] reverse’’’ as = reel as [] 25 Programmieren in Haskell Verstärkung der Rekursion reel :: [a] -> [a] -> [a] reel [] y = y reel (a:as) y = reel as (a:y) reverse xs = reverse xs ++ [] (Def. (++)) = (reel xs []) (Spezifikation) reverse’’’ :: [a] -> [a] reverse’’’ as = reel as [] 25 Programmieren in Haskell