7 Funktionaler Programmierstil und Rekursion (2. Teil) 7.7 Formen ...
7 Funktionaler Programmierstil und Rekursion (2. Teil) 7.7 Formen ...
7 Funktionaler Programmierstil und Rekursion (2. Teil) 7.7 Formen ...
Erfolgreiche ePaper selbst erstellen
Machen Sie aus Ihren PDF Publikationen ein blätterbares Flipbook mit unserer einzigartigen Google optimierten e-Paper Software.
Peter Hubwieser Ablaufmodellierung WS 2002/03<br />
KAPITEL 7: <strong>Funktionaler</strong> <strong>Programmierstil</strong> (2) LERNINHALTE V 1.0 Seite 1<br />
7 <strong>Funktionaler</strong> <strong>Programmierstil</strong> <strong>und</strong> <strong>Rekursion</strong> (<strong>2.</strong> <strong>Teil</strong>)<br />
<strong>7.7</strong> <strong>Formen</strong> der <strong>Rekursion</strong><br />
Die Anwendung <strong>und</strong> die Effizienz rekursiver Funktionen hängt sehr stark von der Struktur der<br />
<strong>Rekursion</strong> ab: rekursive Funktionsanwendungen erzeugen ja neue Prozesse, die in der Regel<br />
die Struktur der Berechnung (zumindest vorübergehend) komplizierter machen. Leider beschränken<br />
sich dabei nicht alle rekursiven Funktionen auf so einfache Veränderungen der<br />
Berechnungsstruktur wie fak (siehe Abschnitt 7.5.3) oder ggT (siehe Abschnitt 7.5). In der<br />
Tat gehören die beiden Funktionen zu einer Klasse rekursiver Funktionen mit relativ einfacher<br />
Struktur (linear rekursive Funktionen).<br />
<strong>7.7</strong>.1 Lineare <strong>Rekursion</strong><br />
Eine rekursive Funktionsdeklaration heißt linear rekursiv, falls in jedem Fallunterscheidungszweig<br />
höchstens ein rekursiver Aufruf der Funktion enthalten ist. Aus der Sicht unserer<br />
dynamischen Datenflussdiagramme enthält dann jedes Diagramm maximal einen Prozess dieser<br />
Funktion. Beispiele dafür sind, wie schon erwähnt, die Funktionen fak <strong>und</strong> ggT.<br />
Besonders einfach (<strong>und</strong> daher sehr effizient zu berechnen) sind linear rekursive Funktionen,<br />
bei deren Berechnung die rekursiven Aufrufe immer der letzte Verarbeitungsschritt sind, wie<br />
z.B. in ggT (siehe Abbildung 1). Solche Funktionen nennt man repetitiv rekursiv.<br />
Auswertung<br />
18 12 18-12 12<br />
6 12-6<br />
ggT ggT<br />
Abbildung 1: Dynamisches Datenflussdiagramm für ggT<br />
ggT<br />
6<br />
Struktur<br />
m n<br />
ggT<br />
falls m=n<br />
falls m>n<br />
falls m
Peter Hubwieser Ablaufmodellierung WS 2002/03<br />
KAPITEL 7: <strong>Funktionaler</strong> <strong>Programmierstil</strong> (2) LERNINHALTE V 1.0 Seite 2<br />
<strong>7.7</strong>.2 Kaskadenartige <strong>Rekursion</strong><br />
Die Definition von linearer <strong>Rekursion</strong> legt die Vermutung nahe, dass es auch Funktionen gibt,<br />
die in mindestens einem Fallunterscheidungszweig mehr als einen rekursiven Aufruf enthalten.<br />
In solchen Fällen spricht man von kaskadenartiger <strong>Rekursion</strong>.<br />
Dazu ein Beispiel:<br />
Die Binomialkoeffizienten bn(n,k) (oft auch „n über k“ gesprochen) haben für vielen Anwendungen<br />
aus der Kombinatorik <strong>und</strong> Wahrscheinlichkeitsrechnung große Bedeutung. So<br />
beträgt beispielsweise die Wahrscheinlichkeit eines 6-ers im Lotto 1/bn(49,6). Wenn man<br />
Potenzen von Binomen (a + b) n ausmultipliziert, dann kommen die Binomialkoeffizienten als<br />
konstante Faktoren vor, z.B. für n=4 als Faktoren 1, 4, 6, 4, 1:<br />
(a + b) 4 = 1a 4 + 4a 3 b + 6a 2 b 2 + 4ab 3 + 1b 4 .<br />
Die Berechnung dieser Binomialkoeffizienten führt uns auf eine Funktionsdeklaration, in der<br />
ein rekursiver Aufruf zwei neue Prozesse erzeugt. Zur Herleitung der Deklaration betrachten<br />
wir zunächst das Pascalsche Dreieck, in dem sie sich die Binomialkoeffizienten sehr übersichtlich<br />
anordnen lassen (siehe Abbildung 2).<br />
1<br />
1 1<br />
1 2 1<br />
1 3 3 1<br />
1 4<br />
+<br />
6 4 1<br />
Abbildung 2: Das Pascalsche Dreieck zur Berechnung der Binomialkoeffizienten<br />
Zur Erzeugung dieses Dreiecks gelten die folgenden Vorgaben (s. a. die folgende Tabelle):<br />
1) Der Index k der Spalten läuft von 0 bis zum jeweiligen Zeilenindex n.<br />
2) An den äußersten Positionen steht (k = 0 <strong>und</strong> k = n) steht immer der Wert 1, also:<br />
bn(n, 0) = bn(n, n ) = 1 für alle n.<br />
3) Die „inneren“ Zahlen (0 < k < n) lassen sich folgendermaßen aus denen der darüber<br />
liegenden Zeile berechnen: bn(n, k) = bn(n–1, k) + bn(n–1, k–1). Bitte beachte Sie dazu<br />
auch das in der folgenden Tabelle grau gekennzeichnete Beispiel zur Berechnung<br />
von bn(5, 2).<br />
k = 0 k = 1 k = 2 k = 3 k = 4 k = 5<br />
n = 0 1<br />
n = 1 1 1<br />
n = 2 1 2 1<br />
n = 3 1 3 3 1<br />
n = 4 1 4 6 4 1<br />
n = 5 1 5 10 10 5 1<br />
usw.
Peter Hubwieser Ablaufmodellierung WS 2002/03<br />
KAPITEL 7: <strong>Funktionaler</strong> <strong>Programmierstil</strong> (2) LERNINHALTE V 1.0 Seite 3<br />
Damit haben wir auch schon die Struktur für unserer rekursive Funktion abgeleitet:<br />
function bn (nat n,k): nat<br />
return if n=0 or k=0 or k=n then 1 else bn(n–1, k) + bn(n–1, k–1) fi<br />
Dabei gelten immer die Nebenbedingung n ≥ k <strong>und</strong> n, k ≥ 0.<br />
Dieses <strong>Rekursion</strong>sschema führt z.B. für „4 über 3“ zu folgender Berechnungsfolge:<br />
bn(5,3) =<br />
bn(4,3) + bn(4,2) =<br />
bn(3,3) + bn(3,2) + bn(3,2) + bn(3,1) =<br />
1 + bn(2,2) + bn(2,1) + bn(2,2) + bn(2,1) + bn(2,1) + bn(2,0) =<br />
1 + 1 + bn(1,1) + bn(1,0) + 1 + bn(1,1) + bn(1,0) + bn(1,1) + bn(1,0) + 1 =<br />
1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 =<br />
10<br />
Kaskadierende <strong>Rekursion</strong>saufrufe vervielfachen die Zahl der aktiven Prozesse <strong>und</strong> sind daher<br />
für die Laufzeit eines Algorithmus von erheblicher Bedeutung. Zudem werden in diesem Fall<br />
viele Funktionen mehrmals benötigt (unterstrichen), ohne dass ihre Ergebnisse dafür zwischendurch<br />
aufbewahrt würden. Dieselbe Berechnung wird daher u.U. mehrfach ausgeführt<br />
(z.B. wird in unserem obigen Ablauf bn(2,1) dreimal berechnet). Dieses Verfahren ist also<br />
sicher nicht sehr effizient.<br />
Struktur<br />
n k<br />
bn<br />
5 3<br />
bn<br />
n 0 UND<br />
k0 UND k
Peter Hubwieser Ablaufmodellierung WS 2002/03<br />
KAPITEL 7: <strong>Funktionaler</strong> <strong>Programmierstil</strong> (2) LERNINHALTE V 1.0 Seite 4<br />
bn(3,3)<br />
bn(4,3)<br />
bn(2,2)<br />
bn(3,2)<br />
bn(2,1)<br />
bn(5,3)<br />
bn(2,2)<br />
bn(3,2)<br />
bn(4,2)<br />
bn(3,1)<br />
bn(2,1) bn(2,1) bn(2,0)<br />
bn(1,1) bn(1,0) bn(1,1) bn(1,0) bn(1,1) bn(1,1)<br />
Abbildung 4: Aufrufbaum für bn(5,3)<br />
<strong>7.7</strong>.3 Vernestete <strong>Rekursion</strong><br />
Die kompliziertesten Aufrufstrukturen entstehen, wenn die rekursiven Aufrufe der Funktion<br />
in den Parameterlisten weitere rekursive Aufrufe enthalten. Solche Funktionen heißen vernestet<br />
rekursiv.<br />
Beispiel:<br />
Die wegen ihrer sehr interessanten Eigenschaften bekannte Ackermann-Funktion enthält (neben<br />
weiteren rekursiven Aufrufen im Rumpf) einen solchen Aufruf in der Parameterliste (unterstrichen).<br />
Zunächst die mathematische Definition:<br />
acker(0, n) = n+1<br />
acker(m, 0) = acker(m–1, 1)<br />
acker(m,n) = acker (m–1, acker (m, n–1)) , falls m>0 <strong>und</strong> n>0.<br />
In FPPS programmiert:<br />
function acker(nat m, n): nat<br />
return if m=0 then n+1 else if n=0 then acker (m–1, 1) else acker (m–1, acker (m, n–1)) fi fi<br />
Die Auswertung der Ackermann-Funktion zeigen wir exemplarisch am Beispiel acker(3,2):<br />
acker(3,2) =<br />
acker(2, acker(3,1)) =<br />
acker(2, acker(2, acker(3,0))) =<br />
acker(2, acker(2, acker(2,1))) =<br />
acker(2, acker(2, acker(1, acker(2,0)))) =<br />
acker(2, acker(2, acker(1, acker(1,1)))) =<br />
acker(2, acker(2, acker(1, acker(0, acker(1,0))))) =<br />
acker(2, acker(2, acker(1, acker(0, acker(0,1))))) =<br />
acker(2, acker(2, acker(1, acker(0, 2)))) =<br />
acker(2, acker(2, acker(1, 3))) =<br />
acker(2, acker(2, acker(0, acker(1,2)))) =
Peter Hubwieser Ablaufmodellierung WS 2002/03<br />
KAPITEL 7: <strong>Funktionaler</strong> <strong>Programmierstil</strong> (2) LERNINHALTE V 1.0 Seite 5<br />
acker(2, acker(2, acker(0, acker(0, acker(1,1))))) =<br />
acker(2, acker(2, acker(0, acker(0, acker(0, acker(1,0)))))) =<br />
acker(2, acker(2, acker(0, acker(0, acker(0, acker(0,1))))))=<br />
acker(2, acker(2, acker(0, acker(0, acker(0, 2))))) =<br />
acker(2, acker(2, acker(0, acker(0, 3)))) =<br />
acker(2, acker(2, acker(0, 4))) =<br />
acker(2, acker(2, 5)) = usw.<br />
Wie man sieht, führen bereits Aufrufe mit sehr niedrigen Parameterwerten zu sehr langen<br />
Auswertungen, die wir hier gar nicht in voller Länge zeigen wollen.<br />
Abbildung 5 zeigt die Auswertung von acker(3,2) im dynamischen Datenflussdigramm. Man<br />
beachte dabei die Verkettung unausgewerteter Funktionsaufrufe (als wartender Prozesse), die<br />
zu einem hohen Verbrauch an Ressourcen führt.<br />
m<br />
3<br />
acker<br />
m=0<br />
n+1<br />
acker<br />
n<br />
2<br />
n=0<br />
Auswertung<br />
m0 UND<br />
n0<br />
m-1<br />
acker<br />
2<br />
1<br />
3<br />
acker<br />
m-1<br />
acker<br />
m n-1<br />
acker<br />
1<br />
acker<br />
2<br />
2<br />
acker<br />
3<br />
acker<br />
acker<br />
0<br />
2<br />
2<br />
acker<br />
acker<br />
acker<br />
Abbildung 5: : Dynamisches Datenflussdiagramm für die Ackermannfunktion<br />
2<br />
1<br />
2<br />
2<br />
acker<br />
1<br />
acker<br />
2 0<br />
acker<br />
acker<br />
usw.<br />
Die folgende Tabelle zeigt anhand einiger Werte für ausgewählte Argumente, dass die Ackermannfunktion<br />
enorm schnell wächst. Dieses sehr schnelle Wachstum (verb<strong>und</strong>en mit der<br />
langen Auswertungsfolge) ist auch genau der Gr<strong>und</strong> für das Interesse an dieser Funktion. Es<br />
führt dazu, dass sich die Anzahl der notwendigen Berechnungen nicht vor dem jeweiligen<br />
Aufruf durch eine Konstante abschätzen lässt. Daher gehört die Ackermannfunktion nicht zur<br />
Klasse der primitiv rekursiven Funktionen, deren Wert sich (imperativ) durch Verwendung<br />
von Wiederholungen mit fester Wiederholungszahl berechnen lässt. Mehr darüber erfahren<br />
Sie im Modul „Theoretische Informatik“ unter dem Stichwort „Klassen berechenbarer Funktionen“.
Peter Hubwieser Ablaufmodellierung WS 2002/03<br />
KAPITEL 7: <strong>Funktionaler</strong> <strong>Programmierstil</strong> (2) LERNINHALTE V 1.0 Seite 6<br />
acker(m,n) n = 0 n = 1 n = 2 n = 3<br />
m = 0 1 2 3 4<br />
m = 1 2 3 4 5<br />
m = 2 3 5 7 9<br />
m = 3 5 13 29 61<br />
m = 4 13 65533 2 65536 -3 2 265536 -3<br />
m = 5 65533 acker(4,65533) acker(4,acker(4,65533)) acker(4,acker(5,2))<br />
m = 6 acker(4,65533) acker(5,acker(4,65533)) acker(5,acker(6,1)) acker(5,acker(6,2))<br />
(nach http://www-users.cs.york.ac.uk/~susan/cyc/a/ackermnn.htm)<br />
<strong>7.7</strong>.4 Verschränkte <strong>Rekursion</strong><br />
Die bisherigen Betrachtungen legen den Schluss nahe, dass eine Funktionsdeklaration genau<br />
dann als rekursiv bezeichnet werden kann, wenn sich in ihrem Rumpf mindestens ein Aufruf<br />
derselben Funktion findet. Leider werden damit nicht alle rekursiven Funktionen erfasst, denn<br />
es gibt noch die Möglichkeit, dass eine Funktion einen rekursiven Aufruf gewissermaßen im<br />
Umweg über den Aufruf einer anderen Funktion (oder sogar einer Folge solcher Aufrufe)<br />
„versteckt“. Wenn die <strong>Rekursion</strong> über mehrere Stufen laufen soll, muss diese andere Funktion<br />
(bzw. die letzte in der Folge der anderen aufgerufenen Funktionen) wiederum einen Aufruf<br />
der ursprünglichen Funktion beinhalten. Ein System solcher Funktionen heißt dann verschränkt<br />
rekursiv.<br />
Beispiel (gerade oder ungerade Zahlen):<br />
Intuitiv kann man die Eigenschaft einer natürlichen Zahl n≥1, gerade oder ungerade zu sein,<br />
folgendermaßen definieren:<br />
• 1 ist ungerade,<br />
• n>1 ist genau dann gerade, wenn n-1 ungerade ist.<br />
• n>1 ist genau dann ungerade, wenn n-1 gerade ist.<br />
Daraus lässt sich nun leicht ein System aus zwei verschränkt rekursiven Funktionen ableiten:<br />
function gerade (nat n): bool<br />
return if n = 1 then false else ungerade(n-1) fi<br />
function ungerade (nat n): bool<br />
return if n = 1 then true else gerade(n-1) fi<br />
Für den Wert n=3 erhält man folgende Aufrufe:<br />
gerade(3) = ungerade(2) = gerade(1) = false<br />
ungerade(3) = gerade(2) = ungerade(1) = true<br />
Ein System verschränkt rekursiver Funktionen liegt also vor, falls die Deklarationen dieser<br />
Funktionen zu einer zyklischen Folge von Aufrufen führen. Dies kann man anhand des Stützgraphen<br />
dieses Systems feststellen. Dabei werden die Funktionen als Knoten des Graphen<br />
aufgefasst <strong>und</strong> Aufrufe von Funktionen (im Rumpf von Deklarationen) als gerichtete Kanten.
Peter Hubwieser Ablaufmodellierung WS 2002/03<br />
KAPITEL 7: <strong>Funktionaler</strong> <strong>Programmierstil</strong> (2) LERNINHALTE V 1.0 Seite 7<br />
Beispiel 1 (Fakultät):<br />
-<br />
fak *<br />
=<br />
Beispiel 2 (gerade/ungerade):<br />
gerade<br />
-<br />
=<br />
ungerade<br />
<strong>Rekursion</strong> tritt in einem System von Funktionen genau dann auf, wenn der Stützgraph dieses<br />
Systems einen Zyklus beinhaltet. Verschränkt ist eine <strong>Rekursion</strong> genau dann, wenn ihr Zyklus<br />
mehr als eine Funktion beinhaltet.<br />
7.8 Funktionen höherer Ordnung<br />
Einer der wesentlichen Vorteile funktionaler Programmiersprachen ist die Möglichkeit, Funktionen<br />
ähnlich zu behandeln wie „normale“ Datentypen. Funktionen werden in funktionalen<br />
Sprachen als (mit allen anderen gleichberechtigte) Datenobjekte angesehen. Daher können<br />
Funktionen auch als Argumente oder Rückgabewerte anderer Funktionen verwendet werden,<br />
was viele Berechnungen stark vereinfacht. Funktionen, die andere Funktionen als Argumente<br />
verwenden oder zurückgeben, heißen Funktionen höherer Ordnung oder Funktionale.<br />
Funktionale Programmiersprachen heißen übrigens u.a. auch deswegen so, weil sie Funktionen<br />
ebenso als „Daten erster Ordnung“ behandeln wie alle andere Sorten.<br />
Ein Beispiel dazu aus der Mathematik:<br />
Der Ableitungsoperator abl (meist durch ´ bzw. d/dx bei Ableitung nach x symbolisiert) ist<br />
ein Funktional, das als Eingabe eine Funktion erhält <strong>und</strong> deren Ableitungsfunktion zurückliefert,<br />
z.B. angewandt auf die Quadratfunktion:<br />
f(x) = x 2 ; abl(f(x)) = f´(x) = 2x. Hier wird also jede (differenzierbare) Funktion f(x) auf ihre<br />
Ableitungsfunktion f´(x) abgebildet:<br />
abl: f(x) → f´(x).<br />
7.8.1 Funktionen als Argumente<br />
Zur Erklärung der Verwendung von Funktionen als Argumente anderer Funktionen wollen<br />
wir einige „Universalfunktionen“ auf Listen behandeln, die in den meisten funktionalen Sprachen<br />
bereits vordefiniert sind.
Peter Hubwieser Ablaufmodellierung WS 2002/03<br />
KAPITEL 7: <strong>Funktionaler</strong> <strong>Programmierstil</strong> (2) LERNINHALTE V 1.0 Seite 8<br />
Beispiel (Anwendung einer Funktion auf eine Liste):<br />
Sehr praktisch ist das in vielen funktionalen Sprachen eingebaute Funktional map, das als<br />
Argumente eine Funktion f <strong>und</strong> eine Liste lin erhält <strong>und</strong> eine Liste lout zurückliefert, die<br />
durch die Anwendung der Funktion f auf alle Elemente der Liste lin entsteht, z.B. mit f = sin<br />
<strong>und</strong> lin = [pi/3,pi/2,pi]:<br />
map (sin, [pi/3,pi/2,pi]) = [0.866025, 1.0, -8.74228e-008]<br />
Zur Definition vom Funktionalen in FPPS müssen wir die Syntax der Funktionsdeklaration<br />
erweitern: Wir lassen nun auch Funktionssorten in den Parameterlisten <strong>und</strong> im Ergebnis der<br />
Funktion zu. Dabei soll z.B. nat → nat eine Funktion symbolisieren, die natürliche Zahlen als<br />
Argumente übernimmt <strong>und</strong> Werte dieser Sorte zurückliefert. Damit könnte man die Funktion<br />
map (zunächst eingeschränkt auf natürliche Zahlen) folgendermaßen definieren (die Sorte<br />
natlist wurde in 7.5.1. definiert):<br />
function map (nat → nat f, natlist lin): natlist<br />
return if isempty(lin) then empty else natlist(f(lin.head), map(f, lin.rest)) fi<br />
Meist spielt es allerdings keine Rolle, welcher Sorte die Elemente der Liste lin angehören,<br />
solange nur die Funktion f darauf anwendbar ist. Wir müssen daher in der obigen Definition<br />
noch Sortenparameter einführen (siehe auch Abschnitt 7.6):<br />
function map ( → f, list lin): list<br />
return if isempty(lin) then empty else list(f(lin.head), map(f, lin.rest)) fi<br />
Die passenden Sorten für die Platzhalter bzw. werden bei der Anwendung von<br />
map dann automatisch richtig gewählt werden, wie das folgende Beispiel zeigt:<br />
Wir verwenden dabei eine vordefinierte Funktion ord:<br />
function ord (char zn): nat code<br />
return // Rückgabe der ASCII-Codenummer des Zeichens zn<br />
Damit setzt die Anwendung<br />
map (ord, [´A´, ´B´, ´C´, ´D´])<br />
die Sorte auf char bzw. auf nat <strong>und</strong> liefert folgenden Wert zurück:<br />
[65, 66, 67, 68].<br />
Funktionen, die (wie map) mit Hilfe von Sortenparametern definiert sind <strong>und</strong> daher auf verschiedene<br />
Sorten angewandt werden (<strong>und</strong>/oder auch verschiedene Sorten zurückliefern) können,<br />
heißen polymorph.<br />
Weitere Beispiele für Funktionen, die andere Funktionen als Parameterwerte übernehmen,<br />
sind:<br />
Beispiel 1 (Filterfunktion):<br />
Diese Funktion filtert aus einer Liste alle Elemente, die ein bestimmtes Prädikat (das ist eine<br />
Funktion mit booleschem Rückgabewert) erfüllen:<br />
function filter( → bool f, liste lin): liste
Peter Hubwieser Ablaufmodellierung WS 2002/03<br />
KAPITEL 7: <strong>Funktionaler</strong> <strong>Programmierstil</strong> (2) LERNINHALTE V 1.0 Seite 9<br />
return if isempty(lin)<br />
then empty<br />
else if f(lin.head)<br />
then list(f(lin.head), filter(f, lin.rest))<br />
else filter(f, lin.rest)<br />
fi<br />
fi<br />
z.B. kann man damit (unter Verwendung der in <strong>7.7</strong>.4 definierten Funktion gerade) die geraden<br />
Zahlen aus einer beliebigen Liste von Zahlen extrahieren:<br />
filter(gerade, [1,2,3,4,5,6,7,8,9,10]) = [2,4,6,8,10]<br />
Beispiel 2 (Faltung):<br />
Diese Funktion verknüpft die Elemente einer Liste mit Hilfe eines zweistelligen Operators.<br />
Verwendet man als Operator z.B. die Addition, so liefert diese Funktion die Summe aller Elemente<br />
der Liste. Dabei muss neben der jeweiligen Operation (als zweistellige Funktion) <strong>und</strong><br />
der Liste der Werte auch das neutrale Element der jeweiligen Operation (z.B. 0 bei Addition<br />
bzw. 1 bei Multiplikation) als Wert der leeren Liste übergeben werden:<br />
function fold((×) → f, neutral, list lin): <br />
return if isempty(lin.rest)<br />
then neutral<br />
else f(lin.head, fold(f, neutral, lin.rest))<br />
fi<br />
Eine beispielhafte Anwendung (unter Verwendung einer Funktion mult(a,b) = a*b):<br />
fold(mult, 1, [1,2,3,4,5]) = 1*2*3*4*5 = 120<br />
Beispiel 3 (Differenzenquotient):<br />
Der Differenzenquotient (f(x + ∆x) – f(x))/∆x liefert für sehr kleine ∆x eine gute Näherung<br />
für die Ableitung f´(x) einer differenzierbaren Funktion f an der Stelle x. Er stellt auch die<br />
Basis für die Definition der Ableitungsfunktion dar (nach Grenzübergang ∆x → 0):<br />
function diffquot(real → real f, real x, deltax): real<br />
return (f(x + deltax) – f(x))/deltax<br />
7.8.2 Funktionen als Funktionswerte<br />
Damit haben wir geklärt, wie eine Funktion als Argument übernommen werden kann. In funktionalen<br />
Sprachen kann man meist auch Funktionen definieren, die andere Funktionen als<br />
Werte zurückliefern. Diese Erzeugung als Werte anderer Funktionen stellt neben der expliziten<br />
Definition eine weitere Möglichkeit dar, neue Funktionen einzuführen, die manchmal<br />
kompakter, übersichtlicher leichter verifizierbar oder einfach nur eleganter ist.<br />
Beispiele wären z.B. die Verkettungsfunktion, die zwei Funktionen zu einer dritten kombiniert<br />
oder die Integration bzw. Ableitung (Differentiation) von Funktionen.<br />
Lässt man Funktionen als Ausgabewerte zu, so ergibt sich daraus auch die Möglichkeit der<br />
Verkettung von Funktionalen, indem eine Funktion als Ausgabe eines Funktionals als Wert an
Peter Hubwieser Ablaufmodellierung WS 2002/03<br />
KAPITEL 7: <strong>Funktionaler</strong> <strong>Programmierstil</strong> (2) LERNINHALTE V 1.0 Seite 10<br />
ein anderes Funktional übergeben wird. Ein Beispiel dafür wäre die Anwendung vom map auf<br />
die Verkettung zweier Funktionen.<br />
Im Wesentlichen kann man in funktionalen Programmiersprachen Funktionen als Ausgabewerte<br />
auf zwei verschiedene Arten erzeugen:<br />
1) durch Verwendung von vordefinierten Funktionaloperatoren oder<br />
2) durch partielle Anwendung einer Funktion auf einen <strong>Teil</strong> ihrer Argumente.<br />
Funktionaloperatoren sind Operatoren, die Funktionen als Argumente verwenden <strong>und</strong>/oder<br />
Funktionen als Werte zurückliefern, wie z.B. die Funktionskomposition (Hintereinanderausführung)<br />
zweier Funktionen f <strong>und</strong> g (z.B. in Haskell durch f.g symbolisiert) :<br />
function komp(→ f, → g): → <br />
return // Die Funktion, die für alle Werte x, für die g definiert ist, den Wert f(g(x)) liefert<br />
Damit kann man nun z.B. die n-fache Iteration einer Funktion (n-fache Komposition) definieren:<br />
function iter( → f, n): → <br />
return if n =1 then f else komp(f, (iter(f, n-1)) fi<br />
Beispiel (Zweierpotenz als iterierte Multiplikation mit 2):<br />
function verdopple (nat n): nat<br />
return 2*n<br />
function zweihoch(nat n): nat<br />
return iter(verdopple, n)<br />
Die zweite Möglichkeit zur Erzeugung von Funktionen als Werten ist die Anwendung einer<br />
erzeugenden Funktion auf lediglich einen <strong>Teil</strong> ihrer Argumente (partielle Anwendung). Ein<br />
Beispiel:<br />
Durch partielle Anwendung der Multiplikationsfunktion mult(a,b) auf den Wert 2 für das erste<br />
Argument kann man (alternativ zu obiger Definition) die Funktion verdopple erzeugen:<br />
function verdopple (nat n): nat<br />
return mult(2,n)<br />
In funktionalen Sprachen verwendet man in diesem Zusammenhang oft das Prinzip von Curry<br />
(zunächst am Beispiel einer zweistelligen Funktion auf der Sorte nat erklärt):<br />
Jede Funktion f: nat × nat → nat kann man als Funktion fC: nat → (nat → nat) auffassen:<br />
Anstatt über f(a,b) = c aus der Verknüpfung zweier Werte a,b einen dritten Wert c zu berechnen,<br />
kann man so über partielle Anwendung auf das erste Argument (für jeden seiner Werte)<br />
eine neue (einstellige) Funktion ga: nat → nat definieren:<br />
ga = fC(a) <strong>und</strong> ga(b) = (fC(a))(b) = f(a,b)<br />
Das Ergebnis der Anwendung von f auf den Wert a ist also eine neue einstellige Funktion ga,<br />
deren Wert für das Argument b identisch mit dem Wert von f(a,b) ist.
Peter Hubwieser Ablaufmodellierung WS 2002/03<br />
KAPITEL 7: <strong>Funktionaler</strong> <strong>Programmierstil</strong> (2) LERNINHALTE V 1.0 Seite 11<br />
fC heiß curried Version von f, umgekehrt heißt f uncurried Version von fC<br />
Dieses Prinzip wurde (wie übrigens auch die Sprache Haskell) nach dem amerikanischen Logiker<br />
Haskell B. Curry (1900–1982) benannt, obwohl es eigentlich zuerst vom deutschen Mathematiker<br />
M. Schönfinkel 1924 vorgeschlagen wurde. „Schönfinkeln“ klingt wohl etwas<br />
seltsam.<br />
Anstatt fC: nat → (nat → nat) schreibt man auch kurz fC: nat → nat → nat<br />
Der Vollständigkeit halber sei hier noch das Prinzip von Curry für eine beliebige Anzahl von<br />
Argumenten formuliert:<br />
Jede Funktion<br />
f: × × ... → lässt sich auch als curried Version darstellen:<br />
fC: → → ... → .<br />
Damit kann man jede Funktion auf eine Funktion mit höchstens einem Argument reduzieren<br />
<strong>und</strong> die Klammer bei der Anwendung daher immer weglassen:<br />
Statt<br />
f(a1, a2, a3, .., an)<br />
schreibt man in der curried Version<br />
fC a1 a2 a3 .. an<br />
<strong>und</strong> meint damit die Anwendung der Funktion, die sich aus der partiellen Anwendung von f<br />
auf a1, a2, a3, .., an-1 ergibt, auf an .<br />
Diese Sichtweise erlaubt nun sehr leicht die partielle Anwendung, wie in folgenden Beispielen:<br />
1) Multiplikation mit 2 (verdopple) entsteht durch Currying der normalen zweistelligen Multiplikation<br />
mult: nat × nat → nat mult(a,b) = c (unsere Ausgangsfunktion f)<br />
multC: nat → nat → nat mult a b = c (curried Version fC)<br />
verdopple: nat → nat verdopple b = mult 2 b<br />
2) Die Nachfolgerfunktion succ(n) kann durch partielle Anwendung der Addition add(a,b) auf<br />
das erste Argument dargestellt werden:<br />
add: nat × nat → nat add(a,b) = c (unsere Ausgangsfunktion f)<br />
addC: nat → nat → nat add a b = c (curried Version fC)<br />
succ: nat → nat succ b = add 1 b