Fachbereich Mathematik & Informatik AG Programmiersprachen und Parallelität Prof. Dr. R. Loogen Seminar “Fun of Haskell Programming” Ausarbeitung zum Thema Universalität und Ausdrucksstärke von Fold von René Frank Matr.-Nr. 2209420 Betreuer: Mischa Dieterle Basierend auf: • Graham Hutton: A tutorial on the universality and expressiveness of fold., J. Funct. Program 1999. Zusammenfassung Der fold-Operator oder auch Faltungsfunktion ist eine häufig verwendete und sehr mächtige Funktion in funktionalen Sprachen. Er erfasst ein wichtiges rekursives Muster, welches in vielen Funktionsdefinitionen regelmäßig Anwendung findet. Thema dieser Seminararbeit ist die Ausdrucksstärke und Vielseitigkeit der Faltungsfunktion. Anhand von Listenoperationen in der funktionalen Sprache Haskell wird dabei näher auf dessen Konzept eingegangen. Es werden einige nicht sofort erkennbare Besonderheiten bzgl. der Funktionsdefinition mittels fold, sowie die daraus resultierende Vereinfachung für den Nachweis von Programmeigenschaften diskutiert. Die Möglichkeit einer effizienten und vorteilhaften Programmierung steht dabei im Fokus. Zudem ist die zugrundeliegende Ausdrucksstärke der Faltungsfunktion ein wesentlicher Punkt dieser Arbeit. Darunter fallen sowohl die Funktionsgenerierung mittels fold als auch dessen Verwendung zur Definition primitiv rekursiver Funktionen. 1 Einleitung In vielen Programmiersprachen, besonders in der funktionalen Programmierung, wird Rekursion verwendet um Schleifen bzw. wiederholende Ereignisse und Zuweisungen zu ersetzen. Dies schränkt jedoch eine reine funktionale Programmiersprache in ihrer Mächtigkeit, in Bezug zu den imperativen Programmiersprachen, nicht ein (siehe Turing-Vollständigkeit) [6]. Durch die rein rekursive Definition von Funktionen, findet sich sehr häufig ein bestimmtes rekursives Muster in verschiedenen Programmfragmenten wieder. Auch der Nachweis von Programmeigenschaften derartiger Funktionen ist davon betroffen. Bei der Beweisführung durch Induktion erhält man jeweils ein bestimmtes induktives Muster. Dies bringt natürlich nicht selten identische Aufgaben bzgl. der Implementierung und der Beweisführung mit sich. Die ständig neue Erstellung des gleichen Musters ist nicht nur zeitraubend, sondern auch fehleranfällig. Eine Funktion welche dieses Problem vereinfacht, ist der in dieser Seminararbeit diskutierte fold-Operator. Der fold-Operator wird auch als foldr oder Faltungsfunktion bezeichnet und ist ein wichtiger Bestandteil der funktionalen Programmierung. Seine Verwendung erstreckt sich über eine Vielzahl verschiedener Datentypen. In dieser Seminararbeit wird seine Vielfalt und Ausdrucksstärke anhand von listenverarbeitenden Funktionen in der funktionalen Sprache Haskell näher betrachtet. Grundsätzlich bietet der fold-Operator die Möglichkeit ohne explizite Rekursion ein weit verbreitetes rekursives Muster nutzen zu können. Zu diesem Zweck wird die Rekursion in der Definition von fold gekapselt. Dem Nutzer bleibt durch dessen Verwendung die ständige Neudefinition dieses Musters erspart. Wie die Kapselung der Rekursion im Einzelnen aussieht und welchen Vorteil dies bezüglich der Beweisführung hat, wird im weiteren Verlauf dieser Seminararbeit diskutiert. Zunächst wird der Operator, sowie das dazugehörige rekursive Muster näher erläutert. Im Anschluss werden besondere Eigenschaften für die konkrete Verwendung des Operators, sowie Vorteile bzgl. der Effizienz betrachtet. Der zweite Teil behandelt 1 die Ausdrucksstärke des fold-Operators. Hierbei wird gezeigt welche Möglichkeiten die Faltungsfunktion trotz ihrer einfachen Definition bietet. 2 Der Fold-Operator Seinen Ursprung hat der fold-Operator in der Rekursionstheorie von Kleene. Erste Anwendungen als zentrales Konzept in Programmiersprachen, waren in Form des Reduktions Operators in APL, f /x1 x2 . . . xn == x1 f x2 . . . f xn welcher den Operator zwischen die einzelnen Bestandteile des Vektors einfügt, sowie wenig später in Form des Insertion Operators in FP. (/f ) :< x1 , x2 , . . . , xn >= f :< x1 , f :< x2 , . . . f :< xn−1 , xn > ... >> In Haskell gehört der fold-Operator zu den higher-order functions der Listenverarbeitung. Neben dem hier besprochenen ursprünglichen fold (foldr) existieren noch weitere abgewandelte Definitionen. Eine häufig verwendete Form ist bspw. die endrekursiv definierte Faltungsfunktion foldl, welche in einem späteren Abschnitt näher betrachtet wird. Die vielfältige Einsetzbarkeit der Faltungsfunktion, sowie das gekapselte rekursive Muster lassen sich anhand der folgenden Beispiele sehr leicht aufzeigen. Wir betrachten nun eine einfache Funktion, welche die Zahlenwerte einer Liste von ganzen Zahlen aufaddiert. Diese Funktion lässt sich mit expliziter Rekursion definieren: sum :: [Int] → Int sum [] = 0 sum (x:xs) = x + sum xs Abbildung 1: Def. sum Problemlos kann das zugrundeliegende Rekursionsschema auf ähnliche Funktionen übertragen werden. Beispielsweise ist die Funktion length, für die Berechnung der Anzahl von Listenelementen wie folgt definiert. length :: [Int] → Int length [] = 0 length (x:xs) = 1 + length xs Aus dem Muster der obigen Beispiele lässt sich sehr gut die Signatur des foldOperators erkennen. Zunächst wird ein Anfangswert benötigt. In den Beispielen 2 wurde dafür der Wert 0 festgelegt. Weiterhin braucht es eine Funktion mit zwei Parametern. Diese kombiniert ein bisheriges Ergebnis mit dem nächsten Listenelement zu einem neuen Ergebnis [9]. Informell gilt also: f old ⊕ v [x1 , x2 , . . . , xn ] = x1 ⊕ (x2 (. . . (xn ⊕ v) . . . )) Die Klammerung ist für den standard fold-Operator rechts-assoziativ. Der Operator reduziert dabei die Liste vom Typ α zu einem Wert vom Typ β [5]. In der prelude ist der fold-Operator wie in Abbildung 2 definiert. f ist eine Funktion vom Typ α → β → β, v ein Wert vom Typ β und die übergebene Liste ist vom Typ α. Man erkennt schnell, dass es sich hierbei um ein rekursives Schema handelt. fold :: (α → β → β) → β → ([α] → β) fold f v [] = v fold f v (x:xs) = f x (fold f v xs) Abbildung 2: Def. Fold-Operator für Listen Die Funktion fold bearbeitet also die Liste indem sie ihre Elemente nacheinander, unter Verwendung eines zusätzlich angegebenen Anfangswertes, mittels einer binären Operation faltet. Die Operation muss sich dabei nicht unbedingt auf die vordefinierten Funktionen beschränken. Selbstdefinierte Funktionen, beispielsweise mittels λ-Abstraktion, können ebenfalls verwendet werden. Die Auswertung der Funktion erfolgt dabei von außen nach innen. Das jeweilige erste Element wird durch eine binäre Operation, mit dem rekursiven Aufruf von fold auf die Restliste, verknüpft. Wird die Liste komplett durchlaufen, so ist der Anfangswert v das Resultat des letzten rekursiven Aufrufs. Konkret werden also bei der Verarbeitung die ConsKonstruktoren (:) durch eine Funktion f und im Falle des kompletten durchlaufens der Liste, der Nil-Konstruktor ([]) durch den Wert v ersetzt. Entsprechend gilt f old (:) [] xs == xs für beliebige endliche Listen xs [10]. Wie folgende Abbildung verdeutlicht. [α] β : f : α1 fold f v : α2 α4 f α2 : α3 α1 f α3 f α4 [] Abbildung 3: Funktionsweise folds [7] 3 v Auf dieses Schema lassen sich unzählige Funktionen zurückführen. Die folgenden Beispiele, unter anderem die oben erstellten Funktionen sum und length können mittels fold wie folgt definiert werden: sum :: [Int] → Int sum = fold (+) 0 length :: [α] → Int length = fold (λx n → 1+n) 0 product :: [Int] → Int product = fold (∗) 1 maxList :: [Int] → Int maxList (x:xs) = fold max x xs Die Auswertung einer solchen Funktion wird nun an der length Funktion für eine Integer-Liste [5,6] demonstriert. Die Liste wird bis zur leeren Liste aufgespaltet und dabei wie oben beschrieben, der Nil ([]) sowie die Cons (:) Konstruktoren durch einem Startwert v bzw. eine Funktion f substituiert. length = f✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿ old (λx n → 1 + n) 0 [5, 6] = (λx n → 1 + n) 5 (f old (λx n → 1 + n) 0 [6]) ✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿ = (λx n → 1 + n) 5 ((λx n → 1 + n) 6 (f old (λx n → 1 + n) 0 [])) ✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿ = (λx n → 1 + n) 5 ((λx n → 1 + n) 6 0) ✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿ = (λx n → 1 + n) 5 (1 + 0) ✿✿✿✿✿✿✿ = (λx n → 1 + n) 5 1 ✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿✿ = 1✿✿✿✿✿ +1 =2 3 Universelle Eigenschaft Eine besonders oft verwendete Eigenschaft der Faltungsfunktion ist die universal property. Die universal property aus Abbildung 4, ergibt sich aus der Äquivalenz zwischen zwei Definitionen einer Funktion g die Listen verarbeitet [11]. Diese Semantikerhaltende Gleichung kann benutzt werden um Funktionen mittels fold zu definieren. Dies bietet u.a. den Vorteil, eine Funktion automatisch aus einer Spezifikation abzuleiten. Im Abschnitt 3.1 wird dieses Definitionsprinzip anhand eines Beispiels 4 demonstriert. Weiterhin ermöglicht die universal property Funktionseigenschaften schnell und einfach, ohne explizite Verwendung von Induktion nachzuweisen. Durch Substitution von g auf der linken Seite durch fold f v, ergibt sich die allgemeine Definition von fold aus Abbildung 2. Die beiden Gleichungen der linken Seite sind für den Beweis der rechten Seite wichtig. Sie sind die notwendigen Annahmen die zeigen, dass g=fold f v sehr einfach durch Induktion nachweisbar ist. Diese besondere Eigenschaft der Faltungsfunktion stellt eine erhebliche Vereinfachung für den Nachweis der jeweiligen Programmeigenschaften dar. Für die verschiedenen durch fold definierten Funktionen genügt es zu zeigen, dass die beiden Annahmen der linken Seite erfüllt sind. Dies geschieht gänzlich ohne Induktion. Für die weitere Beweisführung ist ein Verweis auf die universal property ausreichend. Dieser Punkt wird im Abschnitt 3.2 behandelt. g [] = v g (x : xs) = f x (g xs) ⇔ g = f old f v Abbildung 4: Def. universal property 3.1 Definitionsprinzip Die eben vorgestellte universal property wird unter anderem genutzt um Funktionen mittels fold neu zu definieren. Am Beispiel der sum-Funktion aus Abbildung 1 soll nun gezeigt werden, wie die universal property zur Funktionsdefinition verwendet wird. Zu diesem Zweck definieren wir sum als sum = fold f v und berechnen die Funktion f und den Wert v. Der erste Schritt ist das Gleichsetzen von sum = fold f v mit der Funktion g, der rechten Seite der universal property. Daraus ergibt sich g=sum. ) g = f old f v sum = f old f v g = sum Nun muss auf der linken Seite der universal property für g, sum eingesetzt werden. Anhand der somit erhaltenen äquivalenten Definition lassen sich f und v bestimmen. sum [] =v sum (x : xs) = f x (sum xs) Für v ergibt sich aus der ersten Gleichung und der Definition von sum aus Abbildung 1 der Wert 0. 5 ⇔ sum [] = v {Def. sum} 0=v Mittels der zweiten Gleichung lässt sich die Definition für f bestimmen. sum (x : xs) = f x (sum xs) ⇔ {Def. sum} x + sum xs = f x (sum xs) ⇐ {sum xs wird zu y umgeformt } x+y =f x y ⇔ f = (+) Durch die Umformung von sum xs zu y gewinnt man eine Gleichung, aus der die Funktion f ohne weiteres bestimmbar ist. Anhand der Berechnungen für f und v ist sum einfach zu implementieren. sum :: [Int] → Int sum = fold (+) 0 Im Allgemeinen können alle, durch fold definierbare Funktionen auf diese Weise transformiert werden. Allerdings ist das Verfahren nicht für alle Funktionen so intuitiv anwendbar wie in diesem Beispiel. Im Abschitt 4 werden einige Funktionen diesbzgl. aufgezeigt. 3.2 Nachweis von Programmeigenschaften Ein wesentlicher Vorteil einer Funktionsdefinition mittels fold ist die Möglichkeit, Funktionseigenschaften ohne Induktion nachweisen zu können. Die universal property bietet hierfür wieder die Grundlage. Grundsätzlich lässt sich mit Hilfe der universal property die Gleichheit aller Funktionen nachweisen, die durch fold definierbar sind und deren Gleichheit auch per Induktion beweisbar ist. Anhand der folgenden Gleichung soll dieses Prinzip illustriert werden. Dabei wird auf der linken Seite der Gleichung, die sum-Funktion auf eine Liste angewendet und am Ende das aufsummiert Resultat inkremmentiert. Die gleiche Funktion mittels fold tauscht jeden Cons- Konstruktor (:) durch eine Funktion f, in diesem Beispiel durch + und die leere Liste wird durch v, im Beispiel durch 1 ersetzt. (+1) . sum = f old (+) 1 Zunächst vergleichen wir die obige Definiton mit der rechten Seite der universellen 6 Eigenschaft folds aus Abbildung 4 und bestimmen g, f und v. g = f old f v (+1) . sum = f old (+) 1 ) g = (+1) . sum, f = (+), v = 1 Die bestimmten Werte müssen nun auf der linken Seite der universal property eingesetzt werden. ((+1) . sum) [] = 1 ((+1) . sum) (x : xs) = (+) x (((+1) . sum) xs) Die somit erhaltene Definition kann nun vereinfacht werden. Zu diesem Zweck wird die Komposition aufgelöst. Im Anschluss lässt sich die Definition wie in folgender Berechnung ganz einfach prüfen. Die eigentlich notwendige Induktion ist dabei durch den fold-Operator gekapselt. sum [] + 1 |{z} = 0+1=1 (1) Def. sum sum(x : xs) + 1 |{z} = (x + sum xs) + 1 |{z} = x + (sum xs + 1) Def. sum 3.3 (2) Assoz. Fusion Property Neben der universal property ist die Fusionseigenschaft eine weitere Besonderheit der Faltungsfunktion. Der Fusionssatz aus Abbildung 5 sagt, dass eine mit fold komponierte Funktion auch durch eine einzelne fold-Operation ausdrückbar ist. Statt also auf das Resultat der Berechnung von fold g w eine weitere Funktion h anzuwenden, wird nun die Verschmelzung der beiden Operationen versucht. Dies führt in der Regel zu einem enormen Effizienzanstieg [12]. Die nötigen Voraussetzungen dafür lassen sich wieder mit Hilfe der universal property berechnen. h . f old g w = f old f v Abbildung 5: Def. Fusionssatz Zunächst wird die Gleichung wieder in Beziehung zur rechten Seite der universal property gesetzt: g = f old f v h . f old g w = f old f v 7 ) g = h . f old g w Die soeben bestimmten Werte müssen nun auf der linken Seite der universal property eingesetzt werden. (h . f old g w) [] = v (h . f old g w) (x : xs) = f x ((h . f old g w) xs) Die resultierende Definition wird bzgl. der Komposition vereinfacht. h (f old g w []) = v h (f old g w (x : xs)) = f x (h (f old g w xs)) Im Anschluss werden die Gleichungen umgeformt. h (f old g w []) = v ⇔ {Def. fold} hw = v Bei der Berechnung der zweiten Gleichung erhalten wir fold g w xs auf beiden Seiten. Dies kann wieder durch y substituiert werden. h (f old g w (x : xs)) = f x (h (f old g w xs)) {Def.f old} h (g x (f old g w xs)) = f x (h (f old g w xs)) ⇐ {(f old g w xs) zu y} h (g x y) = f x (h y) ⇔ Wir haben zwei einfache Bedingungen berechnet (Abbildung 6) welche bestimmen, wann eine Komposition einer Funktion mit fold zu einem einzelnen fold "fusioniert"werden kann. Diese Eigenschaft folds lässt sich genau wie die universal property als Beweisprinzip nutzen. Häufig ist sie aufgrund ihrer Einfachheit der universal property vorzuziehen. Auch hier kann komplett auf Induktion verzichtet werden. hw = v h (g x y) = f x (h y) ⇒ h . f old g w = f old f v Abbildung 6: Fusions-Bedingung Als Beispiel dient wiederum die um 1 inkremmentierte sum Funktion des letzten Unterabschnittes. Zunächst wird sum mittels fold definiert. 8 (+1) . sum = f old (+) 1 ⇔ (+1) . f old (+) 0 = f old (+) 1 Diese Gleichung wird im Anschluss in Beziehung zur rechten Seite der Fusionseigenschaft aus Abbildung 6 gesetzt. h . f old g w = f old f v (+1) . f old (+) 0 = f old (+) 1 Daraus ergeben sich folgende Werte: h = (+1), g = (+), w = 0, f = (+), v = 1, die auf der linken Seite der fusion property eingesetzt werden können. (+1) 0 = 1 (+1) ((+) x y) = (+) (x) ((+1) y) Die Gleichungen werden vereinfacht und man erhält eine wahre Aussage. 0 + 1 = 1 und (x + y) + 1 = x + (y + 1) Allgemein gilt für beliebige assoziative Infix-Operatoren (⊕ a) . f old (⊕) b = f old (⊕) (b ⊕ a) Mit Hilfe der Vielfältigkeit der universal property war es möglich eine weitere wichtige Eigenschaft abzuleiten. Es wurden zwei hinreichende Bedingungen berechnet, um die Fusion der Komposition einer Funktion f und fold, zu einer einzeln foldFunktion zu garantieren. Dies dient u.a. einer besseren Strukturierung, sowie einer Effizienzsteigerung der Implementierung. Aufgrund ihrer Einfachheit ist die Fusionseigenschaft der universal property häufig vorzuziehen. 4 4.1 Mächtigkeit von fold Generieren von Tupel Eine sehr mächtige Eigenschaft folds ist die effiziente Generierung von Tupel. Ein Beispiel dafür soll die Funktion sumlength aus Abbildung 7 sein. Durch die separate Berechnung beider Funktionen wird die Liste xs zwangsläufig doppelt durchlaufen. Die sehr einfache aber ineffiziente Definition kann schnell verbessert werden. Zur Vermeidung des obigen Problems lässt sich sumlength mittels fold und einer 9 λ-Abstraktion definieren. Das resultierende sumlength’ benötigt für die gleiche Berechnung nur einen Listendurchlauf. Dieses Prinzip ist auf alle Tupel anwendbar deren einzelne Funktionen mittels fold ausdrückbar sind. sumlength :: [Int] → (Int, Int) sumlength xs = (sum xs, length xs) sumlength’ :: [Int] → (Int, Int) sumlength’ = fold (λn (x,y) → (n+x,1+y)) (0,0) Abbildung 7: Def. sumlength und sumlength’ Viele andere Funktionen, wie bspw. die aus der prelude bekannte Funktion dropWhile p, sind etwas schwieriger zu definieren. Für deren Definition ist wieder die universal property hilfreich. dropWhile :: (α → Bool) → [α] → [α] dropWhile p [] = [] dropWhile p (x:xs) = if p x then dropWhile p xs else x:xs Für dropWhile p = fold f v erhalten wir auf der rechten Seite der universal property g = dropWhile p. ) g = f old f v dropW hile p = f old f v g = dropW hile p Dies für g in auf der linken Seite der universal property eingesetzt erzeugt folgende Gleichungen. dropW hile p [] =v dropW hile p (x : xs) = f x (dropW hile p xs) Anhand der ersten Gleichung und der ursprünglichen Definition von dropWhile gilt v = []. Die zweite Gleichung ist wieder für die Definition von f notwendig. dropW hile p (x : xs) = f x (dropW hile p xs) ⇔ {Def. dropW hile} if p x then dropW hile p xs else x : xs = f x (dropW hile p xs) ⇐ {dropW hile p xs wird zu ys substituiert } if p x then ys else x : xs = f x ys 10 Das Problem dieser Berechnung ist die freie Variable xs. Auf dem direkten Weg lässt sich die Funktion dropWhile nicht definieren. In diesem Fall hilft die weitaus allgemeinere Funktion dropWhile’. Sie paart das Resultat von dropWhile p mit der übergebenen Liste. dropWhile’ :: (α → Bool) → [α] → ([α],[α]) dropWhile’ p xs = (dropWhile p xs, xs) Für dropWhile’ p = fold f v erhalten wir auf der rechten Seite der universal property g = dropWhile’ p. Die daraus resultierenden Gleichungen sehen wie folgt aus: dropW hile′ p [] =v ′ dropW hile p (x : xs) = f x (dropW hile′ p xs) Anhand der ersten Gleichung wird v bestimmt dropW hile′ p [] = v ⇔ {Def. dropW hile′ } (dropW hile p [], []) = v ⇔ {Def. dropW hile} ([], []) = v Die zweite Gleichung liefert diesmal eine gültige Definition für f, da alle Variablen gebunden vorkommen. dropW hile′ p (x : xs) = f x (dropW hile′ p xs) ⇔ {Def. dropW hile′ } (dropW hile p (x : xs), x : xs) = f x (dropW hile p xs, xs) ⇔ {Def. dropW hile} (if p x then dropW hile p xs else x : xs, x : xs) = f x (dropW hile p xs, xs) ⇔ {(dropW hile p xs) wird zu ys substituiert } (if p x then ys else x : xs, x : xs) = f x (ys, xs) Die neue Definition von dropWhile’ aus Abbildung 8 genügt der Gleichung dropWhile’ p xs = (dropWhile p xs, xs) ohne dropWhile zu verwenden. Um die eigentliche Funktion dropWhile zu definieren bedient man sich dem ersten Element des Ergebnistupels von dropWhile’. Grundsätzlich lässt sich jede Funktion über endliche Listen, die das gewünschte Ergebnis mit der Ausgangsliste paart, durch fold definieren. 11 dropWhile’ :: (α → Bool) → ([α] → ([α],[α])) dropWhile’ p = fold f v where f x (ys,xs) = (if p x then ys else x:xs, x:xs) v = ([],[]) Abbildung 8: Def. dropWhile’ mit fold dropWhile :: (α → Bool) → ([α] → [α]) dropWhile p = fst . dropWhile’ p 4.2 Primitive Rekursion “Eine Funktion f : Nn → N heißt primitiv rekursiv, falls f entweder eine primitiv rekursive Grundfunktion ist oder sich aus diesen in endlich vielen Schritten durch Komposition und/oder primitive Rekursion erzeugen lässt.” [8] Die primitive Rekursion ist ein Spezialfall der linearen Rekursion. Primitiv rekursive Funktionen über den natürlichen Zahlen lassen sich mit der Zahl 0 und der Nachfolgefunktion definieren. f 0 = ... f n = . . . f (n − 1) . . . Bei primitiv rekursiven Funktionen über Listen ist dies relativ ähnlich. Es wird ein Startpunkt, bspw. die leere Liste benötigt. Weiterhin braucht es einen Übergang zum nächsten Listenelement. Die dafür verwendete Nachfolgefunktion muss von f xs auf f (x:xs) übergehen. Im folgenden Beispiel wird eine Funktion sumP verwendet, welche die Summe aus den Produkten aller übergebenen Zahlenlisten bestimmt. Dabei werden die Listenelemente sukzessive durchlaufen und einer weiteren Funktion übergegen [3]. sumP :: [[Int]] → Int sumP [] = prod [] sumP (x:xs) = (prod x) + (sumP xs) prod prod [] prod [x] prod (x:xs) :: [Int] → Int = 0 = x = x ∗ prod xs Statt prod kann jede Funktion f vom Typ [Int] → Int verwendet werden. Das somit erhaltene Muster soll als Beispiel für die nachfolgende allgemeine Definition dienen. 12 sumP :: ([Int] → Int) → [[Int]] → Int sumP f [] = f [] sumP f (x:xs) = (+) (f x) (sumP f xs) Jede primitiv rekursive Funktion über Listen ist auch mittels fold definierbar. Um dies zu zeigen benötigen wir die Tupeltechnik des letzten Abschnittes. Wie bereits bekannt kapselt der fold-Operator das folgende rekursive Muster zur Definition einer Funktion h. h [] =v h (x : xs) = g x (h xs) Solche Funktionen lassen sich als h = fold g v neu definieren. Um dieses Muster primitiv rekursiv zu definieren sind zwei Schritte notwendig. Zunächst muss ein neues Argument y zu der Funktion h hinzugefügt werden. Für den Fall der leeren Liste ist y das Argument einer neuen Funktion f. Im rekursiven Zweig der Funktion wird y unverändert an g und h übergeben. Daraus ergibt sich für die Funktion h: hy [] =f y h y (x : xs) = g y x (h y xs) Im zweiten Schritt geben wir die Liste xs als extra Argument zur Hilfsfunktion g. Die somit erhaltene Definition für die Funktion h ist primitiv rekursiv. hy [] =f y h y (x : xs) = g y x xs (h y xs) Um nun die Funktion h mit fold zu definieren, muss für die Gleichung h y = fold i j die Funktion i und der Wert j bestimmt werden. Dies ist allerdings nicht direkt möglich. Grund dafür ist die freie Variable xs, welche nach Anwendung der universal property auf der linken Seite stehen bleibt. Die Funktion i kann dadurch nicht gültig definiert werden. Abhilfe schafft die Anwendung der Tupeltechnik. Die dafür verwendete allgemeinere Funktion k paart das Ergebnis von h y auf eine Liste und die Liste selbst. k y xs = (h y xs, xs) Mittels der universal property finden wir wieder die äquivalente Gleichung zu k y = fold i j. Die rechte Seite der universal property gibt g = k y. Dies in die linke Seite eingesetzt erzeugt folgende Gleichungen: ky [] =j k y (x : xs) = i x (k y xs) 13 Aus der Berechung der ersten Gleichung erhält man j = (f y, []). k y [] = j ⇔ {Def. k} (h y [], []) = j ⇔ {Def. h} (f y, []) = j Aus der zweiten Gleichung wird im Anschluss die Definition für i bestimmt. k y (x : xs) = i x (k y xs) {Definition von k} (h y (x : xs), x : xs) = i x (h y xs, xs) ⇔ {Definition von h} (g y x xs (h y xs), x : xs) = i x (h y xs, xs) ⇐ {Generalisieren (h y xs) zu z} (g y x xs z, x : xs) = i x (z, xs) ⇔ Die Funktion k lässt sich nun wie in Abbildung 9 implementieren. Diese Definition ist semantisch äquivalent zu k y xs = (h y xs, xs), benötigt jedoch die Funktion h nicht. Die eigentliche Implementierung von h lässt sich nun durch h y = fst . k y realisieren. Damit wurde gezeigt das eine beliebige primitiv rekursive Funktion über Listen durch fold neu definiert werden kann. k y = fold i j where i x (z, xs) = (g y x xs z, x:xs) j = (f y, []) Abbildung 9: primitiv rekursive Funktion k 4.3 Funktionsgenerierung Funktionen sind in Haskell first-class values. D.h. sie können als Argumente in anderen Funktionen verwendet werden. Dieses Prinzip erhöht natürlich auch die Ausdrucksstärke des fold-Operators. Thema dieses Abschnittes ist die Generierung neuer Funktionen durch die Faltungsfunktion. Das Prinzip wird wieder anhand der Funktion sum erläutert. Die ursprüngliche Implementierung sum = fold (+) 0 verarbeitet die Liste von rechts nach links. Eine effizientere Implementierung ist die Funktion suml aus Abbildung 10. Die Liste wird dabei von links nach rechts abgearbeitet. Dazu verwendet suml eine interne Funktion die einen weiteren Parameter als Akkumulator nutzt. 14 suml :: [Int] → Int suml xs = suml’ xs 0 where suml’ [] n = n suml’ (x:xs) n = suml’ xs (n+x) Abbildung 10: Def. suml Die Definition von suml mit fold ist wieder nicht direkt möglich. Daher wird dies über den indirekten Weg mit der Hilfsfunktion suml’ gezeigt. Durch Anwendung der universal property erhalten wir die äquivalente Gleichung zu suml’ = fold f v. Die rechte Seite der universal property ergibt sich mit g = suml’. Dies in die linke Seite eingesetzt erzeugt folgende Gleichungen: suml′ [] =v suml′ (x : xs) = f x (suml′ xs) Für v erhalten wir durch die erste Gleichung n = n. Somit gilt v = id. Durch die zweite Gleichung erhalten wir die Definition für die Funktion f. suml′ (x : xs) = f x (suml′ xs) ⇔ {Funktionen} suml′ (x : xs) n = f x (suml′ xs) n ⇔ {Definiton von suml’} ′ suml xs (n + x) = f x (suml′ xs) n ⇐ {Generalisierung (suml’ xs) zu g } g (n + x) =f xgn ⇔ {Funktionen} f = λx g → (λn → g (n + x)) Anhand von f und v lässt sich suml’ wie folgt definieren: suml’ = fold (λx g → (λn → g (n+x))) id Der damit erhaltene Funktionstyp suml’ :: [Int] -> (Int -> Int) ist hierbei der entscheidende Punkt für die Verwendung des fold-Operators. Sind beispielsweise die beiden Argumente in ihrer Reihenfolge vertauscht, ist die Funktion nicht mehr durch fold definierbar. Bei der Verarbeitung einer Liste durch suml’ wird das Listenende durch die Indetitätsfunktion und der jeweilige Kons-Konstruktor durch die Funktion f ersetzt. Die durch f dargestellte Funktion erzeugt aus einem Wert x und einer 15 Funktion g eine neue Funktion. Diese neue Funktion besitzt einen Akkumulatorparameter n, welcher mit x addiert und das Ergebnis im Anschluss von g verwendet wird. Die Auswertung soll durch folgendes Beispiel veranschaulicht werden. suml′ [3, 14] suml′ = f old (λx g → (λn → g (x + n))) id [3, 14] = (λx g → (λn → g (x + n))) 3 (f old f id [14]) }| z | {z } { (λx g → (λn → g (x + n))) 14 (f old f id []) | {z id } = (λx g → (λn → g (x + n))) 3 (λn → id (14 + n)) = λn → (λn1 → id (14 + n1 )) (3 + n) Durch die per fold neu definierte Hilfsfunktion suml’ lässt sich suml ohne weiteres wie in Abbildung 11 implementieren. Anhand des Beispiels wurde gezeigt wie sich Funktionen durch den fold-Operator generieren lassen. Dies stellt eine erhebliche Erweiterung der Einsatzmöglichkeiten der Faltungsfunktion dar. suml = fold (λx g → (λn → g (n+x))) id xs 0 Abbildung 11: Def. suml mit fold 4.4 Faltung von links Eine endrekursive Variante der Faltung ist die Funktion foldl. Ihr wesentlicher Unterschied zu foldr wird in Abbildung 12 sichtbar [2]. Die Listenelemente werden diesmal während der Reduktion nach links geklammert. Die Funktion nimmt die Listenelemente und den Startwert/Akkumulator v und führt die Funktion f damit aus. Das jeweilige Resultat der Berechnung steht im Akkumulator des nächsten Iterationsschritts zur Verfügung [5]. f old (⊕) v [x1 , x2 , . . . , xn ] = (. . . ((v ⊕ x1 ) ⊕ x2 ) . . . ) ⊕ xn 16 : ⊕ α1 ⊕ α2 foldr (+) v ... ⊕ : α1 ... α2 αn α4 v αn ⊕ : ⊕ ... foldl (+) v α2 ⊕ [] v α1 Abbildung 12: foldl vs. foldr Die Definition des foldl-Operators ist in Abbildung 13 zu sehen. Für kommutative Faltungsoperationen macht es semantisch keinen Unterschied, ob eine Faltung von links oder rechts vorgenommen wird. Beide Funktionen sind polymorphe Standardprozeduren und sich im Prinzip sehr ähnlich. Bei näherer Untersuchung ergibt sich sogar, dass foldl durch den standard fold-Operator ausdrückbar ist. Dies ist umgekehrt jedoch nicht möglich. Die Beziehungen zwischen foldr und foldl lassen sich mit den Dualitätstheoremen zeigen. Es gibt einige Funktionen bei denen sich trotz der Gleichheit beider Operatoren, Effizienzunterschiede bemerkbar machen. foldl :: (β → α → β) → β → ([α] → β) foldl f v [] = v foldl f v (x:xs) = foldl f (f v x) xs Abbildung 13: Def. foldl Anhand der in der prelude definierten Funktion reverse soll der Unterschied beider Funktionen verdeutlicht werden. Abbildung 14 zeigt mögliche Implementierungen von reverse mit foldl und foldr. Wie sich leicht erkennen lässt ist die Definition mit foldl wesentlich effizienter. Aufgrund der linksseitigen Klammerung kann der ineffiziente append Operator (++) komplett vermieden werden. reverseL :: [α] → [α] reverseL = foldl (λxs x → x:xs) [] reverseR :: [α] → [α] reverseR = foldr (λx xs → xs ++ [x]) [] Abbildung 14: Def. reverse mit foldr und foldl 17 Für eine Funktionsdefinition mittels foldr und linksseitiger Klammerung kann das Verfahren aus Abschnitt 4.3 genutzt werden. Dafür wird reverseR um eine Hilfsfunktion reverseR’ erweitert. reverseR :: [a] → [a] reverseR xs = reverseR’ xs [] where reverseR’ [] ys = ys reverseR’ (x:xs) ys = reverseR’ xs (x:ys) Die Hilfsfunktion wird durch die universal property als reverseR’ = fold f v neu definiert. Für die rechte Seite der universal property erhalten wir g = reverseR’. Somit können nun folgende Gleichungen aufgestellt werden. reverseR′ [] = v ′ reverseR (x : xs) = f x (reverseR′ xs) Für v ergibt sich anhand der ersten Gleichung und der ursprünglichen Definition von reverseR’ die Identitätsfunktion (id). Die Definition für f ergibt sich aus folgender Berechnung. reverseR′ (x : xs) = f x (reverseR′ xs) ⇔ {Funktionen} ′ reverseR (x : xs) ys = f x (reverseR′ xs) ys ⇔ {Definiton von reverseR’} reverseR′ xs (x : ys) = f x (reverseR′ xs) ys ⇐ {Generalisierung (reverseR’ xs) zu g } g (x : ys) = f x g ys ⇔ {Funktionen} f = λx g → (λys → g (x : ys)) Daraus erhalten wir die Definition für reverseR’ und somit auch für reverseR. reverseR’ = foldr (λx g → (λys → g (x:ys))) id reverseR xs = foldr (λx g → (λys → g (x:ys))) id xs [] Der andere Fall zeigt sich bspw. anhand der im prelude definierten Funktion concat zum Flachklopfen einer Liste von Listen. Für die assoziative Listenkonkatenation (++) gilt, dass die Rechtsfaltung wie auch die Linksfaltung das gleiche Resultat 18 liefern. Dies ist durch die leere Liste als neutrales Element der Listenkonkatenation möglich. foldr (++) [] xss = foldl (++) [] xss Der Aufwand der Rechtsfaltung ist jedoch linear in der Länge der Komponentenlisten. Die Linksfaltung führt zu einem quadratischen Aufwand [4]. Wie dieses Beispiel zeigt haben beide Definitionen ihre Daseinsberechtigung. Die Wahl des jeweiligen Operators ist vom spezifischen Verwendungszweck abhängig. Fazit Durch die Kapselung eines besonders häufig verwendeten rekursiven Musters, ist der fold-Operator sehr flexibel für viele Funktionen einsetzbar. Dies bietet unter anderem die Möglichkeit Funktionen einfacher und strukturierter zu implementieren, da keine explizite Rekursion notwendig ist. Ebenso wurde gezeigt wie der Nachweis von Programmeigenschaften durch den fold-Operator erheblich vereinfacht wird. Viele seiner besonderen Eigenschaften sind auf dem ersten Blick nicht sofort erkennbar. Beispielsweise der Fusionssatz für die Verschmelzung zweier komponierter Funktion, was in der Regel einen erheblichen Effizienzgewinn nach sich zieht. Aber auch die Tupelgenerierung oder die Möglichkeit Funktionen durch fold generieren zu lassen machen den fold-Operator zu einem wesentlichen Bestandteil der funktionalen Programmierung. Literatur [1] Graham Hutton. A tutorial on the universality and expressiveness of fold. Cambridge University Press 1999. [2] Christoph Lüth. Einfuhrung in die Funktionale Programmierung. http:// www.informatik.uni-bremen.de/~cxl/lehre/pi3.ws08/slides/handouts-11.pdf, Zugriff 10.01.2012. [3] Simon Thompson. The Craft of Functional Programming. Second Edition. [4] Rita Loogen. Konzepte von Programmiersprachen. Vorlesungsskript WS 2010/11, Universität Marburg 2011. [5] T. Grust. Funktionale Programmierung - Listenverarbeitung, 2003. http: //www.inf.uni-konstanz.de/dbis/teaching/ss03/functional-programming/ download/fp-8.pdf, Zugriff 29.01.2012. [6] Wolfram Amme. Funktionale Programmierung. FSU Jena, SS 2006. https://caj.informatik.uni-jena.de/main?eFJD=RE9XTkxPQUQ% 3D&eElE=TlRNNE53JTNEJTNE, Zugriff 01.02.2012. 19 [7] haskellwiki. Fold, 2011. http://www.haskell.org/haskellwiki/Fold, Zugriff 25.01.2012. [8] Rita Loogen. Theoretische Informatik. Vorlesungsskript SS 2011, Universität Marburg 2011. [9] Horst Hansen. Folding, 2011. http://www.f4.htw-berlin.de/people/hansen/ FHTW-AI/Lehre/2011SS/PProg/Folien/Folding.full.pdf, Zugriff 15.01.2012. [10] Paul Hudak. The Haskell School of Music, 2012.http://www.cs.yale.edu/ homes/hudak/Papers/HSoM.pdf, Zugriff 25.01.2012. [11] Rodriguez, Loidl, Abel. Funktionale Programmierung Folds und Nested Datatypes, 2009.http://www2.tcs.ifi.lmu.de/lehre/SS09/Fun/AFP09.pdf, Zugriff 15.01.2012. [12] Timo Wlecke. Introduction to Functional Programming using Haskell, 2003.http://www.fh-wedel.de/~si/seminare/ws03/Ausarbeitung/3.zahlen/ zahlen04.htm, Zugriff 18.01.2012. 20