3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: Funktionsabstraktion (3) 3. Abstraktion über Funktionsbezeichner: Ausdruck: Abstraktion: f (f x) \ f x -> f (f x) Mit Bezeichnervereinbarung: twice = \ f x -> f (f x) erg = ( twice sqrt) 3.0 Äquivalente Vereinbarung: twice2 = \ f -> \ x -> f (f x) erg = ( twice sqrt) 3.0 ©Arnd Poetzsch-Heffter TU Kaiserslautern 217 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Funktionsdeklaration In Haskell gibt es unterschiedliche syntaktische Formen für die Funktionsdeklaration: 1. mittels direkter Wertvereinbarung: <Funktionsbez > = <Ausdruck von Funktionstyp > Beispiel: fib = \ n -> if n == 0 then 0 else if n == 1 then 1 else fib (n -1) + fib (n -2) ©Arnd Poetzsch-Heffter TU Kaiserslautern 218 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Funktionsdeklaration (2) 2. mittels einem oder mehreren formalen Parametern: <Funktionsbez > <Parameterbez1 > ... = <Ausdruck > Beispiel: fib n = if n == 0 then 0 else if n == 1 then 1 else fib (n -1) + fib (n -2) ©Arnd Poetzsch-Heffter TU Kaiserslautern 219 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Funktionsdeklaration (3) 3. mittels formalen Parametern und Fallunterscheidung über Wächtern: <Funktionsbez > <Parameterbez1 > ... | <boolscher Ausdruck > = <Ausdruck > ... | <boolscher Ausdruck > = <Ausdruck > Die boolschen Ausdrücke in der Deklaration heißen Wächter, engl. guards. Beispiel: fib | | | n n == 0 = 0 n == 1 = 1 otherwise = fib (n -1) + fib (n -2) Das Schlüsselwort otherwise steht hier für True. ©Arnd Poetzsch-Heffter TU Kaiserslautern 220 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Funktionsdeklaration (4) 4. mittels Fallunterscheidung über Mustern: <Funktionsbez > <Parametermuster > ... = ... <Funktionsbez > <Parametermuster > ... = <Ausdruck > <Ausdruck > Muster sind ein mächtiges Programmierkonstrukt, das weiter unten genauer behandelt wird. Beispiel: fib 0 = 0 fib 1 = 1 fib n = fib (n -1) + fib (n -2) ©Arnd Poetzsch-Heffter TU Kaiserslautern 221 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Funktionsdeklaration (5) 5. mittels Kombinationen der Formen 3 und 4. Beispiel: fib fib | | 0 = 0 n n==1 = 1 otherwise = fib (n -1) + fib (n -2) ©Arnd Poetzsch-Heffter TU Kaiserslautern 222 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bemerkungen: • Jeder Funktionsdeklaration sollte die Funktionssignatur vorangestellt werden und ein Kommentar, der mindestens die Voraussetzungen an die Parameter beschreibt. Beispiel: fib :: Integer -> Integer -- fib k verlangt : k >= 0 fib 0 = 0 fib 1 = 1 fib n = fib (n -1) + fib (n -2) • Die Form einer Funktionsdeklaration sollte so gewählt werden, dass die Deklaration gut lesbar ist. ©Arnd Poetzsch-Heffter TU Kaiserslautern 223 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: rekursive Funktionsdekl. 1. Einzelne rekursive Funktionsdeklaration: rpow :: Float -> Integer -> Float -- rpow r m verlangt : m >= 0 rpow r n = if n == 0 then 1.0 else r * rpow r (n -1) 2. Verschränkt rekursive Funktionsdeklaration: gerade :: Integer -> Bool ungerade :: Integer -> Bool -- Bedingung an Parameter n bei beiden Funktionen : -- n >= 0 gerade n = (n == 0) || ungerade (n -1) ungerade n = if n == 0 then False else gerade (n -1) ©Arnd Poetzsch-Heffter TU Kaiserslautern 224 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Deklaration rekursiver Funktionen Begriffsklärung: (rekursive Definition) Eine Definition oder Deklaration nennt man rekursiv, wenn der definierte Begriff bzw. das deklarierte Programmelement im definierenden Teil verwendet wird. Bemerkung: • Rekursive Definitionen finden sich in vielen Bereichen der Informatik und Mathematik, aber auch in anderen Wissenschaften und der nichtwissenschaftlichen Sprachwelt. • Wir werden hauptsächlich rekursive Funktions- und Datentypdeklarationen betrachten. ©Arnd Poetzsch-Heffter TU Kaiserslautern 225 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Definition: (rekursive Funktionsdekl.) Eine Funktionsdeklaration heißt direkt rekursiv, wenn der definierende Ausdruck eine Anwendung der definierten Funktion enthält. Eine Menge von Funktionsdeklarationen heißt verschränkt rekursiv oder indirekt rekursiv (engl. mutually recursive), wenn die Deklarationen gegenseitig voneinander abhängen. Eine Funktionsdeklaration heißt rekursiv, wenn sie direkt rekursiv ist oder Element einer Menge verschränkt rekursiver Funktionsdeklarationen ist. ©Arnd Poetzsch-Heffter TU Kaiserslautern 226 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: (rekursive Funktion) Eine Funktion heißt rekursiv, wenn es rekursive Funktionsdeklarationen gibt, mit denen sie definiert werden kann. Bemerkungen: • Die Menge der rekursiven Funktionen ist berechnungsvollständig. • Rekursive Funktionsdeklarationen können als eine Gleichung mit einer Variablen verstanden werden, wobei die Variable von einem Funktionstyp ist: Beispiel: Gesucht ist die Funktion f , die folgende Gleichung für alle n ∈ Nat erfüllt: f n = if n = 0 then 1 else n ∗ f (n − 1) ©Arnd Poetzsch-Heffter TU Kaiserslautern 227 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Zur Auswertung von Funktionsanwendungen: Sei f x = A[x] ; Eine Funktionsanwendungen f e kann nach unterschiedlichen Strategien durch Verwendung der Deklarationsgleichungen ausgewertet werden, zum Beispiel call-by-value: • Werte Ausdruck e aus; Ergebnis nennt man den aktuellen Parameter z . • Ersetze x in A[x] durch z . • Werte den resultierenden Ausdruck A[z] aus. Haskell benutzt die Auswertungsstrategie call-by-need (siehe 3.4). Beispiele: (Rekursion) siehe Vorlesung ©Arnd Poetzsch-Heffter TU Kaiserslautern 228 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: (lineare/repetitive Rekursion) Vereinfachend betrachten wir hier nur Funktionsdeklarationen, bei denen die Fallunterscheidung „außen“ und die rekursiven Aufrufe in den Zweigen der Fallunterscheidung stehen. • Eine rekursive Funktionsdeklaration heißt linear rekursiv, wenn in jedem Zweig der Fallunterscheidung höchstens eine rekursive Anwendung erfolgt (Beispiel: Definition von fac). • Eine rekursive Funktionsdeklaration heißt repetitiv (rekursiv), wenn sie linear rekursiv ist und die rekursiven Anwendungen in den Zweigen der Fallunterscheidung an äußerster Stelle stehen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 229 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: • Die übliche Definition von fac ist nicht repetitiv, da im Zweig der rekursiven Anwendung die Multiplikation an äußerster Stelle steht. • Die folgende Funktion facrep ist repetitiv: facrep :: Integer -> Integer -> Integer -- facrep n res verlangt : n >= 0 && res >= 1 facrep n res = if n == 0 then res else facrep (n -1) (res*n) fac n = facrep n 1 ©Arnd Poetzsch-Heffter TU Kaiserslautern 230 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärung: (Geschachtelte Rekursion) • Eine rekursive Funktionsdeklaration für f heißt geschachtelt rekursiv, wenn sie Teilausdrücke der Form f (. . . f (. . . ) . . . ) enthält. • Eine rekursive Funktionsdeklaration für f heißt kaskadenartig rekursiv, wenn sie Teilausdrücke der Form h(. . . f (. . . ) . . . f (. . . ) . . . ) enthält. ©Arnd Poetzsch-Heffter TU Kaiserslautern 231 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (kaskadenartige Rekursion) Berechne: Wie viele Kaninchen-Pärchen leben nach n Jahren, wenn man • am Anfang mit einem neu geborenden Pärchen beginnt, • jedes neu geborene Pärchen nach zwei Jahren und dann jedes folgende Jahr ein weiteres Pärchen Nachwuchs erzeugt und • die Kaninchen nie sterben. ©Arnd Poetzsch-Heffter TU Kaiserslautern 232 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (kaskadenartige Rekursion) (2) Die Anzahl der Pärchen stellen wir als Funktion ibo von n dar: • vpr dem 1. Jahr: ibo(0) = 1 • nach dem 1. Jahr: ibo(1) = 1 • nach dem 2. Jahr: ibo(2) = 2 • nach dem n. Jahr: die Anzahl aus dem Jahr vorher plus die Anzahl der im n. Jahr Geborenen; und die ist gleich der Anzahl vor zwei Jahren, also: ibo n = ibo(n − 1) + iob(n − 2) für n > 1. Insgesamt ergibt sich folgende kaskadenartige Funktionsdeklaration: ibo n = ©Arnd Poetzsch-Heffter if n<=1 then 1 else ibo (n -1) + ibo (n -2) TU Kaiserslautern 233 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Bemerkung: • Aus Beschreibungssicht spielt die Form der Rekursion keine Rolle; wichtig ist eine möglichst am Problem orientierte Beschreibung. • Aus Programmierungssicht spielt Auswertungseffizienz eine wichtige Rolle, und diese hängt von der Form der Rekursion ab. Beispiel: Kaskadenartige Rekursion führt im Allg. zu einer exponentiellen Anzahl von Funktionsanwendungen (z.B. bei ibo 30 bereits 1.664.079 Anwendungen). ©Arnd Poetzsch-Heffter TU Kaiserslautern 234 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Unterabschnitt 3.1.3 Listen und Tupel ©Arnd Poetzsch-Heffter TU Kaiserslautern 235 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der Listen Eine Liste über einem Typ T ist eine total geordnete Multimenge mit Elementen aus T (bzw. eine Folge, d.h. eine Abb. Nat -> T ). Eine Liste heißt endlich, wenn sie nur endlich viele Elemente enthält. Haskell stellt standardmäßig eine Datenstruktur für Listen bereit, die bzgl. des Elementtyps parametrisiert ist. Typparameter werden üblicherweise geschrieben als a, b, ... ©Arnd Poetzsch-Heffter TU Kaiserslautern 236 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der Listen (2) Typ: [a] , a ist Typparameter Funktionen: (==), (/=) (:) (++) head, last tail, init null length (!!) take, drop :: :: :: :: :: :: :: :: :: [a] → [a] → Bool a → [a] → [a] [a] → [a] → [a] [a] → a [a] → [a] [a] → Bool [a] → Int [a] → Int → a Int → [a] → [a] wenn (==) auf a definiert Konstanten: [] ©Arnd Poetzsch-Heffter :: [a] TU Kaiserslautern 237 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der Listen (3) Dem Typ [a] ist als Wertemenge die Menge aller Listen über Elementen vom Typ a zugeordnet. Notation: In Haskell gibt es eine vereinfachende Notation für Listen: statt x1 : x2 : ... : xn : [] kann man schreiben: [ x1 , x2 , ..., xn ] ©Arnd Poetzsch-Heffter TU Kaiserslautern 238 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Funktionen auf Listen) 1. Addiere alle Zahlen einer Liste von Typ [Int]: mapplus :: [Int] -> Int mapplus xl = if null xl then 0 else (head xl) + mapplus (tail xl) mapplus [1 ,2 ,3 ,4 ,5 ,6] 2. Prüfen einer Liste von Zahlen auf Sortiertheit: ist_sortiert :: [Int] -> Bool ist_sortiert xl = if null xl || null (tail xl) then True else if (head xl)<=(head (tail xl)) then ist_sortiert (tail xl) else False ©Arnd Poetzsch-Heffter TU Kaiserslautern 239 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Funktionen auf Listen) (2) 3. Zusammenhängen zweier Listen (engl. append): append :: [a] -> [a] -> [a] append l1 l2 = if l1 == [] then l2 else (head l1):( append (tail l1) l2) 4. Umkehren einer Liste: rev :: [a] -> [a] rev xl = if null xl then [] else append (rev (tail xl)) [head xl] ©Arnd Poetzsch-Heffter TU Kaiserslautern 240