8 Listenverarbeitung 8 · Listenverarbeitung I Funktionen, die Listen als Argumente besitzen, orientieren sich oft an der rekursiven Struktur von Listen (cf. Seite 146): Rekursionsabbruch Das Argument ist die leere Liste [] :: [α], oder Rekursion das Argument ist von der Form x:xs mit x :: α und xs :: [α]. I Die Definition einer listenverarbeitenden Funktion f :: [α] → β nutzt daher oft eine entsprechende Fallunterscheidung f [] = z f (x : xs) = c (h x) (t xs) wobei • z :: β das Ergebnis bei Rekursionsabbruch darstellt, und • im Rekursionsfall I h :: α → γ auf den Kopf (head), und I t :: [α] → δ auf den Rest (tail) des Arguments angewandt werden, I während c :: γ → δ → β das Ergebnis beider Aufrufe kombiniert. Dabei ruft t typischerweise f selbst rekursiv auf. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 208 8 · Listenverarbeitung Beispiel Die Summe über eine Liste 1 2 3 sum :: [Integer] -> Integer sum [] = 0 sum (x:xs) = x + sum xs 1 2 > sum [1..10] 55 I Dabei sind z = 0, h = id, t = sum und c = (+). I Die Identitätsfunktion id :: α → α ist Teil der standard prelude und hat die Definition id = \x -> x, also id = λx. x Beispiel map f wendet die Funktion f auf jedes Element einer Liste an. 1 2 3 map :: (α -> β) -> [α] -> [β] map f [] = [] map f (x:xs) = f x : map f xs 1 2 > map (+1) [1..10] [2, 3, 4, 5, 6, 7, 8, 9, 10, 11] I Dabei sind z = [], h = f, t = map f und c = (:). I Man sagt auch: Eine Funktion über eine Liste mappen. Frage Welche Funktion f erhält man mittels z = False, h = (e ==), t = f e und c = ||? Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 209 8 · Listenverarbeitung Haskells Standard Prelude definiert nützliche Funktionen über Listen: 1 1 2 head (x:_) = x 2 3 3 4 tail (_:xs) = xs init [x] init (x:xs) = [] = x : init xs 4 5 5 6 7 9 7 13 last [x] last (_:xs) = x = last xs length [] length (_:xs) = 0 = 1 + length xs 9 10 11 14 15 16 19 20 23 [] — analog: drop [] x : take (n-1) xs 18 = [] = reverse xs ++ [x] concat [] = [] concat (xs:xss) = xs ++ concat xss 19 20 21 reverse [] reverse (x:xs) [] ++ ys = ys (x:xs) ++ ys = x : xs ++ ys 16 17 take n _ | n <= 0 = take _ [] = take n (x:xs) = filter _ [] = [] filter p (x:xs) | p x = x : filter p xs | otherwise = filter p xs 13 15 21 22 12 14 (x:_) !! 0 = x (_:xs) !! n | n>0 = xs !! (n-1) 17 18 zip = zipWith (,) 8 11 12 _ = [] [] = [] (y:ys) x y : zipWith f xs ys 6 8 10 zipWith _ [] zipWith _ _ zipWith f (x:xs) = f 22 23 dropWhile _ [] = [] — analog: takeWhile dropWhile p xs@(x:xs') | p x = dropWhile p xs' | otherwise = xs Typen? Funktionsweise? Welche Funktionen sind nur partiell definiert (Fehler bei falschen Arg.)? Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 210 8 · Listenverarbeitung Haskells Standard Prelude definiert nützliche Funktionen über Listen: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 head :: [a] -> a head (x:_) = x tail :: [a] -> [a] tail (_:xs) = xs init :: [a] -> [a] init [x] = [] init (x:xs) = x : init xs last :: [a] -> a last [x] = x last (_:xs) = last xs length :: [a] -> Int length [] = 0 length (_:xs) = 1 + length xs (!!) :: [a] -> Int -> a (x:_) !! 0 = x (_:xs) !! n | n>0 = xs !! (n-1) take :: Int -> [a] -> [a] take n _ | n <= 0 = [] — analog: drop take _ [] = [] take n (x:xs) = x : take (n-1) xs reverse :: [a] -> [a] reverse [] = [] reverse (x:xs) = reverse xs ++ [x] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 zipWith zipWith zipWith zipWith :: (a->b->c) -> [a]->[b]->[c] _ [] _ = [] _ _ [] = [] f (x:xs) (y:ys) = f x y : zipWith f xs ys zip :: [a] -> [b] -> [(a, b)] zip = zipWith (,) filter :: (a -> Bool) -> [a] -> [a] filter _ [] = [] filter p (x:xs) | p x = x : filter p xs | otherwise = filter p xs (++) :: [a] -> [a] -> [a] [] ++ ys = ys (x:xs) ++ ys = x : xs ++ ys concat :: [[a]] -> [a] concat [] = [] concat (xs:xss) = xs ++ concat xss dropWhile :: (a -> Bool) -> [a] -> [a] dropWhile _ [] = [] — analog: takeWhile dropWhile p xs@(x:xs') | p x = dropWhile p xs' | otherwise = xs Die tatsächliche Implementierung weicht teilweise ab. Partiell: head, tail, init, last, (!!) Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 211 8 · Listenverarbeitung Erinnerung: Operatoren Auf den letzten Folien wurden zwei Operatoren definiert. I (!!) :: [a] -> Int -> a 1 2 Positionaler Zugriff auf Listenelemente (x:_) !! 0 = x (_:xs) !! n | n>0 = xs !! (n-1) (++) :: [a] -> [a] -> [a] Konkatenation zweier Listen I 1 2 [] ++ ys = ys (x:xs) ++ ys = x : xs ++ ys Operatoren unterscheiden sich von “normalen” (Präfix)-Funktionen nur syntaktisch, ansonsten verhalten sich beide in allen Kontexten gleich (cf. Seite 138). Man könnte sogar Präfix- und Infix-Schreibweise in der gleichen Definition mischen – das hilft aber nicht der Lesbarkeit: 1 2 3 (#@!) :: Integer -> [a] -> [a] 0 #@! _ = [] (#@!) n xs = xs ++ ((n-1) #@! reverse xs) Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 212 8 · Listenverarbeitung Was ist Foldable? Sollten das nicht Listen sein? Moderne Versionen des GHCi zeigen für manche Funktionen überraschend einen Typ mit Foldable statt mit Listen an: I 1 2 Prelude> :t length length :: Foldable t => t a -> Int I Das verstehen wir leider erst später, cf. Seite 307. I Vorerst stellen wir uns vor es ginge um Listen29 , d.h., wir lesen Foldable t ⇒ ... t α ... einfach als ... [α] ... ersetzen also jedes t α durch [α]. Beispiele Wir verwenden zunächst nur: null :: [α] → Bool length :: [α] → Int concat :: [[α]] → [α] 29 Tatsächlich statt statt statt null :: Foldable t ⇒ t α → Bool length :: Foldable t ⇒ t α → Int concat :: Foldable t ⇒ t [α] → [α] geht es um allgemeinere Datenstrukturen, die wir aber wohl nicht besprechen werden. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 213 8 · Listenverarbeitung 8.1 foldr und foldl · 8.1 foldr und foldl Das besprochene Rekursionsschema ist in der Standard Prelude mit zwei Funktionen, foldr (fold right) und foldl (fold left), implementiert. foldr (auch bekannt unter dem Namen reduce) I “reduziert” eine Liste vom Typ [α] zu einem Wert des Typs β. I Informell gilt: (dabei ist ein Infix-Operator des Typs α → β → β) foldr () z [x1 , x2 , ..., xn ] ≡ x1 (x2 (· · · (xn z) · · · )) Damit ist foldr :: (α → β → β) → β → [α] → β. I Eselsbrücken: Die Klammerung ist rechts-assoziativ, und das z erscheint ganz rechts. Ein mögliche Definition von foldr ist: 1 2 3 foldr :: (α -> β -> β) -> β -> [α] -> β foldr () z [] = z foldr () z (x:xs) = x foldr () z xs Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 214 8 · Listenverarbeitung foldr und foldl · 8.1 Alternativ lässt sich der Effekt von foldr damit auch wie folgt illustrieren: foldr () z (x1 : (x2 : (...( xn : [] )...))) ↓ ↓ ↓ ↓ ↓ ↓ ↓ = (x1 (x2 (...( xn z )...))) I Die Funktion foldr ersetzt also alle Listen-Konstruktoren, und zwar • : durch , und • [] durch z. Beispiel Viele Standardfunktionen implementieren: sum = product = concat = and = or = Michael Grossniklaus · DBIS lassen sich einfach mittels foldr foldr (+) 0 foldr ? ? foldr ? ? foldr ? ? foldr ? ? Informatik 2 · Sommer 2017 215 8 · Listenverarbeitung Lösung: I foldr und foldl · 8.1 = = = = = sum product concat and or foldr (+) 0 foldr (*) 1 foldr (++) [] foldr (&&) True foldr (||) False Mit der Behauptung sum so definieren zu können, behaupte ich eine Äquivalenz von sum von Folie 209 und foldr (+) 0: sum ≡ foldr (+) 0 (Die entsprechenden Beweise ggf. später in den Übungen.) ~ Obacht Wir hatten vereinbart (cf. Seite 116), dass zwei λ-Ausdrücke e1 , e2 äquivalent sind, gdw. sie zur gleichen Normalform m reduziert werden können, d.h.: e1 ≡ e2 ⇔ ∃m. e1 _∗ m ∧ e2 _∗ m Frage Was ist z.B. mit sum ≡ foldr (+) 0 — gibt es so ein m? Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 216 8 · Listenverarbeitung foldr und foldl · 8.1 Gleichheit von Funktionen Das Extensionalitätsprinzip Antwort Leider nicht. Ohne Argumente können wir nicht weit reduzieren. I I Die Aussage dass sich zwei Funktionen gleich verhalten macht aber natürlich trotzdem Sinn. Deswegen erweitern wir unsere Definition von Äquivalenz etwas: Definition Äquivalenz von Funktionen Zwei Funktionen f , g heißen äquivalent gdw. sie für das gleiche Argument äquivalente Ergebnisse liefern, d.h.: f ≡g ⇔ ∀x. f x ≡ g x Für die Äquivalenz auf der rechten Seite bemühen wir diese Definition erneut, oder verwenden die bekannte Äquivalenz von Seite 116. I Dieses Prinzip ist in der Mengenlehre als Extensionalitätsprinzip bekannt. Man sagt auch: f und g sind extensional gleich. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 217 8 · Listenverarbeitung foldr und foldl · 8.1 Monoid Nochmal: sum product concat and or ≡ ≡ ≡ ≡ ≡ foldr (+) 0 foldr (*) 1 foldr (++) [] foldr (&&) True foldr (||) False Beobachtung All diesen Beispielen ist gemeinsam, dass assoziativ ist und sich z bzgl. dieser Operation neutral verhält. Definition Monoid Eine Menge M mit einer binären Verknüpfung ⊕ :: M × M → M heißt Monoid, genau dann, wenn (Dabei sei M das Universum der Quantoren) I die Operation ⊕ assoziativ ist, I und es ein Neutralelement e gibt. ∀a b c. a ⊕ (b ⊕ c) ≡ (a ⊕ b) ⊕ c ∃e. ∀a. e ⊕ a ≡ a ≡ a ⊕ e Übrigens: Falls :: α → β → β assoziativ ist, gilt bereits α = β. Warum? Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 218 8 · Listenverarbeitung I foldr und foldl · 8.1 Natürlich kann foldr auch auf Strukturen angewendet werden, die keinen Monoid bilden: Beispiel 1 2 3 4 filter p length reverse takeWhile p = = = = 5 6 7 foldr (\x xs -> if p x then x:xs else xs) [] foldr (\_ n -> 1+n) 0 foldr (\x xs -> xs ++ [x]) [] foldr (#) [] where x # xs | p x = x:xs | otherwise = [] Damit könnte takeWhile (<3) [1..4] etwa wie folgt reduziert werden: 1 takeWhile (<3) [1..4] 2 3 4 5 6 Michael Grossniklaus · DBIS _ foldr (#) [] (1 : (2 : (3 : _ 1 # (2 # (3 # _ 1 : (2 # (3 # _ 1 : (2 : (3 # _ 1 : (2 : []) _ [1,2] Informatik 2 · Sommer 2017 (4 (4 (4 (4 : # # # [])))) []))) []))) []))) 219 8 · Listenverarbeitung foldr und foldl · 8.1 Linksassoziative Variante von foldr Analog zu foldr gibt es die vordefinierte Funktion foldl. foldl (also fold left) I klammert die Listenelemente während der Reduktion nach links. I Informell gilt: (dabei ist ein Infix-Operator des Typs β → α → β) foldl () z [x1 , x2 , ..., xn ] ≡ (· · · ((z x1 ) x2 ) · · · ) xn Damit ist foldl :: (β → α → β) → β → [α] → β I Eselsbrücken: Die Klammerung ist links-assoziativ, und das z erscheint ganz links. Ein mögliche Definition von foldl ist: 1 2 3 foldl :: (β -> α -> β) -> β -> [α] -> β foldl () z [] = z foldl () z (x:xs) = foldl () (z x) xs Hier übernimmt z die Rolle eines akkumulierenden Parameters, in dem das Endergebnis aufgesammelt wird. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 220 8 · Listenverarbeitung foldr und foldl · 8.1 Beispiel Eine praktische Anwendung von foldl ist die Funktion pack, die eine Liste von Ziffern [xn−1 , xn−2 , ..., x0 ] mit xi ∈ {0...9} in den durch sie “dargestellten” Wert transformiert: n−1 X xk · 10k k=0 1 2 3 pack :: [Integer] -> Integer pack xs = foldl (#) 0 xs where n # x = 10 * n + x Eine Beispiel-Reduktion sähe folgendermaßen aus: 1 pack [1, 9, 8, 4] 2 3 4 5 6 Michael Grossniklaus · DBIS _ foldl (#) 0 (1 : (9 : (8 : (4 _ (((0 # 1) # 9) _ ((1 # 9) _ (19 _ _ Informatik 2 · Sommer 2017 : [])))) # 8) # 4 # 8) # 4 # 8) # 4 198 # 4 1984 221 8 · Listenverarbeitung foldr und foldl · 8.1 Das 1. Dualitätstheorem I Auch hier gilt: Falls :: β → α → β assoziativ ist, dann ist α = β. I Dann haben foldl und foldr den gleichen Typ: foldr, foldl :: (α → α → α) → α → [α] → α Es gilt sogar: Satz 1. Dualitätstheorem Falls (⊕, α) einen Monoid mit Neutralelement e bilden, dann gilt: foldr (⊕) e ≡ foldl (⊕) e Beweis Evtl. Übung, nach Kapitel über Induktion (cf. Seite 233). Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 222 8 · Listenverarbeitung foldr und foldl · 8.1 Unmittelbare Konsequenz: I Die Funktionen von Seite 217 können wir auch mit foldl bauen. sum product concat and or I ≡ ≡ ≡ ≡ ≡ foldl (+) 0 foldl (*) 1 foldl (++) [] foldl (&&) True foldl (||) False Es bleibt zu klären, welche Variante effizienter ist. Das machen wir später genauer. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 223 8 · Listenverarbeitung foldr und foldl · 8.1 Das 2. Dualitätstheorem Auch ohne einen Monoid sind foldr und foldl eng verwandt: Satz 2. Dualitätstheorem Falls für alle x, y , z geeigneten Typs gilt x (y z) x y ≡ (x y ) z und ≡ y x dann gilt foldr () ≡ foldl () Beweis Übung, nach Kapitel über Induktion (cf. Seite 233). Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 224 8 · Listenverarbeitung foldr und foldl · 8.1 Beispiel length = foldr (λ n. 1 + n) 0 length0 = foldl (λn . 1 + n) 0 I Diese beiden Funktionen sind äquivalent. I Die Variante mit foldl kann effizienter ausgewertet werden. (Dazu müssen wir aber noch mehr über Auswertestrategien wissen, cf. später.) Beweis mit dem 2. Dualitätstheorem. Zu zeigen: foldr (λ n. 1 + n) 0 | {z } ≡ I Zeigen x z ≡ z x: ≡ ≡ x z 1+z z x Michael Grossniklaus · DBIS I foldl (λn . 1 + n) 0 | {z } Zeigen x (y z) ≡ (x y ) z: ≡ ≡ x (y z) 1 + (y z) 1 + (1 + y ) Informatik 2 · Sommer 2017 ≡ ≡ (x y ) z 1 + (x y ) 1 + (1 + y ) 225 8 · Listenverarbeitung foldr und foldl · 8.1 Das 3. Dualitätstheorem Schließlich gilt noch: Satz 3. Dualitätstheorem Sei reverse wie auf Seite 210 definiert, und sei flip f x y = f y x. Dann gilt für alle , z und xs geeigneten Typs: foldr () z xs ≡ foldl (flip ()) z (reverse xs) Mit anderen Worten: Mit x y ≡ y x gilt: foldr () z xs ≡ foldl () z (reverse xs) Beweis Kann man als (aufwändige) Übung machen, nach Kapitel über Induktion (cf. Seite 233). Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 226 8 · Listenverarbeitung foldr und foldl · 8.1 Unendliche Listen ~ Vorsicht Eine Bemerkung zu foldl/foldr auf unendlichen Listen: foldl () z (x:xs) = foldl () (z x) xs foldr () z (x:xs) = x foldr () z xs I Für die nicht-leere Liste ruft sich foldl sofort rekursiv selbst auf, und das Ergebnis des rekursiven Aufrufs ist das Ergebnis der Funktion. I Bei foldr hängt das Ergebnis hingegen von ab. Der rekursive Aufruf von foldr ist nur Argument von . ⇒ Konsequenz: • foldl terminiert sicher nicht auf unendlichen Listen! • Bei foldr entscheidet der Operator ob Rekursion stattfindet, und kann vor Ende der Liste abbrechen. (cf. Seite 219, takeWhile) Beispiel head ≡ foldr const ⊥ funktioniert auf unendlichen Listen (mit const = \x y -> x aus der Prelude und ⊥ ∼ = undefined) Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 227 8 · Listenverarbeitung foldr und foldl · 8.1 Anmerkungen I Die Ersetzung der Konstruktoren eines Datentyps (hier für Listen, also cons (:) und nil []) durch Operatoren bzw. Werte ist ein Prinzip, das sich auch für andere konstruierte Datentypen sinnvoll anwenden lässt. I Im Gegensatz zu foldr ersetzt foldl nicht nur die Konstruktoren, sondern ändert die Klammerung der rechtstief konstruierten Liste. Insofern nimmt es eine Sonderrolle ein. Beide Punkte werden wir später wieder aufgreifen. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 228 8 · Listenverarbeitung 8.2 Effizienz · 8.2 Effizienz Die Anzahl der Reduktionen bei der Reduktion eines Ausdrucks auf seine Normalform ist in FPLs ein naheliegendes Maß für die Komplexität einer Berechnung. Beispiel Die Länge der ersten Argumentliste bestimmt die Anzahl der Reduktionen der Listen-Konkatenation ++. [3,2] ++ [1] I ++.2 steht dabei für die 2. Zeile der Definition von ++, cf. Seite 210. _ I Für die Auswertung von xs ++ ys mit length xs _ n werden n Reduktionen via ++.2, gefolgt von einer Reduktion via ++.1 benötigt. _ I Die letzte Zeile ist lediglich eine Änderung der Schreibweise. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 _ ≡ ++.2 3:([2] ++ [1]) ++.2 3:(2:([] ++ [1])) ++.1 3:(2:[1]) [3,2,1] 229 8 · Listenverarbeitung Effizienz · 8.2 Beispiel Ähnliche Überlegungen gelten für reverse, das in seiner Definition ++ nutzt: reverse [1,2,3] _ _ _ _ _ _ _ Gilt length xs _ n, dann benötigt reverse xs reverse.2 reverse [2,3] ++ [1] reverse.2 (reverse [3] ++ [2]) ++ [1] I n Reduktionen via reverse.2, I gefolgt von einer Reduktion via reverse.1, I gefolgt von reverse.2 ((reverse [] ++ [3]) ++ [2]) ++ [1] reverse.1 1 + 2 + ... + n = (([] ++ [3]) ++ [2]) ++ [1] ++.1 ([3] ++ [2]) ++ [1] ++.2, ++.1 [3,2] ++ [1] ++.2, ++.2, ++.1 n · (n + 1) 2 Reduktionen, um mittels ++ die Konkatenationen auszuführen. Damit ist die Anzahl der Reduktionen in O(n2 ). [3,2,1] Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 230 8 · Listenverarbeitung Effizienz · 8.2 Listeninvertierung in linearer Zeit Eine Liste lässt sich aber durchaus in linearer Zeit (proportional zur Listenlänge n) reversieren: 1 2 3 4 5 rev :: [α] -> [α] rev xs = shunt [] xs where shunt ys [] = ys shunt ys (x:xs) = shunt (x:ys) xs I I Tatsächlich reversiert shunt ys xs nicht nur die Liste xs, sondern konkateniert diese zusätzlich auch noch mit ys. Das erste Argument ys von shunt wird akkumulierender Parameter genannt: • Zwischenergebnisse der Berechnung werden an die nächste Rekursion weitergegeben. • Am Ende der Rekursion enthält ys das Ergebnis (oder einen Teil davon). Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 231 8 · Listenverarbeitung Effizienz · 8.2 Beispiel rev xs benötigt lineare Anzahl (proportional zu (length xs)) Reduktionen rev [1,2,3] _ _ _ _ _ rev.1 shunt [] [1,2,3] shunt.2 shunt [1] [2,3] shunt.2 shunt [2,1] [3] shunt.2 shunt [3,2,1] [] shunt.1 [3,2,1] Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 232 8 · Listenverarbeitung 8.3 I Induktion über Listen · 8.3 Induktion über Listen Dank referenzieller Transparenz kann man Behauptungen wie rev ≡ reverse relativ einfach beweisen. I Beweise über listenverarbeitende Funktionen können häufig mittels Induktion über Listen, analog zu Induktionsbeweisen für Behauptungen über Elemente aus N, geführt werden: 1. Induktionsanfang (aka. Induktionsverankerung). I Beweise die Aussage mit der leeren Liste []. 2. Induktionsschritt von xs zu x : xs. I Übung Dabei wird die Induktionshypothese (die Aussage mit einer beliebigen, festen Liste xs) verwendet, um die Aussage mit x : xs für alle x zu zeigen. Für alle Listen xs gilt xs ++ [ ] ≡ xs. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 233 8 · Listenverarbeitung Induktion über Listen · 8.3 Beispiel Beweisen rev xs ≡ reverse xs für alle Listen xs. I Da rev xs _ shunt [ ] xs (cf. Seite 231), genügt es, die allgemeinere Behauptung zu zeigen: shunt ys xs ≡ reverse xs ++ ys für alle xs, ys :: [α] Induktionsverankerung Sei xs = [ ] und ys :: [α] beliebig. shunt ys [ ] ≡ ys ≡ [ ] ++ ys ≡ reverse [ ] ++ ys Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 (shunt.1) (++.1) (reverse.1) 235 8 · Listenverarbeitung Induktion über Listen · 8.3 Induktionshypothese Für ein beliebiges, festes xs :: [α] und für alle ys :: [α] gelte: shunt ys xs ≡ reverse xs ++ ys Induktionsschritt Von xs zu x : xs. Seien x :: α und ys :: [α]. Mit dem xs aus der Induktionshypothese gilt: shunt ys (x : xs) ≡ ≡ ≡ ≡ ≡ ≡ shunt (x : ys) xs reverse xs ++ (x : ys) reverse xs ++ (x : ([ ] ++ ys)) reverse xs ++ ([x] ++ ys) (reverse xs ++ [x]) ++ ys reverse (x : xs) ++ ys (shunt.2) (Hypothese) (++.1 rückw.) (++.2 rückw.) (++ assoziativ) (reverse.2) Übung Die hier verwendete Annahme über die Assoziativität von ++ müssen wir ebenfalls noch beweisen: xs ++ (ys ++ zs) ≡ (xs ++ ys) ++ zs. Auch hier führt Listeninduktion über den Parameter zum Ziel, über den die Rekursion der betrachteten Funktion ++ formuliert ist, also xs. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 236 8 · Listenverarbeitung 8.4 Programm-Synthese · 8.4 Programm-Synthese Bei der Beweisführung über Programme werden Eigenschaften eines gegebenen Programms Schritt für Schritt nachvollzogen und dadurch bewiesen (s. rev und reverse). Programm-Synthese kehrt dieses Prinzip um: I Gegeben ist eine formale Spezifikation eines Problems, I gesucht ist ein problemlösendes Programm, das durch schrittweise Umformung der Spezifikation gewonnen (synthetisiert) wird. Wenn die Transformationen diszipliniert vorgenommen werden, kann die Synthese als Beweis dafür gelesen werden, dass das Programm die Spezifikation erfüllt (der Traum aller Software-Ingenieure). Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 237 8 · Listenverarbeitung Programm-Synthese · 8.4 Beispiel Die Funktion init der standard prelude bestimmt das initiale Segment ihres Listenargumentes, also etwa init [1..10] _ [1, 2, 3, 4, 5, 6, 7, 8, 9]. I Damit wäre eine naheliegende Spezifikation für init die folgende: init xs = take (length xs - 1) xs wobei xs endlich und nicht leer “Nimm alle Elemente von xs, aber nicht das letzte Element” I Die Synthese versucht eine effizientere Variante von init abzuleiten (die Spezifikation wäre ja prinzipiell schon ausführbar, traversiert xs zur Berechnung des Ergebnisses aber zweimal). Für die Synthese instantiieren wir xs 1. mit [x] und 2. mit x1 : x2 : xs. Jede nichtleere Liste besitzt die eine oder die andere Form. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 238 8 · Listenverarbeitung Programm-Synthese · 8.4 Fall 1 [x] init [x] = — Instanziierung Spezifikation take (length [x] − 1) [x] = length, Arithmetik take 0 [x] = take.1 [] Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 239 8 · Listenverarbeitung Programm-Synthese · 8.4 Fall 2 x1 : x2 : xs init (x1 : x2 : xs) = — Instanziierung Spezifikation take (length (x1 : x2 : xs) − 1) (x1 : x2 : xs) = length, Arithmetik take (length xs + 1) (x1 : x2 : xs) = take.3 x1 : take (length xs) (x2 : xs) = = length.2, Arithmetik x1 : take (length (x2 : xs) − 1) (x2 : xs) x1 : init (x2 : xs) Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 240 8 · Listenverarbeitung Programm-Synthese · 8.4 Zusammenfassen der beiden so erhaltenen Gleichungen ergibt 1 2 3 init :: [a] -> [a] init [x] = [] init (x1:x2:xs) = x1 : init (x2:xs) Weitere Verbesserungen 1 2 3 4 I Das wiederholte Zerlegen und Zusammensetzen der Liste x2 : xs kann man sich sparen. I Für die leere Liste geben wir einen brauchbaren Fehler aus. init init init init :: [a] -> [a] [x] = [] (x:xs) = x : init xs [] = error "init: empty list" Übung Versuchen Sie Ihren Haskell-Code durch simple Äquivalenzumformungen zu verbessern. Manchmal gewinnt man dabei überraschende Einsichten. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 241 8 · Listenverarbeitung 8.5 List Comprehensions · 8.5 List Comprehensions List Comprehensions sind vor allem in modernen FPLs als eine alternative Notation für Operationen auf Listen verbreitet30 . I Die Notation mittels Set Comprehension31 ist aus der Mathematik (Mengenlehre) bekannt. Die Idee dabei: Term Prädikat+ • Beschreibt eine Menge, deren Elemente jeweils von Term beschrieben werden, • unter den durch die Prädikate bestimmten Bedingungen. I List Comprehensions erweitern die Ausdruckskraft der Sprache nicht, erlauben aber oft eine kompakte, leicht lesbare und elegante Notation von Listenoperationen. Wir werden eine Abbildung auf den Haskell-Kern besprechen. 30 Miranda™ (Dave Turner, 1976) sah als erste FPL List Comprehensions syntaktisch 31 aka. “set-builder notation”. Einen deutschen Begriff scheint’s nicht zu geben. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 vor. 242 8 · Listenverarbeitung List Comprehensions · 8.5 Beispiel Die Menge aller natürlichen geraden Zahlen kann durch eine set comprehension kompakt notiert werden: {n | n ∈ N, n mod 2 = 0} Eine entsprechende List Comprehension (die unendliche Liste aller geraden Zahlen) wird syntaktisch ganz ähnlich notiert: [ n | n <- [0 .. ], n `mod` 2 == 0 ] Beispiel Die Standardfunktionen map und filter sind mittels List Comprehensions ohne die sonst notwendige Rekursion formulierbar: 1 2 map :: (α -> β) -> [α] -> [β] map f xs = [ f x | x <- xs ] Michael Grossniklaus · DBIS 1 2 filter :: (α -> Bool) -> [α] -> [α] filter p xs = [ x | x <- xs, p x ] Informatik 2 · Sommer 2017 243 8 · Listenverarbeitung List Comprehensions · 8.5 Syntax der List Comprehension Die allgemeine Form einer List Comprehension ist [ e | q1 , q2 , ..., qn ] wobei I der Kopf e ein beliebiger Ausdruck ist, und I die Qualifier qi (mit n ≥ 1), eine von drei Formen besitzen: Generator pi <- ei , wobei ei :: [αi ], und pi ein Pattern für Werte des Typs αi ist — schreiben salopp pi :: αi . Prädikat qi :: Bool. lokale Bindung let { pi1 = ei1 ; pi2 = ei2 ... }. Dabei sind die eij beliebige Ausdrücke, und die pij entsprechende Patterns. Beispiel von vorhin: [ n | n <- [0 .. ], n `mod` 2 == 0 ] . Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 244 8 · Listenverarbeitung List Comprehensions · 8.5 ListComp → [ Expr | Qual (, Qual)∗ ] Qual → | | Pattern <- Expr — Pattern :: α, Expr :: [α] Expr — Expr :: Bool — cf. Seite 172 let { Pattern = Expr (; Pattern = Expr)∗ } — Zusammenfassung Semantik der List Comprehension — in Worten I Ein Generator qi = pi <- ei versucht das Pattern pi der Reihe nach gegen die Elemente der Liste ei zu matchen. • Für jeden erfolgreichen Match werden die nachfolgenden Qualifier qi+1 , ..., qn ausgewertet. • Die durch den Match gebundenen Variablen des Patterns pi sind in den nachfolgenden Qualifiern sichtbar und an entsprechende Werte gebunden. I Eine lokale Bindung (let) kann ebenfalls Variablen an Werte binden. I Jede Bindung wird solange nach rechts propagiert, bis ein Prädikat unter ihr zu False evaluiert wird. I Der Kopf e wird für alle Bindungen ausgewertet, die alle Prädikate passieren konnten. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 245 8 · Listenverarbeitung List Comprehensions · 8.5 Entsprechend dieser Semantik wird also in [ e | p1 <- e1 , p2 <- e2 ] I zuerst über die Domain e1 des Generators p1 <- e1 iteriert, und dann I für jeden Match von p1 über die Domain e2 des Generators p2 <- e2 . Dies trifft die Intuition der aus der Mengenlehre bekannten Set Comprehension: [ (x,y) | x <- [x1 , x2 ], y <- [y1 , y2 ] ] _ Michael Grossniklaus · DBIS [(x1 ,y1 ), (x1 ,y2 ), (x2 ,y1 ), (x2 ,y2 )] Informatik 2 · Sommer 2017 246 8 · Listenverarbeitung List Comprehensions · 8.5 Beispiel Elegante (aber ineffiziente) Variante von Quicksort als 2-Zeiler 1 2 3 4 5 qsort :: (α -> α -> Bool) -> [α] -> [α] qsort _ [] = [] qsort (<?) (x:xs) = qsort (<?) [ y | y <- xs, y <? x ] ++ [x] ++ qsort (<?) [ y | y <- xs, not (y <? x) ] Beachte: In der split-Phase dieser Implementation wird die Liste xs jeweils (unnötigerweise) zweimal durchlaufen. Beispiel Matrix über Typ α als Liste von Zeilenvektoren (wiederum Listen). Bestimme ersten Spaltenvektor: 1 2 firstcol :: [[α]] -> [α] firstcol m = [ e | (e:_) <- m ] firstcol nutzt die Möglichkeit, in Generatoren Patterns zu spezifizieren. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 247 8 · Listenverarbeitung List Comprehensions · 8.5 Beispiel Alle Permutationen einer Liste xs 1. Die leere Liste [ ] hat sich selbst als einzige Permutation. 2. Wenn xs nicht leer ist, wähle ein Element a aus xs und stelle a den Permutationen der Liste xs ohne a voran. 3. Führe 2. für jedes Element der Liste xs aus. 1 2 3 perms :: [Integer] -> [[Integer]] perms [] = [[]] perms xs = [ a:p | a <- xs, p <- perms $ xs \\ [a] ] 4 5 6 > perms [2, 3] [[2, 3], [3, 2]] I I Dabei entfernt die Listendifferenz xs \\ ys alle Elemente von ys aus der Liste xs, etwa: [1, 2, 1, 2, 3] \\ [2, 5] _ [1, 1, 3]. Wie könnte man \\ implementieren? Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 248 8 · Listenverarbeitung List Comprehensions · 8.5 Beispiel Berechne alle Pythagoräischen Dreiecke, in denen keine Seite länger als n ist. 1 pyth n = [ (a,b,c) | c <- [1..n], a <- [1..c], b <- [1..a], a^2 + b^2 == c^2 ] 2 3 4 > pyth 20 [(4,3,5),(8,6,10),(12,5,13),(12,9,15),(15,8,17),(16,12,20)] Beispiel Was berechnet die folgende Funktion bar? Wie lautet ihr Typ? 1 bar xs = [ x | [x] <- xs ] Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 249 8 · Listenverarbeitung Beispiel 1 2 List Comprehensions · 8.5 “Join” zwischen zwei Listen bzgl. eines Prädikates p join :: (α -> β -> γ) -> (α -> β -> Bool) -> [α] -> [β] -> [γ] join f p xs ys = [ f x y | x <- xs, y <- ys, p x y ] Den “klassischen relationalen Join” R1 1fst=fst R2 auf binären Relationen Ri , erhält man dann durch 1 2 3 4 5 foo = join (\x y -> (fst x, snd x, snd y)) (\x y -> fst x == fst y) [(1, "John"), (2, "Jack"), (3, "Bonnie")] [(2, "Ripper"), (1, "Doe"), (3, "Parker"), (2, "Dalton"), (1, "Cleese")] 6 7 8 9 Prelude> foo [ (1, "John", "Doe"), (1, "John", "Cleese"), (2, "Jack", "Ripper") , (2, "Jack", "Dalton"), (3, "Bonnie", "Parker")] Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 250 8 · Listenverarbeitung List Comprehensions · 8.5 Operationale Semantik für List Comprehensions Die Semantik der List Comprehensions, welche wir vorhin (cf. Seite 245) eher durch “hand-waving” erklärt haben, lässt sich – ganz ähnlich wie bei der β-Reduktion des λ-Kalküls – durch Reduktionsregeln formal erklären. Definition Semantik der List Comprehension, ohne Pattern Matching Sei e ein Haskell-Ausdruck, v Variable, qs eine Sequenz32 von Qualifiern. Die folgenden Regeln reduzieren jeweils den ersten Qualifier: ○ 1 [ e | v <- [], qs ] _ [] [ e | v <- (x : xs), qs ] _ [ e | qs ][v x] 2 ++ [ e | v <- xs, qs ] ○ [ e | False, qs ] _ [] [ e | True, qs ] _ [ e | qs ] [ e | ] _ [ e ] 32 qs ○ 3 ○ 4 ○ 5 ist keine Haskell-Liste, sondern ein Konstrukt der Meta-Ebene, cf. Seite 245. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 251 8 · Listenverarbeitung List Comprehensions · 8.5 I Die ersten beiden Reduktionsregeln reduzieren einen Generator über einer 1 bzw. nichtleeren ○ 2 Liste. leeren ○ I 3 und ○ 4 testen Prädikate. Regeln ○ I 5 ist anwendbar, sobald die Sequenz der Qualifier vollständig Regel ○ reduziert wurde. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 252 8 · Listenverarbeitung List Comprehensions · 8.5 Beispiel Reduktion von [ x^2 | x <- [1,2,3], odd x ] [ x^2 | x <- [1,2,3], odd x ] _ = _ _ _ _ 2 verwenden ○ [ x^2 | odd x ][x 1] ++ [ x^2 | x <- [2,3], odd x ] | {z } A [ 1^2 | odd 1 ] ++ A [ 1^2 | True ] ++ A 4 verwenden ○ [ 1^2 | ] ++ A 5 verwenden ○ [1^2] ++ A A z }| { 1 : [ x^2 | x <- [2,3], odd x ] Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 253 8 · Listenverarbeitung List Comprehensions · 8.5 1 : [ x^2 | x <- [2,3], odd x ] _ = _ _ _ _ _ = 2 verwenden ○ 1 : [ x^2 | odd x ][x 2] ++ [ x^2 | x <- [3], odd x ] | {z } 1 : [ 2^2 | odd 2 ] ++ B B 1 : [ 2^2 | False ] ++ B 3 verwenden ○ 1 : [] ++ B B z }| { 1 : [ x^2 | x <- [3], odd x ] 2 verwenden ○ 1 : [ x^2 | odd x ][x 3] ++ [ x^2 | x <- [], odd x ] 1 links wie gehabt (_ 9), und rechts verwenden wir ○ 1 : 9 : [] [1, 9] Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 254 8 · Listenverarbeitung List Comprehensions · 8.5 Abbildung von List Comprehensions auf den Haskell-Kern I Prinzipiell erlaubt das System der eben besprochenen Reduktionsregeln, List Comprehensions auf in Haskell vordefinierte Funktionen zurückzuführen. I Im Folgenden betrachten wir ein Übersetzungsschema J·K, das List Comprehensions aus Haskell-Code entfernt33 : J Code mit List Comprehension K = Code ohne List Comprehension I J·K kann vom Compiler auf Haskell-Quellcode angewandt werden, um äquivalenten Code ohne List-Comprehensions zu erhalten. 33 Hier verwenden wir semantische Klammern etwas anders als gewohnt. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 255 8 · Listenverarbeitung 1 2 List Comprehensions · 8.5 Dabei basiert das Schema J·K auf der Funktion concatMap: concatMap :: (α -> [β]) -> [α] -> [β] concatMap f = foldr (\x xs -> f x ++ xs) [] Frage Was tut diese Funktion? Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 256 8 · Listenverarbeitung 1 2 List Comprehensions · 8.5 concatMap :: (α -> [β]) -> [α] -> [β] concatMap f = foldr (\x xs -> f x ++ xs) [] 3 4 5 > concatMap (replicate 3) "hello" "hhheeellllllooo" Antwort concatMap f xs wendet die Funktion f auf jedes Element von xs an. Dabei gibt f jeweils eine Liste zurück, welche von concatMap konkateniert werden. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 257 8 · Listenverarbeitung List Comprehensions · 8.5 Übersetzungsschema J·K immer noch ohne Pattern Matching Sei e ein Ausdruck, v eine Variable, b ein Boolescher Ausdruck, und qs wieder eine Sequenz von Qualifiern. J[ e | v <- xs, qs ]K = concatMap (λv . J[ e | qs ]K) JxsK ○ 1 2 J[ e | b, qs ]K = if JbK then J[ e | qs ]K else [] ○ J[ e | ]K = [ JeK ] JeK = Wende J·K rekursiv auf alle nicht-primitiven ○ 3 ○ 4 Teilausdrücke von e an. I I 1 : Generatoren; ○ 2 : Prädikate) Wieder wird die Sequenz der Qualifier (○ 3 den Fall ohne Qualifier behandeln kann. reduziert, bis ○ 4 steigt rekursiv im AST ab, um evtl. weitere List Regel ○ Comprehensions zu übersetzen. 2 , statt einfach b zu schreiben? Frage Wozu brauchen wir JbK in Regel ○ Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 258 8 · Listenverarbeitung List Comprehensions · 8.5 Beispiel Übersetzung von [ x^2 | x <- [1..5], odd x ] = = = = J[ x^2 | x <- [1..5], odd x ]K ○ 1 concatMap (λx. J[ x^2 | odd x ]K) J[1..5]K ○ 4 concatMap (λx. J[ x^2 | odd x ]K) [1..5] ○ 2 concatMap (λx. if Jodd xK then J[ x^2 | ]K else []) [1..5] ○ 3 und 2 × ○ 4 concatMap (λx. if odd x then [ x^2 ] else []) [1..5] Frage Bisher haben wir Pattern Matching in unserem Übersetzungsschema J·K nicht berücksichtigt. Wo müssen wir nachbessern? Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 259 8 · Listenverarbeitung List Comprehensions · 8.5 Pattern Matching in List Comprehensions Beispiel Was tut die Funktion heads? Was ist der Typ? 1 heads xs = [ y | (y:_) <- xs ] Übersetzen heads mit dem Übersetzungsschema J·K: = = = Jλxs. [ y | (y:_) <- xs ]K ○ 4 λxs. J[ y | (y:_) <- xs ]K ○ 1 , behandeln Pattern wie Variable λxs. concatMap λ(y:_). J[ y | ]K ○ 3,○ 1 JxsK λxs. concatMap λ(y:_). [y] xs ] η concatMap λ(y:_). [y] Frage Gilt heads ≡ concatMap λ(y:_). [y] Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 ? 260 8 · Listenverarbeitung List Comprehensions · 8.5 Antwort Nein: Wenn der Pattern Match gegen (y:_) fehlschlägt, wird das Programm abgebrochen! 1 2 3 4 > heads [[1],[2,3],[4,5,6]] [1,2,4] > concatMap (\(y:_)-> [y]) [[1],[2,3],[4,5,6]] [1,2,4] 5 6 7 8 9 > heads [[1],[],[4,5,6]] [1,4] > concatMap (\(y:_)-> [y]) [[1],[],[4,5,6]] [1*** Exception: <interactive>:23:12-23: Non-exhaustive patterns in lambda Frage An welcher Stelle können wir das fixen? Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 261 8 · Listenverarbeitung List Comprehensions · 8.5 Definition Übersetzungsschema J·K mit Pattern Matching Sei e ein Ausdruck, p ein Pattern, v eine neue Variable, b ein Boolescher Ausdruck, und qs wieder eine Sequenz von Qualifiern. J[ e | p <- xs, qs ]K = concatMap λv . case v of p → J[ e | qs ]K _ → [] JxsK ○ 1 2 J[ e | b, qs ]K = if JbK then J[ e | qs ]K else [] ○ J[ e | ]K = [ JeK ] JeK = Wende J·K rekursiv auf alle nicht-primitiven ○ 3 ○ 4 Teilausdrücke von e an. I I I Variable v matcht gegen jeden Wert aus JxsK (cf. Seite 156), case prüft dann ob v auf Pattern p matcht (cf. Seite 162), für fehlgeschlagene Matches werden keine Ergebnisse erzeugt. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 262 8 · Listenverarbeitung List Comprehensions · 8.5 Beispiel Nochmal: heads xs = [ y | (y:_) <- xs ] Übersetzen heads mit dem Übersetzungsschema J·K: = = Jλxs. [ y | (y:_) <- xs ]K ○ 4 λxs. J[ y | (y:_) <- xs ]K ○ 1 , diesmal richtig λxs. concatMap λv . case v of (y:_) → J[ y | ]K _ → [] JxsK ○ 3,○ 1 = λxs. concatMap λv . case v of { (y:_) → [y]; _ → [] } xs ] η concatMap λv . case v of { (y:_) → [y]; _ → [] } Jetzt gilt: heads ≡ concatMap λv . case v of { (y:_) → [y]; _ → [] } Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 263 8 · Listenverarbeitung List Comprehensions · 8.5 Bibliographie Richard Bird and Philip Wadler: Introduction to Functional Programming using Haskell, Prentice Hall International, Series in Computer Science, 1998. Jeroen Fokker: Functional Programming, Department of Computer Science, Utrecht University, 1995. http://www.staff.science.uu.nl/~fokke101/courses/fp-eng.pdf Torsten Grust and Marc H. Scholl: How to Comprehend Queries Functionally, Journal of Intelligent Information Systems, vol. 12, p. 191–218, 1999. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 264