Funktionale Programmierung 31.5.2005 Das Funktionale Quiz Nenne einen Unterschied zwischen Typklassen und OO-Klassen Das Funktionale Quiz Nenne eine Gemeinsamkeit zwischen Typklassen und OO-Klassen Das Funktionale Quiz Ist es möglich, ein Haskell-Programm mit Modulen in ein Haskell-Programm ohne Module umzuformen? Ja, mittels Umbenennung 1-7 Themen heute Lazy Programming • Lazy Evaluation Lazy Evaluation: Auswertungsstrategie für Haskell • Parserkombinatoren Darauf aufbauend: Techniken zur Programmierung • Listen unendlicher Länge Auswertungsstrategie • Bis jetzt: Wert eines Ausdrucks durch Vereinfachen und Ersetzen mittels Gleichungen Beispiel trues = True:trues take 0 l = [] take n (x:xs) = x:(take (n-1) xs) take 2 trues ==> [True,True] • Reihenfolge bis jetzt nicht festgelegt Reduktionsreihenfolge die Normalform findet: • Für Implementierung aber notwendig (Strategie) take 2 trues = take 2 True:trues = True:take (2-1) trues = True:take 1 True:trues = True:True:take (1-1) trues = True:True:take 0 trues = True:True:[] • Wünschenswert: Finde Normalform, wenn sie existiert 8-11 Lazy Evaluation Gefahr • Ausdrücke werden eventuell mehrfach ausgewertet Zugrundeliegende Idee: double x = x + x • Werte Argumente nicht aus • Beim Pattern-Matching Parameter nur so weit auswerten, bis Muster passt/nicht passt Findet Normalform, wenn sie existiert Repräsentation als Graph double (9 - 3) = (9 - 3) + (9 - 3) = 6 + (9 -3) = 6 + 6 = 12 • Für das Ergebnis egal, in der Praxis untragbar • Lösung: Beim Ersetzen der Parameter anstatt der Argumente nur Zeiger auf Argumente einsetzen. Keine doppelte Auswertung • Mit Zeigern werden Ausdrücke nicht mehr als Baum (Terme) repräsentiert, sondern als Graph • Zeiger werden automatisch dereferenziert add3 x = x + 3 • Somit wird jedes Argument nur einmal ausgewertet: add3 5 = ⋅ + 3 = 8 double (9 - 3) = 5 ⋅ + ⋅ (9 - 3) = ⋅ + ⋅ = 12 6 • Wenn Zeiger vereinfacht werden muss, Ziel vereinfachen: add3 (7 - 6) = ⋅ + 3 (7 - 6) = ⋅ + 3 = 4 1 12-15 Beschreibung von Lazy-Evaluation Funktionsanwendung Betrachte allgemeinste Funktionsdefinition: Nach Art des Ausdrucks: 1. Funktionsanwendung 2. Operatoren und sonstige Syntax 3. Geschachtelte Funktionsanwendung f p1 ... pK | g1 = e1 ... | otherwise = eO where v1 a1,1 ... = r1 ... f q1 ... qK ... ... -- oder v1 = r1 Auswertung von Funktionsanwendung f a1 ... aK Drei Aspekte der Funktionsanwendung 1.1 Pattern-Matching Ziel: Gleichung auswählen Schritte zum Funktionswert: Vorgehen: 1.1. Gleichung auswählen über Pattern-Matching 1.2. Zweig auswählen mittels Guards 1.3. Lokale Definitionen berücksichtigen • Argumente a1 ... aK jeweils so weit auswerten, bis klar ist, ob sie auf p1 ... pK passen • Falls ja, erste Gleichung auswerten • Andernfalls mit zweiter Gleichung fortfahren 16-19 Beispiel: Pattern-Matching 1.2. Guards f :: [Int] -> [Int] -> Int f [] ys = 0 f (x:xs) [] = 0 f (x:xs) (y:ys) = x + y Auswertung von f [1 .. 3] [5 .. 9]: Beispiel: Guards a b c a >= b b >= c True = (51-2) 49 5*7 23 | ⋅>=⋅&&⋅>=⋅=⋅ | ⋅>=⋅=⋅ | True = ⋅ 5*7 23 | ⋅>=⋅&&⋅>=⋅=⋅ | ⋅>=⋅=⋅ | True = ⋅ 49 • Rechte Seite (mit lokalen Definitionen) auswerten | True&&⋅>=⋅=⋅ | ⋅>=⋅=⋅ | True = ⋅ | ⋅>=⋅&&⋅>=⋅=⋅ | ⋅>=⋅=⋅ | True = ⋅ 49 • Parameter durch Zeiger auf Argumente ersetzen Beispiel Guards && a >= c = a = b c (5*7) 23 51-2 Vorgehen: • Guards von oben nach unten auswerten, bis erster True ergibt f (1:[1+1 .. 3]) [5 .. 9] -- 1. Gl. passt nicht f (1:[1+1 .. 3]) (5:[5+1 .. 9]) -- 2. Gl. passt nicht -- 3. Gl. passt max | | | max Ziel: Zweig einer Gleichung auswählen 35 23 35 23 | True && True =⋅ | ⋅>=⋅=⋅ | True = ⋅ 49 35 23 | True =⋅ | ⋅>=⋅=⋅ | True = ⋅ 49 35 23 ⋅ 49 20-23 1.3. Lokale Definitionen Ziel: Ausdruck mit lokalen Definitionen auswerten Beispiel: Lokale Definitionen double x = xx where xx = x + x Vorgehen: double 5 • Ausdruck auswerten Auswertung: xx where {xx = ⋅ + ⋅} • Rechte Seite einer Definition auswerten, wenn Variable benötigt wird 5 • Lokale Definitionen bleiben also erhalten - Graph mittels Namen xx where {xx = 10} 10 2. Operatoren und sonstige Syntax 3. Geschachtelte Funktionsaufrufe • Argumente der Operatoren auswerten je nach Operator: in Haskell definierbar ( &&...) oder strikt (z.B. arithmetische Operatoren) oder so lazy wie möglich ( ==, :, ...) • Lambda-Ausdrücke wie Funktionen mit Namen Vorgehen: • Von außen nach innen f e1 (g e2) wertet f 1 (g e2) aus • Von rechts nach links f e1 + g e2 wertet erst f e1 dann g e2 aus • case wie Pattern-Matching oben 24-27 Parser-Kombinatoren mittels Lazy-Evaluation Parser Kombinator: Geschlossener Ausdruck type Parse a b = [a] -> [(b, [a])] Bezug zur Lazy-Evaluation: Dabei: • Grammatiken enthalten oft Alternativen und Sequenzen. • [a]: Eingabe • Mit Lazy-Evaluation stoppt der Parser, sobald feststeht, dass eine Alternative erfüllt ist oder das erste Element einer Folge nicht passt. • (b,[a]): Ergebnis und restliche Eingabe • "Backtracking" Einfache Parser • Scheitert immer none :: Parser a b none inp = [] • Liest nichts, liefert Argument succeed :: b -> Parser a b succeed val inp = [(val,inp)] • Liest genau t token :: Eq a => a -> Parse a a token t (x:xs) | t==x = [(t,xs)] | otherwise = [] token t [] = [] • [(b,[a])]: Liste verschiedener Möglichkeiten, leer wenn Parser gescheitert ist Einfache Parser • Liest Zeichen, das Präsikat erfüllt spot :: (a -> Bool) -> Parse a a spot p (x:xs) | p x = [(x,xs)] | otherwise = [] spot p [] = [] • Alternative alt :: Parse a b -> Parse a b -> Parse a b alt p1 p2 inp = p1 inp ++ p2 inp • Sequenz zweier Parser (>*>) :: Parse a b -> Parse a c -> Parse a (b,c) (>*>) p1 p2 inp = [((y,z),rem2) | (y,rem1) <- p1 inp , (z,rem2) <- p2 rem1 ] 28-31 Einfache Parser • Wandelt Ergebnis um build :: Parse a b -> (b -> c) -> Parse a c build p f inp = [ (f x,rem) | (x,rem) <- p inp ] • Sequenz einer Liste list :: Parse a b -> Parse a [b] list p = (succeed []) `alt` ((p >*> list p) `build` convert) where convert = uncurry (:) Listen unendlicher Länge Einstiegspunkt in den Parser Parser anwenden, erste Möglichkeit zurückgeben topLevel :: Parse a b -> [a] -> b topLevel p inp = case results of [] -> error "parse unsuccessful" _ -> head results where results = [ found | (found,[]) <- p inp ] Erweiterung der Listennotation Lazy Evaluation macht Beschreibung unendlicher Datenstrukturen möglich Schlusselement weglassen ergibt unendlich lange Liste Zwei Aspekte: • Liste aller Zahlen ab 3: [3 ..] • Erzeugen ones :: [Int] ones = 1:ones • Liste aller ungeraden Zahlen ab 3: [3,5 ..] • Konsumieren addFirstTwo :: [Int] -> Int addFirstTwo (x:y:xs) = x + y addFirstTwo ones = 2 Pythagoreische Tripel pythagTriples = [ (x,y,z) | z <- [2 ..], y <- [2 .. z-1], x <- [2 .. y-1], x*x + y*y == z*z] 32-35 Liste aller Primzahlen Idee: Streiche aus der Liste aller Zahlen die Vielfachen der bereits bekannten Primzahlen heraus primes = sieve [2 ..] sieve (x:xs) = x : sieve [ y | y <- xs, y `mod` x > 0] Die ersten 6 Primzahlen: take 6 primes ==> [2,3,5,7,11,13] Folge von Pseudo-Zufallszahlen Lineare Kongruenzmethode: Lineare Funktion anwegendet auf Vorgänger liefert neuen Wert nextRand :: Int -> Int nextRand n = (multiplier*n + increment) `mod` modulus randomSequenze :: Int -> [Int] randomSequenze = iterate nextRand iterate :: (a -> a) -> a -> [a] iterate f x = x : iterate f (f x) Folge von Pseudo-Zufallszahlen seed = 17489 multiplier = 25173 increment = 13849 modulus = 65536 randomSequenze seed = [17489,59134,9327,52468,...] Auf bestimmten Bereich skalieren: scaleSequence :: Int -> Int -> [Int] -> [Int] scaleSequence s t = map scale where scale n = n 'div' denom + s range = t-s+1 denom = modulus 'div' range scaleSequence 40 50 (randomSequenze seed) = [42,49,41,48,...] 36-38