Funktionale Programmierung ALP I Die Natur rekursiver Funktionen SS 2011 Prof. Dr. Margarita Esponda Prof. Dr. Margarita Esponda Funktionale Programmierung Die Natur rekursiver Funktionen Rekursive Funktionen haben oft folgende allgemeine Form: f :: a -> a f 0 =c f (n+1) = h (f n ) Diese Art der Definitionen wird oft als Strukturelle Rekursion über die natürlichen Zahlen bezeichnet. Prof. Dr. Margarita Esponda Funktionale Programmierung Die Natur rekursiver Funktionen Eine Funktionsdefinition dieser Form über die natürlichen Zahlen sieht aus wie folgt: Sei 1+1+1+…+1+0 = die natürliche Zahl n. Wenn wir die 0 mit c und (1+) mit h ersetzen, bekommen wir folgenden Ausdruck h(h(…(h(c)…), in dem h n-mal auf c = f(0) angewendet wird. Prof. Dr. Margarita Esponda Funktionale Programmierung Die Natur rekursiver Funktionen Folgende Faltungsfunktion stellt eine Verallgemeinerung der Funktionen mit dieser einfachen Grundform dar: data Natural = Zero | S Natural deriving (Eq, Show) fold :: (a->a) -> a -> Natural -> a fold h c Zero = c fold h c (S n) = h (fold h c n) Prof. Dr. Margarita Esponda Funktionale Programmierung Rekursionsarten Lineare Rekursion Rekursive Funktionen, die in jedem Zweig ihrer Definition maximal einen rekursiven Aufruf beinhalten, werden als linear rekursiv bezeichnet. Beispiel: f 0 = 1 f n | n<0 = 2 * (f (n+1)) | otherwise = 3 * (f (n-1)) Prof. Dr. Margarita Esponda Funktionale Programmierung Rekursionsarten Endrekursion (tail recursion) Linear rekursive Funktionen werden als endrekursive Funktionen klassifiziert, wenn der rekursive Aufruf in jedem Zweig der Definition die letzte Aktion zur Berechnung der Funktion ist. Das bedeutet, dass keine weiteren Operationen nach der Auswertung der Rekursion berechnet werden müssen . Prof. Dr. Margarita Esponda Endrekursion Beispiel: Eine nicht endrekursive Funktion ist folgende Definition der Fakultätsfunktion: factorial 0 = 1 factorial n = n * factorial (n-1) Ablauf einer Berechnung: factorial 6 => => => => => => => => => => => => => Prof. Dr. Margarita Esponda Der Ausführungsstapel wächst bei jedem rekursiven Aufruf und Teilausdrücke müssen ständig zwischengespeichert werden. 6 * factorial 5 6 * (5 * factorial 4) 6 * (5 * (4 * factorial 3)) 6 * (5 * (4 * (3 * factorial 2))) 6 * (5 * (4 * (3 * (2 * factorial 1)))) 6 * (5 * (4 * (3 * (2 * (1 * factorial 0))))) 6 * (5 * (4 * (3 * (2 * (1 * 1))))) 6 * (5 * (4 * (3 * (2 * 1)))) 6 * (5 * (4 * (3 * 2))) 6 * (5 * (4 * 6)) Die Endberechnungen finden erst 6 * (5 * 24) beim Abbau des Ausführungsstapels 6 * 120 statt. 720 Endrekursion Beispiel einer endrekursiven Definition der Fakultätsfunktion quickFactorial n = factorial_helper 1 n where factorial_helper a 0 = a factorial_helper a n = factorial_helper (a*n) (n-1) Ablauf einer Berechnung: quickFactorial 6 => => => => => => => => factorial_helper factorial_helper factorial_helper factorial_helper factorial_helper factorial_helper factorial_helper 720 1 6 6 5 30 4 120 3 360 2 720 1 720 0 Es müssen keine Zwischenausdrücke gespeichert werden. Endrekursive Funktionen können aus diesem Grund oft vom Übersetzer (Compiler) optimiert werden, indem diese in einfache Schleifen verwandelt werden. Funktionale Programmierung Beispiele endrekursiver Funktionen Klassisches Beispiel einer nicht endrekursiven Definition ist: Die Standarddefinition der reverse-Funktion rev :: [a] -> [a] rev [] = [] rev (x:xs) = rev xs ++ [x] Berechnungsaufwand von rev: Reduktionen rev [x1, x2, …, xn] => rev [x2, …, xn] ++ [x1] => rev [x3, …, xn] ++ [x2] ++ [x1] 1 1 ... => [xn] ++ [xn-1] ++ [x2] ++ [x1] 1 => [] ++ [xn] ++ … ++ [x2] ++ [x1] 1 bis hier (n+1) Reduktionen! Prof. Dr. Margarita Esponda Funktionale Programmierung Berechnungsaufwand von rev bis hier (n+1) Reduktionen! (++) :: [a] -> [a] -> [a] (++) [] ys = ys (++) (x:xs) ys = x:(xs ++ ys) Reduktionen => [] ++ [xn] ++ [xn-1] ++ … ++ [x2] ++ [x1] => [xn] ++ [xn-1] ++ … ++ [x2] ++ [x1] 1 => [xn, xn-1] ++ … ++ [x2] ++ [x1] 2 => [xn, xn-1 ,xn-2] ++ … ++ [x2] ++ [x1] 3 => . . . . => [xn, xn-1 , … ,x1] n Die gesamte Anzahl der Reduktionen ist: Prof. Dr. Margarita Esponda Quadratischer Ausführungsaufwand! Funktionale Programmierung Eine effizientere Version von rev quickRev xs = rev_helper xs [] where rev_helper [] ys = ys rev_helper (x:xs) ys = rev_helper xs (x:ys) Berechnungsaufwand: Reduktionen quickRev [x1, x2, …, xn] => rev_helper [x1,…,xn] [] 1 => rev_helper [x2,…,xn] (x1:[]) 1 n => rev_helper [x3,…,xn] (x2:x1:[]) 1 ... => (xn:, … ,x2:x1:[]) … 1 => (xn:, … ,x2:[x1]) 1 ... => (xn:, … , x3:[x2,x1]) … 1 lineare Komplexität Prof. Dr. Margarita Esponda 2n = O(n) n Funktionale Programmierung Wichtiges Beispiel von Endrekursion foldl-Funktion: foldl :: (b -> a -> b) -> b -> [a] -> b foldl f z [] = z foldl.1 foldl f z (x:xs) = foldl f (f z x) xs foldl.2 Hier werden Zwischenergebnisse akkumuliert und weitergeleitet. Mit Hilfe von Faltungs-Operatoren können sehr leicht endrekursive Funktionen definiert werden. Beispiel: reverse_reloaded = foldl (flip (:)) [] flip :: (a -> b -> c) -> b -> a -> c flip f x y Prof. Dr. Margarita Esponda = fyx Funktionale Programmierung Berechnungsverlauf reverse_reloaded [x1, x2, … , xn] foldl.2 => foldl (flip (:)) [] [x1, x2,…, xn] => foldl (flip (:)) ((flip (:)) [] x1) [x2,x3,…, xn] => foldl (flip (:)) ((:) x1 []) [x2,x3,…, xn] => foldl (flip (:)) (x1:[]) [x2,x3,…, xn] => foldl (flip (:)) [x1] [x2,x3,…, xn] foldl.2 => foldl (flip (:)) ((flip (:)) [x1] x2) [x3,…, xn] => foldl (flip (:)) ((:) x2 [x1]) [x3,…, xn] => foldl (flip (:)) (x2:[x1]) [x3,…, xn] foldl.2 Prof. Dr. Margarita Esponda => foldl (flip (:)) [x2, x1] [x3,…, xn] => . . . Funktionale Programmierung Berechnung der Fibonacci-Zahlen Wie viele Reduktionsschritte brauchen wir, um fib n zu berechnen? fib 0 1 fib 1 + 1 1 = + fib 2 2 2 2 fib 3 fib 4 5 3 = + fib 5 fib 6 8 13 Reduktionen 3 3 3 = + 5 5 3 = + 8 5 = 13 Die Anzahl der Reduktionen für fib n ist gleich fib (n+1) Die rekursive Berechnung der FibonacciZahlen hat eine exponentielle Komplexität n O( (1,618...) ) Prof. Dr. Margarita Esponda Funktionale Programmierung Berechnung der Fibonacci-Zahlen 2. Lösung Endrekursive Funktion quickFib funktioniert nur, wenn diese mit den ersten zwei Fibonacci-Zahlen gestartet wird. fib' n = quickFib 0 1 n where quickFib a b 0 = a Zähler quickFib.1 quickFib a b n = quickFib b (a+b) (n-1) quickFib.2 Innerhalb jedes rekursiven Aufrufs wird eine neue Fibonacci-Zahl berechnet und der Zähler verkleinert. Die neue Zahl und ihr Vorgänger werden beim nächsten rekursiven Aufruf als Parameter weitergegeben. Anzahl der Reduktionen Für die Berechnung von quickFib n benötigen wir n Reduktionen, d.h. wir haben nur einen linearen Aufwand O(n) Prof. Dr. Margarita Esponda Funktionale Programmierung Eine effiziente tree2list-Funktion Noch ein Beispiel für die Verbesserung der Effizienz einer Funktion ist die Funktion tree2List. data Tree = Nil | Leaf Int | Node Int Tree Tree tree2list :: Tree -> [Int] tree2list Nil = [] tree2list (Leaf n) = [n] tree2list (Node n l r) = tree2list l ++ [n] ++ tree2list r Die Verwendung der (++)-Funktion verursacht wieder einen quadratischen Berechnungsaufwand. Prof. Dr. Margarita Esponda Funktionale Programmierung Eine effiziente tree2list-Funktion Hier benutzen wir die gleiche Technik wie bei der rev-Funktion, indem wir eine Art Akkumulator für Zwischenergebnisse einbauen: data Tree = Nil | Leaf Int | Node Int Tree Tree tree2list' :: Tree -> [Int] tree2list' t = tree2liste' t [] tree2liste' :: Tree -> [Int] -> [Int] tree2liste' Nil ns = ns tree2liste' (Leaf n) ns = n:ns tree2liste' (Node n l r) ns = (tree2liste' l (n:(tree2liste' r ns))) Prof. Dr. Margarita Esponda