21.07.2013 Aufrufe

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 ...

MEHR ANZEIGEN
WENIGER ANZEIGEN

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

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!