Universalität und Ausdrucksstärke von Fold

Werbung
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
Herunterladen