Vorlesungsnotizen Funktionale Programmierung
|
|
|
- Friedrich Brinkerhoff
- vor 10 Jahren
- Abrufe
Transkript
1 Vorlesungsnotizen Funktionale Programmierung Wilfried Klaebe Einführung Vorteile Funktionaler Sprachen hohes Abstraktionsniveau, keine Manipulation von Speicherzellen keine Seiteneekte, kompositioneller (Codeoptimierung: Verständlichkeit) Programmierung über Eigenschaften, nicht über zeitlichen Ablauf implizite Speicherverwaltung (GC) geringe Sourcecodegröÿe, kürzere Entwicklungszeiten modularer Programmaufbau, Polymorphismus, Funktionen höherer Ordnung, Wiederverwendbarkeit von Code, Typinferenz Strukturen in Funktionalen Programmen Variablen unbekannten Werten Programme Mengen von Funktionsdenitionen Speicher nicht explizit verwendbar, automatische Verwaltung Programmablauf Reduktion von Ausdrücken (math. Theories des λ-kalküls, Church, '41) Historie Funktionaler Sprachen λ-kalkül LISP: Listen als Datenstrukturen, Verwendung in KI und emacs Scheme: LISP-Dialekt mit statischer Bindung SASL: lesbares LISP für die Lehre ML: polymorpheres Typsystem KRC: Guards, Pattern Matching, Laziness Miranda: Erweiterung von KRC um polymorphes Typsystem, Modulkonzept; geringe Verbreitung, da Payware 1
2 Haskell als PD-Variante von Miranda Festsetzung als Standard Haskell98 SML: Standardisierung von ML Erlang: entwickelt von Ericcson Hier: zunächst Haskell98 2. Funktions- und Typdenitionen In der Mathematik: Variablen für unbekannte Werte: x^2-4x+4=0 x=2 In imperativen Sprachen: x=x+1 (steht im Widerspruch zur Mathematik) Idee: auch in der Programmierung sollen Variablen für unbekannte Werte stehen. in der Mathematik: Funktionen zur Berechnung aber in imperativen Sprachen: Funktionen zur Modularisierung Aber: wegen Seiteneekten kein wirklicher Zusammenhang zur Mathematik. In Haskell: keine Seiteneekte, Funktionen liefern immer gleiches Ergebnis bei gleichen Argumenten. 2.1 Funktionsdenitionen f x1... xn = e Funktionsname: beginnt mit einem Kleinbuchstaben formale Parameter Rumpf (Ausdrücke) Mögliche Ausdrücke: Zahlen: Basisoperationen: 3+4, 5*7 Funktionsanwendungen: f (g 3) 7 bedingte Ausdrücke: if b then e1 else e2 Variablen Auswertung: betrachte Funktionsdenition als orientierte Gleichung: Reduktionssemantik Binde formale Parameter an aktuelle Parameter und ersetze Funktionsaufrufe durch rechte Seite fib1 n = if n == 0 then 0 else if n == 1 then 1 else fib1 (n-1) + fib1 (n-2) 2
3 fib2' fibn fibnp1 n = if n==0 then fibn else fib2' fibnp1 (fibn+fibnp1) (n-1) fib2 n = fib2' 0 1 n Aus softwaretechnischer Sicht unschön: fib2' kann (auch) falsch verwendet werden. Lokale Denition: fib2l n = let fib2l' fibn fibnp1 n = if n == 0 then fibn else fib2l' fibnp1 (fibn+fibnp1) (n-1) in fib2l' 0 1 n fib2w n = fib2w' 0 1 n where fib2w' fibn fibnp1 n = if n == 0 then fibn else fib2w' fibnp1 (fibn+fibnp1) (n-1) Vorteile von lokalen Denitionen: Vermeidung von Namenskonikten und falscher Verwendung von Hilfsfunktionen Bessere Lesbarkeit, kleineres Interface Vermeidung von Mehrfachberechnungen Einsparung von Parametern der Hilfsfunktion 2.2 Basisdatentypen Int: Integer: beliebig groÿ/klein (Operationen: +,-,*,div,mod; Vergleiche: <=,=>,<,>,/=,==) Bool: boolesche Werte (Operationen: &&,,==,/=,not) Float: Flieÿkommazahlen (Operationen: +,-,*,/; Vergleiche wie Integer) Char: Zeichen 2.3 Typannotationen Alle Werte/Ausdrücke in Haskell haben einen Typ, welcher auch annotiert werden kann. (curryzierte Funktionsschreibweise nach Haskell B. Curry) 3
4 3::Int, 3::Integer square :: Int -> Int min :: Int -> Int -> Int 2.3 Algebraische Datenstrukturen Eigene Datenstrukturen können als neue Datentypen deniert werden. Werte werden mittels Konstruktoren aufgebaut. Konstrukturen sind frei interpretierbare Funktionen (nicht reduzierbar). Denition eines algebraischen Datentyps data tau = c_1 tau_1_1... tau_1_n1... c_k tau_k1. wobei τ der neu denierte Typ ist c 1,..., c k die denierten Konstruktoren sind τ i1... τ ini die Argumenttypen des Konstruktors c i sind, also c i :: τ i1 ->... -> τ ini -> τ Beachte: Sowohl Typen als auch Konstruktoren müssen in Haskell mit Groÿbuchstaben beginnen Beispiele: a) Aufzählungstypen (nur 0-stellige Konstruktoren) data Color = Red Blue Yellow b) Verbundtypen (nur ein Konstruktor) data Complex = Complex Float Float addc :: Complex -> Complex -> Complex addc (Complex r1 i1) (Complex r2 i2) = Complex (r1 + r2) (i1 + i2) c) Listen (zunächst nur über Int) data List = Nil Cons Int List append :: List -> List -> List append Nil ys = ys append (Cons x xs) ys = Cons x (append xs ys) In Haskell sind Listen bereits deniert mit data [Int] = [] Int : [Int] [] ++ ys = ys (x:xs) ++ ys = x:(xs ++ ys) Bem: Operatoren sind zweistellige Funktionen, die inx geschrieben werden und aus Sonderzeichen bestehen. Durch Klammerung werden sie zu normalen Funktionen: [1] ++ [2] = (++) [1] [2] Umgekehrt können zweistellige Funktionen mittels `...` inx verwendet werden: div 4 2 = 4 `div` 2 4
5 Bem für Übungsblatt 2: Für selbstdenierte Datentypen besteht nicht automatisch die Möglichkeit, Werte zu vergleichen oder auszugeben. Hierzu: data MyType =... deriving (Show,Eq,Ord) 3. Polymorphismus Betrachte nochmal Def. von (++) und length. Beide arbeiten zwar auf Listen, aber der Typ der Listenelemente ist nicht relevant. Möglich wären: length :: [Int] -> Int length :: String -> Int (wobei String=[Char]) length :: [String] -> Int Allgemein: length :: [a] -> Int Es können nur Listen über Elemente gleichen Typs zusammengehängt werden. Aber wie deniert man polymorphe Datenstrukturen? Typkonstruktoren zum Aufbau von Typen: dataka 1... a m = c 1 τ τ 1n1... c k τ k1... τ knk wie zuvor, aber: K Typkonstruktor a 1,..., a m Typvariablen τ ij Typen oder Typvariablen Funktionen/Konstruktoren werden auf Werte angewendet. Analog werden Typkonstruktoren auf Typen oder Typvariablen angewendet. Hierbei ergibt sich ein neuer Typ. Bsp: Partielle Werte data Maybe a = Nothing Just a Dann sind Typen: Maybe Int, Maybe (Maybe Int), Just 42, Nothing, Just Nothing, Just (Just 42) Bei Anwendung eines Typkonstruktors auf Typvariablen erhält man einen polymorphen Typ: isnothing :: Maybe a -> Bool isnothing Nothing = True isnothing (Just _) = False 5
6 Bsp: Binärbäume (Werte in Knoten) data Tree a = Node (Tree a) a (Tree a) Empty height :: Tree a -> Int height Empty = 0 height (Node tl _ tr) = max (height tl) (height tr) + 1 In Haskell vordeniert: Listen \\data [a] = [] a:[a] Einige Listen-Funktionen: head :: [a] -> a head (x:_) = x tail :: [a] -> [a] tail (_:xs) = xs last :: [a] -> a last [x] = x last (_:xs) = last xs concat :: [[a]] -> [a] concat [] = [] concat (l:ls) = l ++ concat ls (!!) :: [a] -> Int -> a (x:xs)!! 0 = x (x:xs)!! n = xs!! (n-1) Strings sind in Haskell ein Typsynonym für Listen von Chars: \\type String = [Char] Weitere vordenierte Typkonstruktoren: Vereinigung zweier Typen: data Either a b = Left a Right b Type (, mit Inxnotation data (,) a b = (,) a b -- oder (a,b) data (,,) a b c = (,,) a b c -- oder (a,b,c) fst :: (a,b) -> a fst (x,_) = x snd :: (a,b) -> b snd (_,y) = y zip/unzip zip :: [a] -> [b] -> [(a,b)] zip [] _ = [] zip _ [] = [] zip (x:xs) (y:ys) = (x,y) : zip xs ys unzip [(a,b)] -> ([a],[b]) unzip ((x,y):xys) = let (xs,ys) = unzip xys in (x:xs,y:ys) 6
7 4. Pattern Matching Komfortabler Programmierstil, Alternative zu Selektoren/Testfunktionen. Linke Seite einer Funktionsdenition entspricht dem Prototypen des Funktionsaufrufs. Funktionsdenitionen durch mehrere Gleichungen: f p_1_1... p_1_n = e_1... f p_k_1... p_k_n = e_k Wähle erste Regel mit passender linker Seite aus und wende diese an. Oft schön: Vermeidung doppelter Regeln. Aufbau der Pattern: Bsp: x (Variable) matched immer, x wird an aktuellen Wert gebunden _ (Unterstrich, Wildcard) matched immer, keine Bindung (c p_1... p_k) mit c k-stelliger Konstruktor x@pat matched, falls pat machted, zusätzlich wird x an aktuellen Wert gebunden (n+k) mit n Variable, k Ganzzahl, matched auf alle Zahlen >= k, wobei n an aktuellen Wert - k gebunden wird f 0 = 1 f m@(n+1) = m * fac n f n = n * fac (n-1) Pattern können auÿer bei Funktionen auch an anderen Stellen verwendet werden: Konstantendenition let (xs,ys) = unzip xys in (x:xs,y:ys) oder unzip ((x,y):xys) = (x:xs,y:ys) where (xs,ys) = unzip xys Dh auch auf toplevel: pi= (dimx,dimy) = evalfieldsize LastField Bem: Konstanten werden in Haskell genau einmal ausgewertet. 7
8 4.1 Case-Ausdrücke Manchmal ist es auch praktisch, in Ausdrücken mittels Pattern Matching zu verzweigen: case e of pat1 -> e1 pat2 -> e2 4.2 Guards Programmieruen mit mehreren Regeln oft schöner als mit if-then-else. Guards erlauben Bedingungen auf linker Seite. Bsp: fac n n == 0 = 1 otherwise = n * fac (n-1) 5. Funktionen höherer Ordnung Idee: Funktionen sind Bürger erster Klasse, d.h. sie können wie alle anderen Werte verwendet werden. Anwendungen: generische Programmierung Programmschemata (Kontrollstrukturen) Wiederverwendbarkeit, Modularität Bsp: Die Ableitungsfunktion ist eine Funktion, die zu einer Funktion eine Funktion berechnet. (wähle ein kleines dx) ableitung :: (Float -> Float) -> (Float -> Float) ableitung f = f' where f' x = (f (x + dx) - f x) / dx Anonyme Funktionen (Lambda-Abstraktion) kleine Funktionen müssen nicht global deniert werden. ableitung (\x -> x * x) Damit können wir auch schreiben: ableitung f = \x -> (f (x+dx) - f x) / dx 8
9 add = \x y -> x + y add 2 ist partielle Applikation (add x) y und add x y sind syntaktisch gleich, die Funktionsapplikation bindet linksassoziativ. a -> b -> c und a -> (b -> c) sind ebenfalls syntaktisch gleich, der Typkonstruktor -> bindet rechtsassoziativ. Wegen der Möglichkeit der partiellen Applikation ist die curryzierte Denition von Funktionen der über Tupeln vorzuziehen. 5.1 Generische Programmierung Betrachte folgende Funktionen: inclist :: [Int] -> [Int] inclist [] = [] inclist (x:xs) = (x+1) : inclist xs code :: Char -> Char code c c == 'Z' = 'A' c == 'z' = 'a' otherwise = chr (ord c + 1) codestr :: String -> String codestr "" = "" codestr (c:cs) = code c : codestr cs Beide Funktionen habenn ähnliche Struktur. Unterschiede: code statt (+1) Verallgemeinerung: map :: (a -> b) -> [a] -> [b] map f [] = [] map f (x:xs) = f x : map f xs Dann: inclist = map (+1) codestr = map code sum :: [Int] -> Int sum [] = 0 sum (x:xs) = x + sum xs checksum :: String -> [Int] checksum [] = 1 checksum (c:cs) = (ord c) + (checksum cs) Verallgemeinerung: foldr :: (a -> b -> b) -> b -> [a] -> b foldr f e [] = e foldr f e (x:xs) = f x (foldr f e xs) 9
10 Dann: sum = foldr (+1) 0 checksum = foldr (\ c sum -> ord c + sum) 1 Weiteres Schema: filter :: (a -> Bool) -> [a] -> [a] filter p [] = [] filter p (x:xs) p x = x : filter p cs otherwise = filter p xs Umwandeln einer Liste in eine Menge (doppelte entfernen) nub :: [Int] -> [Int] nub [] = [] nub (x:xs) = x : nub (filter (x /=) xs) Sortieren von Listen mittels Quicksort: qsort :: [Int] -> [Int] qsort [] = [] qsort (x:xs) = qsort (filter (<= x) xs) ++ [x] ++ qsort (filter (>x) xs) Auch filter ist mit foldr programmierbar: filter p = foldr (\x ys -> if p x then x : ys else ys) [] foldr ist sehr allgemeines Skelett Katamorphismus der Kategorientheorie foldr f e (x1:x2:...xn:[]) f x1 (f x2 (... (f xn e)...)) Verwendung von foldr ist aber manchmal problematisch, da äuÿeres f erst berechnet werden kann, wenn gesamte Liste durchlaufen wurde. Bessere Lösung oft durch Akkumulatortechnik. foldl :: (b -> a -> b) -> b -> [a] -> b foldl f e [] = e foldl f e (x:xs) = foldl f (f e x) xs 5.2 Kontrollstrukturen (Programmschemata) while :: (a -> Bool) -> (a -> a) -> a -> a -- Prädikat Rumpf Anfangswert while p f x p x = while p f (f x) otherwise = x while (<100) (2*)
11 5.3 Funktionen als Datenstrukturen Ein ADT ist ein Objekt mit folgenden Operationen: Konstruktoren Selektoren Testfunktionen Verknüpfungen Wichtig hierbei ist die Funktionalität (Schnittstelle) und nicht die Implementierung. Somit: ADT Satz von Funktionen Bsp: Arrays Konstruktoren: emptyarray :: Array a putindex :: Array a -> Int -> a -> Array a Selektoren: getindex :: Array a -> Int -> a Implementierung als Funktion: emptyarray i = error("zugriff auf nichtinitialisierte Arraykomponente" ++ show i) getindex a i = a i putindex a i v = a' where a' j i == j = v otherwise = a j 5.4 Wichtige Funktionen höherer Ordnung (.) :: (b -> c) -> (a -> b) -> a -> c (f. g) x = f (g x) flip :: (a -> b -> c) -> b -> a -> c flip f x y = f y x curry :: ((a,b) -> c) -> a -> b -> c curry f x y = f (x,y) uncurry :: (a -> b -> c) -> (a,b) -> c uncurry f (x,y) = f x y const :: a -> b -> a const x _ = x 11
12 6. Typklassen Betrachte folgende Funktionsdenition: elem x [] = False elem x (y:ys) = x == y elem x ys Was soll der Typ von elem sein? elem :: Int -> [Int] -> Bool? elem :: Bool -> [Bool] -> Bool? elem :: a -> [a] -> Bool? Der letzte Typ ist zu allgemein, da a existieren, für die Gleichheit nicht deniert werden kann. Lösung: Einschränkung auf Typen, für die Gleichheit deniert ist. elem :: Eq a => a -> [a] -> Bool Die Klasse Eq ist in Haskell wie folgt deniert: class Eq a where (==),(/=) :: a -> a -> Bool In einer Klasse werden alle Typen zusammengefaÿt, für die die Funktionen der Klasse deniert sind. Typen werden zu Instanzen einer Klasse durch: data Tree = Empty Node Tree Int Tree instance Eq Tree where Empty == Empty = True (Node tl i tr) == (Node tl' i' tr') = tl == tl' && i == i' && tr == tr' _ == _ = False t1 /= t2 = not (t1 == t2) Dann sind (==) und (/=) für Tree verwendbar. data Tree a = Empty Node (Tree a) a (Tree a) Gleichheit auf Tree a nur deniert, wenn Gleichheit auf a deniert ist. instance Eq a => Eq (Tree a) where... Klassen können auch vordenierte Funktionsdenitionen enthalten, (/=) wird in fast jeder Instanz als Negation von (==) deniert. Deshalb: class Eq a where (==),(/=) :: a -> a -> Bool x1 == x2 = not (x1 /= x2) x1 /= x2 = not (x1 == x2) 12
13 Bei Instanzenbildung sollte mindestens eine Funktion überschrieben werden. Erweiterung von Klassen: Für manche Typen macht es auch Sinn, eine totale Ordnung zu denieren (z.b. für die Schlüssel eines Suchbaums). Erweiterung der Klasse Eq: class Eq a => Ord a where compare :: a -> a -> Ordering (<),(<=),(>),(=>) :: a -> a -> Bool max, min :: a -> a -> a... Weitere vordenierte Klassen: Num zum Rechnen mit Zahlen (+) :: Num a => a -> a -> a Show zum Umwandeln in Strings show :: Show a => a -> String Read zum Konstruieren von Werten aus Strings read :: Read a => String -> a Automatische Instanziierung vordenierter Klassen (auÿer Num) mittels deriving (Classname) hinter der Datentypdenition. Die Klasse Read vordeniert: type Reads a = String -> [(a,string)] class Read a where readsprec :: Int -> Reads a readlist :: Reads [a] weiter: reads :: Read a => Reads a reads = readpres 0 read :: Read a => String -> a read xs = case reads xs of (x,"") _ -> x _ -> error "no parse" 7. Lazy Evaluation Betrachte folgendes Programm: 13
14 f x = 1 h = h und die Anfrage f h. Zwei ausgezeichnete Reduktionen: left-most-inner-most (LI, strikte Funktion) left-most-outer-most (LO, nicht-strikte Funktion) Vorteil von left-most-outer-most: berechnungsvollständig. Alles, was mit irgendeiner Reduktion berechnet werden kann, wird mit LO berechnet. Auch praxisrelevant? Ja. Vermeidung von überüssigen Berechnungen Rechnen mit unendlichen Datenstrukturen from :: Num a => a -> [a] from n = n : from (n+1) take :: Int -> [a] -> [a] take n _ n <= 0 = [] take n [] = [] take n (x:xs) = x : (take (n-1) xs) Dann: take 3 (from 1) [1,2,3] Vorteile: Trennung von Kontrolle (take 3) und Daten (from 1) Bsp: Primzahlberechnung mittels Sieb des Erathostenes sieve :: [Int] -> [Int] sieve (p:xs) = p : sieve (filter (\x -> x `mod` p > 0) xs) primes = sieve (from 2) Die Programmierung mit unendlichen Datenstrukturen kann als Alternative zur Akkumulatortechnik verwendet werden. Bsp: Fibonaccifunktion fibgen :: Int -> Int -> [Int] fibgen n1 n2 = n1 : fibgen n2 (n1+n2) fibs :: [Int] fibs = fibgen 0 1 fib :: Int -> Int fib n = fibs!! n Nachteil von LO: Berechnungen können dupliziert werden (siehe double). Keine Programmiersprache verwendet LO, zu inezient. Optimierung: Lazy Evaluation Statt Termen werden Graphen reduziert. Variablen des Programms entsprechen Zeigern auf Ausdrücke. Auswertung eines Ausdrucks gilt für alle Variablen, die auf diese zeigen. Normalisierung führt Variable für jeden Teilausdruck ein. Lazy Evaluation ist optimal bezüglich Länge der Auswertung. Allerdings benötigt sie oft viel Speicher. 14
15 Ja. Idee: verwende Funk- Ist Laziness auch in strikten Sprachen möglich? tionen zur Verzögerung: x:xs -> \ _ -> x:xs [] -> \ _ -> [] Neue Typen: type List a = () -> ListD a data ListD a = cons a (List a) Nil headl :: List a -> a headl xs = let cons x _ = xs () in x taill :: List a -> List a taill xs = let cons _ ys = xs () in ys nulll :: List a -> Bool nulll xs = case xs () of Nil -> True _ -> false froml :: Int -> list Int froml n = \ _ -> Cons n (from (n+1)) --from n = n : from (n+1) takel :: Int -> List a -> List a takel n xs n <= 0 = \ _ -> Nil isnil xs = \ _ -> Nil otherwise = \ _ -> Cons (headl xs) (takel (n-1) (taill xs)) tolist :: List a -> [a] tolist xs isnil xs = [] otherwise = headl xs : tolist (taill xs) 8. Ein-/Ausgabe Für Ein-/Ausgaben spielt die Reihenfolge der Aktion eine Rolle. main = let str = getline in putstr str Aber was ist funktionales Ergebnis von putstr und damit von main? () Dann ist aber eigentlich keine Ein-/Ausgabe nötig, um das Ergebnis () zu berechnen. Dashalb in ML: Ausgabe als Seiteneekt, wenn getline/putstr ausgewertet wird. Aber was bedeutet dann: main = getline ++ getline main = let str = getline in str ++ str Werden ein oder zwei Strings eingelesen? Noch problematischer in lazy Sprachen ist folgendes Szenario: 15
16 main = let database = readdbfromuser request = readreqfromuser in lookup request database Wegen lazyness wird database erst eingelesen, nachdem request eingelesen wurde, und auch nur so weit wie nötig. Später ggf weiter für einen weiteren Request. Lösung in Haskell: Die IO-Monade Wichtig für IO sind nicht die Werte, sondern die Aktionen und ihre Sequentialisierung. Aktionen haben in Haskell den Typ IO(). Bsp: putchar :: Char -> IO() IO-Aktionen sind auch rst-class-citizens. Sie werden nur ausgeführt, wenn sie in die Toplevel-IO-Aktion (von main oder der Anfrage in hugs/ghci) eingebaut werden. Zusammensetzen von IO-Aktionen (Sequenz): (>>) :: IO() -> IO() -> IO() Dann: main = putchar 'a' >> putchar 'a' verhält sich gleich wie main = let p = putchar 'a' in p >> p putstr :: String -> IO() putstr "" = () putstr (c:cs) = putchar c >> putstr cs oder putstr = foldr (\c -> (putchar c >>)) (return ()) Input: Wie verarbeiten? Der Typkonstruktor IO hat zusätzlich einen Typparameter für die Übergabe von Werten an folgende IO-Aktionen: IO a Bsp: getchar :: IO Char return :: a -> IO a 16
17 Bind-Operator: Sequenz mit Weitergabe (>>=) :: IO a -> (a -> IO b) -> IO b getchar >>= putchar getline :: IO String getline = getchar >>= \c -> if c == '\n' then return "" else getline >>= \cs -> return (c:cs) oder getline :: IO String getline = c := getchar ; if c == '\n' then return "" else cs := getline ; return (c:cs) Die do-notation: Vereinfachte Schreibweise, welche statt (>>=), (>>) mittels ; oder o-side-rule getrennte Sequenzen verwendet. Einleitung mittels Schlüsselwort do. Bsp: main = do c <- getchar putchar c statt main = getchar >>= \c -> putchar c Variablen sind ab ihrer Einführung über <- oder let gültig. Pattern müssen passen, sonst Laufzeitfehler. Bsp: getline :: IO String getline = do c <- getchar if c == '\n' then return "" else do cs <- getline return (c:cs) 17
18 writefile :: String -> String -> IO() -- writefile "text" "file" readfile :: String -> IO String -- readfile "file" Bsp: Fakultät mit Ausgabe aller Zwischenergebnisse fac :: Int -> IO Int fac 0 = do putstr "1" return 1 fac (n+1) = do fac_n <- fac n let fac_np1 = (n+1) * fac n putstr (' ':show fac_np1) return fac_np1 Besseres Vorgehen: funktionale Berechnung aller Zwischenergebnisse: fac :: Int -> [Int] Ausgabe der Ergebnislisten Trennung von IO und Berechnung 9. List Comprehensions Komfortable Notation von Listen: [1..4] = [1,2,3,4], [1..] = [1,2,3,..., [1,3..10] = [1,3,5,7,9], [1,4..] = [1,4,7,... Auch für kompliziertere Listen gibt es komfortable Notationen ähnlich wie für Mengen in der Mathematik: {(i, j) i {1,..., 3}, j {2, 3, 4}, i j} = [(i,j) i <- [1..3], j <- [2,3,4], i /= Allgemeine Form: [e lce1,..., lcen ] mit e beliebiger Ausdruck über aus dem Kontext bekannte und in lce1,..., lcen eingeführte Variable Bsp: concat xs = [y ys <- xs, y <- ys] 10. Der λ-kalkül Grundlage funktionaler Sprachen: λ-kalkül, entwickelt von Church (1941) Motivation: Grundlage der Mathematik, insbesondere mathematischer Logik Formalisierung von Berechenbarkeit, Erkenntnis: Äquivalenz zur Turingmaschine, Church'sche These 18
19 Syntaktische Konstrukte: Funktionen als syntaktische Objekte gebundene/freie Variablen Def: Syntax des λ-kalküls Var abzählbare Menge von Variablen Exp Ausdrücke des reinen λ-kalküls deniert durch (e Exp). e :: v - Variablen (ee ) - Applikationen λv : e - Abstraktionen Konvention zur Klammervermeidung: Applikation linksassoziativ: xyz = ((xy)z) Wirkungsweise von λ so weit rechts wie möglich: λx : xx = λx : (xx) (λx : xx) Liste von Parametern: λxy : e = λxλye Beachte: Keine Konstanten oder if-then-else. Später ein getyptes λ-kalkül. Semantik des λ-kalküls rechnen in funktionalen Sprachen: λx : e anonyme Funktion \x -> e Sonst: (λx : x)z z Problem: Namenskonikte bei Variablen Def: Freie/gebundene Variablen Die Funktionen free, bound : Exp 2 Var sind deniert durch: free(v) = {v} free(λx : e) = free(e)\{x} free((ee )) = free(e) free(e ) bound(v) = bound(λx : e) = bound(e) {x} bound((ee )) = bound(e) bound(e ) Ausdrücke e heiÿen geschlossen (Kombinator) gdw free(e) =. Wichtig: Bei Applikationen nur Ersetzung der freien Vorkommen der Parameter, wobei Namenskonike vermieden werden müssen Präzisierung durch 19
20 Def: Substitution Seien e, f Exp, v Var. Dann ist e[v/f] (ersetze v durch f in e) deniert durch: v[v/f] = f x[v/f] = x, x v λv : e[v/f] = λv : e λx : e[v/f] = λx : e[v/f], x v,, x free(f) λx : e[v/f] = λy : e[x/y][v/f], x v, x free(f), y free(e) free(f) {v} (ee )[v/f] = (e[v/f]e [v/f]) Somit: v free(e) e[v/f] = e Damit Semantik für Ausrechnen der Applikation als Reduktionsrelation Def: β-reduktion β Exp Exp ist def. durch: (λv : e)f β e[v/f] (Ersetze formale Parameter durch aktuelles Argument) Bsp: (λf : λx : fx)x β λy : xy (λf : λx : fx)x β λz : xz Somit: β nicht konuent Aber: Namen der Parameter spielen keine Rolle bzgl Bedeutung der Funktionen/- Ausdrücke. Lösung: α-reduktion α Exp Exp mit λx : e α λy : e[x/y], wobei y free(e) Bsp: λx : x α λy : y λx : xy α λz : zy λx : xy α λx : xx Somit: e α e (e und e sind α-äquivalent) gdw e und e sich nur durch Namen der Parameter unterscheiden Im Folgenden: Betrachte λ-äquivalente Ausdrücke als gleich, d.h. rechne auf λ- Äquivalenzklasse statt auf Ausdrücke Konuenz β-reduktion: Ausrechnen von Applikationen von λ-ausdrücken, allerdings nur auÿen Fortsetzung von β auf beliebige Stellen im Ausdruck: e β e ef β e f und fe β fe und λx : e β λx : e (analog für α-reduktion) Eigenschaften der β-reduktion Satz: (a) β ist konuent (b) Jeder Ausdruck besitzt höchstens eine Normalform bzgl β (bis auf die α- Äquivalenz) 20
21 Es gibt allerdings auch Ausdrücke ohne Normalform. (λx : xx)(λx : xx) β (λx : xx)(λx : xx) Selbstapplikation erlaubt Rekursion Wie ndet man die Normalform, falls sie existiert? β-redux: Teilausdruck der Form (λx : e)f Reduktionsstrategie: strat: Exp -> Redux nächste zu reduzierende Redux LO-Strategie: wähle linkesten äuÿersten Redux LI-Strategie: wähle linkesten innersten Redux Allgemein: Satz: Ist e eine Normalform von e, d.h. e β e, dann existiert eine LO- Ableitung von e nach e. Somit: LO-Strategie berechnet Normalform, falls sie existiert, LI manchmal auch nicht. LO ist berechnungsstärker als LI. Äquivalenz von Ausdrücken: Intuitiv: e und e sind äquivalent gdw e überall für e eingesetzt werden kann, ohne das Ergebnis zu verändern. Bsp: λx : x äquivalent zu λy : y Schön wäre es, Äquivalenz durch syntaktische Transformationen nachweisen zu können. Bisher: α und β (α- und β-äquivalenz) Noch nicht ganz ausreichend: (+1) ist äquivalent zu (λx : (+)1x)z β (+)1z Daher: Def: η-reduktion Relation η Exp Exp mit λx : e η e, falls x free(e) Andere Sichtweise: η-reduktion ist vorweggenommene β-reduktion: (λx : ex)f β ef, falls x free(e) Extensionalität: Funktionen sind äquivalent, falls sie gleiche Funktionsgraphen (Menge der Argument-Wert-Paare) haben: Falls fx β gx, dann f νβ g Zusammenfassung der Reduktionsregeln des λ-kalküls: α-reduktion Umbenennung von Parametern β-reduktion Funktionsanwendung 21
22 ν-reduktion Elimination redundanter λ-abstraktion δ-reduktion Rechnen mit vordenierten Funktionen: (+)12 δ 3 aber: vordenierte Funktionen sind nicht notwendig, da im reinen λ-kalkül kodierbar: Datenobjekte im λ-kalkül: Datentypen: Objekte + Operationen darauf Idee: Stelle Objekte durch geschlossene λ-ausdrücke dar und deniere dann passende Operationen Bsp: Bool Objekte: True, False wichtigste Operation: if-then-else if-then-else(b, e 1, e 2 ): b = True e 1, b = False e 2 True/False sind Projektionsfunktionen: True λxy : x (nimm erstes Argument) False λxy : y (nimm zweites Argument) cond λbxy : bxy Dann ist cond das if-then-else: cond True e 1 e 2 (λbxy : bxy)textt rue e 1 e 2 3 β True e 1 e 2 (λxy : x)e 1 e 2 2 β e 1 Kodierung natürlicher Zahlen: Church-Numerals: Stelle n als Funktional (Funktion höherer Ordnung) dar, oder eine Funktion n-mal auf ein Argument angewendet: 0 λfx : x 1 λfx : fx 2 λfx : f(fx) Operationen auf natürlichen Zahlen: succ λn : λfx : nf(fx) succ1 (λn : λfx : nf(fx))(λf : λx : fx) β λgx : (λf : λx : fx)g(gx) β λgx : (λx : gx)(gx) β λgx : g(gx) 2 Test auf 0: is0 λn : n(λx : False)True is00 (λn : n(λx : False)True)(λfx : x) β λfx : x(λx : False)True β λfx : xtrue β True Nachfolger: Ist der reine α-kalkül berechnungsvollständig? Ja, denn auch Rekursion ist darstellbar. 22
23 Fixpunktsatz: Zu jedem F Exp gib es einen Ausdruck X mit F X β X. Wähle z.b. X = Y F mit dem Fixpunktkombinator Y = λf : (λx : f(xx))(λx : f(xx)) Reiner λ-kalkül: minimal und berechnungsvollständig, aber mehr theoretische Relevanz. Für funktionale Programmierung relevant: Angereichterter λ-kalkül Hinzunahme von Konstanten (vordenierte Objekte und Funktionen), let, if-thenelse und Fixpunktkombinator Syntax: e := v Var k (ee ) λv : e letv = eine ifethene 1 elsee 2 µv : e (µ Fixpunktkombinator Operationelle Semantik: α, β, η-reduktion + δ-reduktion für Konstanten letv = eine e [v/e] ( ) iftruethene 1 elsee 2 e 1 iffalsethene 1 elsee 2 e 2 µv : e e[v/µv : e] Angereicherter λ-kalkül bildet Basis der Implementierung funktionaler Sprachen. 1.) Übersetze Haskell-Programme in diesen Kalkül (Core-Haskell bei ghc) Pattern-Matching if-then-else + Selektoren (oder case) fx 1... x n = e f = λx 1... x n : e rekursive Funktionen Fixpunktkombinatoren Programm: Folge von let-deklarationen und auswertendem Ausdruck 2.) Implementiere Kalkül durch spezielle abstrakte Maschine (z.b. SECD-Maschine, Graphmaschine) Weitere Anwendungen des λ-kalküls: denotationelle Semantik (Programmanalyse) Typisierung (getypter λ-kalkül) Beweissysteme (Einschränkungen des reinen λ-kalküls) 23
24 11. Monaden Untersucht man die IO-Monade und ihre Verknüpfungen genauer, erkennt man die Gültigkeit folgender Gesetze: return () >> m == m m >> return () == m m >> (n >> o) == (m >> n) >> o return () ist neutrales Element, (>>) ist assoziativ. D.h. return () und (>>) formen ein Monoid. Entsprechend gilt für return () und (>>=): return v >>= \x -> m = m == m[x/v] m >>= \x -> return x == m m >>= \x -> (m >>= \y -> o) == (m >>= \x -> m) >>= \y -> o -- falls x \in o (*) Diese Struktur heiÿt nach Leibniz Monade. In Haskell: Def: Eine Monade ist ein einstelliger Typkonstruktor m, zusammen mit den Operationen return, (>>=), fail, welcher (*) erfüllt. class Monad m where (>>=) : m a -> (a -> m b) -> m b return : a -> m a (>>) : m a -> m b -> m b p >> q = p >>= \_ -> q fail :: String -> m a fail s = error s do-notation also syntaktischer Zucker für alle Monaden. IO ist Instanz der Klasse Monad. Weitere Instanz: Maybe. Bsp: Auswertung arithmetischer Ausdrücke data Exp = Exp :+: Exp Exp :/: Exp Num Float Problem: Vermeidung von Laufzeitfehlern (hier: /0) Lösung durch Maybe-Ergebnis: eval (Num 3 :+: Num 4) Just 7.0 eval (Num 3 :/: (Num (-1) :+: Num 1)) Nothing eval :: Exp -> Maybe Float eval (Num x) = Just x eval (e1 :+: e2) = case eval e1 of Nothing -> Nothing Just v1 -> case eval e2 of Nothing -> Nothing Just v2 -> Just (v1 + v2) 24
25 eval (e1 :/: e2) = case eval e1 of Nothing -> Nothing Just v1 -> case eval e2 of Nothing -> Nothing Just 0 -> Nothing Just v2 -> Just (v1 / v2) Besser: Maybe als Monade Idee: Nothing (Fehler) schlägt durch instance Monad Maybe where return = Just Nothing >>= K = Nothing Just v >>= K = K v fail = Nothing Dann: eval (e1 :/: e2) = do v1 <- eval e1 v2 <- eval e2 if v2 == 0 then Nothing else return (v1/v2) Andere Sicht der Maybe-Monade: Maybe ist ein Container, der 0 bis 1 Elemente aufnehmen kann. Verallgemeinerung des Containers Maybe sind Listen als Container für beliebig viele Elemente. Auch Listen sind Monaden. instance Monad [] where return x = [x] (x:xs) >>= f = f x ++ xs >>= f [] >>= f = [] fail _ = [] Dann: [1,2,3] >>= \x -> [4,5] >>= y -> return (x,y) [(1,4),(1,5),(2,4),(2,5),(3,4),(3,5)] oder: do x <- [1,2,3] y <- [4,5] return (x,y) oder [(x,y) x <- [1,2,3], y <- [4,5]] Weitere Eigenschaften der Listenmonade: [] ist Null der Struktur m >>= \x -> [] = [] [] >>= K = [] 25
26 ausgezeichnete Funktion (++) mit (m ++ n) ++ o == m ++ (n ++ o) zusammengefaÿt in der Klasse MonadPlus: class Monad m => MonadPlus m where zero :: m a (++) :: m a -> m a -> m a instance MonadPlus [] where zero = [] [] ++ ys = ys (x:xs) ++ ys = x:(xs ++ ys) Viele Funktionen auf Monad/MonadPlus denierbar, die für konkrete Instanz verwendbar: sequence :: Monad m => [m a] -> m [a] sequence = foldr means (return []) where means p q = p >>= \x -> q >>= \y -> return (x:y) sequence_ :: Monad m => [m a] -> m () sequence_ = foldr (>>) (return ()) mapm :: Monad m => (a -> m b) -> [a] -> m [b] mapm f as = sequence (map f as) mapm_ :: Monad m => (a -> m b) -> [a] -> m () mapm_ f as = sequence_ (map f as) if-then ohne else: when :: Monad m => Bool -> m () -> m () when b a = if b then a else return () guard :: MonadPlus m => Bool -> m () guard b = if b then return () else zero 10.3 Implementierung der IO-Monade Kann auch die IO-Monade in Haskell implementiert werden? Ja: IO-Aktionen nehmen eine Welt und geben veränderte Welt in Kombination mit dem Ergebnis zurück. type IO a = World -> (a,world) Wobei wir die Welt repräsentieren als type World = () Dann können die Monaden-Funktionen deniert werden: return :: a -> IO a return x w = (x,w) (>>=) :: IO a -> (a -> IO b) -> IO b a >>= K = \w -> case a w of (r,w') -> K r w' 26
27 Durchschleifen der Welt, so daÿ a ausgeführt werden muÿ, bevor K ausgeführt werden kann. Beachte: Welt wird nicht dupliziert! Anderer Ansatz: Clean stellt durch das Uniqueness-Typsystem sicher, daÿ Welten nicht dupliziert werden können, und gewährleistet so Sequentialität. return :: a -> IO a return x () = (x,()) (>>=) :: IO a -> (a -> IO b) -> IO b a >>= k = \w -> case a w of (r,w') = k r w' In den primitiven Funktionen muÿ dann die Umwandlung in/von C-Datenstrukturen durchgeführt werden, bevor die Welt zurückgegeben wird. Starten von IO-Monade durch Applikation auf (). runio :: IO () -> Int runio a -> case a () of ((),()) -> 42 Für eigene IO-Monade auch denierbar: unsafeperformio :: IO a -> a unsafeperformio a = case a () of (r,()) -> r Aber unsicher, da nicht referentiell transparent. f :: () -> String f () = unsafeperformio getline f () kann bei jeder Ausführung ein anderes Ergebnis liefern - Widerspruch zu funktional Erweiterung der IO-Monade um Zustände kein Haskell-Standard, Modul IoExts Abstrakter Datentyp: data IORef a polymorphe Speicherzellen Interface: newioref :: a -> IO (IORef a) generiert neue IoRef, Parameter zur Initialisierung 27
28 readioref :: IORef a -> IO a -- liest aktuellen Wert aus der IORef writeioref :: IORef a -> a -> IO () -- modifiziert IORef Aktion wird zwar in IO-Monade sequentialisiert, aber die Werte in IORefs werden lazy ausgewertet Eine einfache Zustandsmonade Wir betrachten folgendes Problem: Durchnumerieren aller Blätter eines Baumes data Tree a = Node (Tree a) (Tree a) Leaf a number :: Tree a -> Tree (Int,a) number t = number' t where number' :: Tree a -> Int -> Tree (Int,a) number' (Leaf x) n = Leaf (n,x) number' (Node t1 t2) n = let (t1',n1) = number' t1 n (t2',n2) = number' t2 n1 in (Node t1' t2', n2) Durchreichen des Zählers kann unübersichtlich werden. In imperativen Sprachen: Verwendung eines globalen Zustands. Idee: Verstecke diesen Zustand in einer Monade, die ihn durchschleift. data State s a = ST (s -> (a,s)) instance Monad (State s) where -- return :: a -> State s a return r = St (\s -> (r,s)) -- (>>=) :: State s a -> (a -> State s b) -> State s b m >>= f = ST (\s0 -> let ST strans1 = m (r1,s1) = strans1 s0 ST strans2 = f r1 in strans2 s1 Auÿerdem: runstate :: s -> State s a -> a runstate start (ST strans) = fst (strans start) update :: (s -> s) -> State s s update f = ST (\s -> (s. f s)) get :: State s s get = update id set :: s -> State s s set ns = update (const ns) Verwendung: 28
29 number :: Tree a -> Tree (Int,a) numer t = runstate 1 (number' t) where number' :: Tree a -> State Int (Tree (Int,a)) number' (leaf x) = do n <- update (+1) return (Leaf (n,x)) number' (Node l r) = do l' <- number' l r' <- number' r return (Node l' r') 11. Debugging Haskell hat viele Vorteile, die aber das Finden von Fehlern erschweren, insbesondere Seiteneektfreiheit, Lazyness und Funktionen höherer Ordnung. Einfachstes Debugging in (strikten) imperativen Sprachen: printf In Haskell auch möglich mit unsicheren Funktionen: trace :: String -> a -> a trace str a = unsafeperformio (putstrln str >> return a) Semantik: Identität, aber wenn Berechnung angestoÿen wird, wird String ausgegeben Debuggen mit Observation (Hood) Idee: Beobachtete Werte wie mit traceandshow, aber _ für nicht benötigte Teilstrukturen. Ausgabe am Programmende. Zusätzlicher String-Parameter zur Unterscheidung der Beobachtungen > :l Observe > take 2 (observe "list" [1..]) [1,2] Verwendung im Programm: import Observe Dann (observe name e) im Programm benutzen Implementierung von Observe data EvalTree = Cons String [EvalRef] Uneval Demand type EvalRef = IORef EvalTree data Tree = Empty Node Tree Tree otree :: EvalRef -> Tree -> Tree otree ref Empty = unsafeperformio $ do 29
30 mkevaltreecons "Empty" ref 0 return Empty otree ref (Node tl tr) = unsafeperformio $ do [aref,bref] <- mkevaltreecons "Node" ref 2 return (Node (otree aref tl) (otree bref tr)) mkevaltreecons :: String -> EvalRef -> Int -> IO [EvalRef] mkevaltreecons consname ref n = do refs <- mapm (const (newioref Uneval)) [1..n] writeioref ref (Cons consname refs) return refs Verallgemeinerung: class Observe a where obs :: EvalRef -> a -> a instance Observe Tree where obs = otree observe :: Observe a => EvalRef -> a -> a observe ref x = unsafeperformio $ do writeioref ref Demand return (obs ref x) Zusätzlich in obs-denition observer statt obs/otree verwenden. Dann: Uneval Demand Cons "name" [, ] Speicherung aller Observationen: global :: IORef [IO ()] global = unsafeperformio (newioref []) observe :: Observe a => String -> a -> a observe label x = unsafeperformio $ do ref <- newioref Uneval modifyioref global ((putstrln (label ++ "\n" ++ (replicate (length label '-'))) >> showevaltre return (observer ref x) run :: IO () -> IO () run = do writeioref global [] catch io (\e -> putstrln ("Runtime Error\n"++ show e) >> printobs) printobs printobs :: IO () printobs = do putstrln ">>> Observations <<<" obs <- readioref global sequence_ obs showevaltreeref :: EvalTreeRef -> IO String showevaltreeref r = do val <- readioref r showevaltree val showevaltree :: EvalTree -> IO String showevaltree Uneval = "_" showevaltree Demand = "!" 30
31 showevaltree (Cons cons []) = return cons showevaltree (Cons cons ts) = do args <- mapm showevaltreeref ts return "("++ concat (intersperse " " (cons:args))++")" + Spezialfälle für Listen, Tupel, Tripel, Funktionen,... Verwendung: Deniere Observe-Instanzen für alle zu observierenden Datentypen (Problem: Denition der Objekt-Instanzen benötigt Wissen über Implementation des Ansatzes) Erweitere Programme um observe-aufrufe (manchmal problematisch bei polymorphen Funktionen, da Typvariablen auf Observe-Typen eingeschränkt werden müssen) Bsp: instance Observe a => Observe [a] where -- obs :: a -> EvalTreeRef -> a obs [] r = unsafeperformio $ do mkevaltreecons "[]" r 0 return 0 obs (x:xs) r = unsafeperformio $ do [aref,bref] <- mkevaltreecons "(:)" r 2 return (observe aref x : observe bref xs) Beobachtung: Denition für alle n-stelligen Konstruktoren gleich, bis auf: Pattern Konstruktor Stringdarstellung des Konstruktors Verallgemeinerung: o0 :: a -> String -> EvalTreeRef -> a o0 cons consname r = unsafeperformio $ do mkevaltreecons consname r 0 return cons o2 :: (Observe a, Observe b) => (a -> b -> c) -> String -> a -> b -> EvalTreeRef -> c o2 cons consname va vb r = unsafeperformio $ do [aref,bref] <- mkevaltreecons consname r 2 return (cons (observe va aref) (observe vb bref) ) 31
32 Dann: instance Observe a => Observe [a] where obs [] = o0 [] "[]" obs (x:xs) = o2 (:) "(:)" x xs Implementierungswissen versteckt in generischen Observern on. Observierung von Funktionen: Teil des Funktionsgraphen: Repräsentation als im Programm verwendeter map (observe "inc" (+1)) [3,4,5] Vergleiche: map (trace "*" (+1)) [3,4,5] Wegen Lazy Evaluation wird observe "inc" (+1) zunächst zu einer Funktion ausgewertet, welche dann 3mal ausgewertet wird. Somit müssen Funktionen im EvalTree mehrere Applikationen speichern können: data EvalTree = Cons String [EvalTreeRef] Uneval Demand Fun [(EvalTreeRef,EvalTreeRef)] Observieren von Funktionen: instance (Observe a, Observe b) => Observe (a->b) where -- obs :: (a->b) -> EvalTreeRef -> a -> b obs f r x = unsafeperformio $ do applrefs <- readiofunref r argref <- newioref Uneval resrev <- newioref Uneval writeioref r (Fun ((argref,resref):applrefs)) return (observe (f (observe x argref)) resref) where readiofunref :: EvalTreeRef f -> [(EvalTreeRef,EvalTreeRef)] readiofunref r = do v <- readioref r case v of Fun applrefs -> return applrefs _ -> return [] Auÿerdem noch Erweiterung der Ausgabe: showevaltree (Fun appls) = do resstrs <- mapm showapp (reverse appls) return concat (intersperse "\n" resstrs) where showapp :: (EvalTreeRef,EvalTreeRef) -> IO String 32
33 showapp (rarg,rres) = do arg <- showevaltreeref rarg res <- showevaltreeref rres return ("{"++arg++"->"++res++"}") Bem: Ausgabe ist sehr einfach gehalten, z.b.: > run $ print $ observe "(+)" (+) 3 4 Bisher noch keine Observer für primitive Typen wie Int, Char, Float,... oprim :: Show a => Obs a oprim x r = unsafeperformio $ do seq x (return ()) mkeval Cons (show x) r 0 return x Dann: instance Observe Int where obs = oprim Gleiches für die anderen primitiven Typen. Ansatz implementiert eine Head-Codierung über linearer Steuerung der EvalTrees in Speicher oder Datei. Hugs arbeitet allerdings auf wirklichen Datenstrukturen im Heap manchmal andere Observationen Nachteil des Ansatzes: Keine Beziehung zwischen unterschiedlichen Observationen möglich. Aber: Sehr einfach zu benutzen, light-weigth-ansatz. Funktioniert unabhängig von Haskell-Implementierung. Tolerant gegen Spracherweiterungen. Anderer Ansatz: Meist trace-basiert (Berechnung wird vollständig aufgezeichnet). Trace wird danach mittels spezieller Viewer analysiert (meist Präsentation mit innermost-sicht). Wichtigstes Tool: Hat Bsp: Insertion Sort sort :: Ord a => [a] -> [a] sort [] = [] sort (x:xs) = insert x (sort xs) insert :: Ord a => a -> [a] -> [a] insert x [] = [x] insert x (y:ys) x <= y = x:y:ys otherwise = x : insert x ys... 33
34 In Hat können alle Tools gleichzeitig verwendet werden. Nachteile ggü Observationen: Programmtransformationen (langsamere Üversetzung, auf Haskell98 beschränkt) langsamere Programmausführung sehr groÿer Trace 13. Rekursion In Programmen haben wir unterschiedliche Formen von Rekursionen kennengelernt. Hierbei werden meist Datenstrukturen (Bäume) durchlaufen. Bekanntes Konzept für Baumalgorithmen Attributierte Grammatiken Sei G = (N, Σ, P, S) eine kontextfreie Grammatik. Dann kann G um eine Attributierung erweitert werden: ordne jedem Symbol X N T eine Menge von synthetischen Attributen zu (Syn(X)) ordne jedem Symbol A N eine Menge von inhertiten Attributen zu (Inh(A)) ordne jeder Regel A X 1... X n P eine Menge von semantischen Regeln (Attributgleichungen) der Form a.i = f(a 1 s 1,..., a k s k ) zu mit: a Syn(X i ), falls i = 0 a Inh(X i ), falls i > 0 für alle 1 i k: a i Syn(X ji ), falls ji = 0 a i Inh(X ji ), falls ji > 0 Idee: synthetische Attribute nur hochreichen, inherite Attribute nur runterreichen. f lokaler Bezeichner, wird später interpretiert, z.b. durch Hashfunktion Beachte: keine Zykel innerhalb einer Regel Attributierung eines Ableitungsbaumes: Zuordnung von Attributen zu Baumknoten. Berechnung gemäÿ Attributsgleichungen der Regeln Bsp: Binärzahlen, rein synthetisch B 0, v.0 = 0 B 1, v.0 = 1 L B, v.0 = 1, l.0 = 1 L LB, v.0 = 2 v.1 + v.2, l.0 = l N L, v.0 = v.1 N L.L, v.0 = v.1 + v.3/2 l.3 34
35 Baum für Nur synthetische Attribute: Syn(N) = Syn(B) = {v} Syn(L) = {v, l} Syn(0) = Syn(1) = Alternative mit inheritem Attribut s für Stelle: B 0, v.0 = 0 B 1, v.0 = 2 s.0 L B, v.0 = v.1, l.0 = 1, s.1 = s.0 L LB, v.0 = v.1 + v.2, l.0 = l.1 + 1, s.1 = s.0 + 1, s.2 = s.1 N L, v.0 = v.1, s.1 = 0 N L.L, v.0 = v.1 + v.3, s.1 = 0, s.3 = l.3 Spezialfälle: reine s-attributierung L-Attributierung: keine Abhängigkeit von rechts nach links, d.h. alle Attributierungen haben die Form: a.i = f(a 1 j 1,..., a k j k ) mit i > 0 i > j k zyklische Attributierung: unerwünscht, entscheidbar Wie kann man Attributierung eines Baumes berechnen? des Ableitungsbaumes als Haskell-Datenstruktur: Repräsentation data N = N1 L N2 L L -- Terminal entfälllt data L = L1 B L2 L B data B = O I testbin = N2 (L2 (L2 (L2 (L1 I) I) O) I) (L2 (L1 O) I) Übergang von Wort ( ) zu Baum durch Parser, später. Dann kann s-attributierung wie folgt umgesetzt werden: synthetische Attribute sind Ergebnisse des Baumdurchlaufs (ggf Tupel bei mehreren Attributen) be :: B -> Int be O = 0 be I = 1 le :: L -> (Int,Int) le (L1 b) = (be b, 1) le (L2 l b) = let (v1,l1) = le l in (2*v1+be b, l1+1) ne :: N -> Int ne (N1 l) = fst (le l) ne (N2 l1 l3) = let (v1,_) = le l1 (v3,l3)= le l3 in v1+v3 / 2**l3 Wie kann L-Attributierung umgesetzt werden? Summe der Zweierpotenzen Binärzahlberechnung als B 0, v.0 = 0 35
36 B 1, v.1 = 1 L 3, v.0 = p.0 v.1 L LB, p.1 = 2 p.0, v.0 = v.1 + p.0 v.2 N L, p.1 = 1, v.0 = 1 Inherite Attribute können als Parameter an Funktionen für Baumdurchlauf übergeben werden. be :: B -> Int be O = 0 be I = 1 le :: L -> Int -> Int le (L1 b) p0 = p0 * be b le (L2 l b) p0 = le l (2*p0) + p0 * be b ne :: N -> Int ne (N l) = le l 1 Kann diese Technik auch auf beliebige zykelfreie Attributierungen angewendet werden? B 0, v.0 = 0 B 1, v.0 = 2 s.0 L B, v.0 = v.1, l.0 = 1, s.1 = s.0 L LB, v.0 = v.1 + v.2, l.0 = l.1 + 1, s.1 = s.0 + 1, s.2 = s.0 N L, v.0 = v.1, s.1 = 0 N L.L, v.0 = v.1 + v.3, s.1 =... be :: B -> Int -> Int be O s0 = 0 be I s0 = 1 le :: L -> Int -> (Int,Int) le (L1 b) s0 = let v1 = be b s0 in (v1,1) le (L2 l b) s0 =... ne :: N -> Int ne (N1 l) = let (v1,_) = le l 0 in v1 ne (N2 l l') = let (v1,_) = le l 0 (v3,l3) = le l' (-l3) in v1+l3 Problem scheint Denition von (v3,l3) = le l' (-l3) zu sein - Nichttermination? Aber Ausführung zeigt: ne testbin Lazy Evaluation zerlegt Rekursion in mehrere Baumdurchläufe. Berechnung von l3 ist unabhängig von s-parameter (-l3). 36
37 Allgemein gilt: Für jede zykelfreie Grammatik stellt obiges Implementierungsschema einen ezienten Attributauswerter dar. Für s-/l-attributierung stellt diese Technik auch eine Implementierung für strikte/imperative Sprachen dar. Beachte: s-/l-attributierungen sind ezient Continunation-Passing-Style Abbrüche während eines Baumdurchlaufs: Betrachte noch einmal data Exp = Exp :+: Exp Exp :/: Exp Num Float Wir haben eine Funktion deniert: eval :: Exp -> Maybe Float mit Hilfe der Maybe-Monade. Untersucht man den Baumdurchlauf, so ergibt sich:... D.h. nach Auftreten von Nothing soll Baumdurchlauf gestoppt werden (angezeigt durch Ergebnis Nothing). Aber Stop wird durch jeden Knoten oberhalb durch Pattern Matching erneut festgestellt und weiterpropagiert. Kann Stop nicht direkt an der Fehlerstelle greifen? Idee: Stelle Berechnung (Ergebnis des Baumdurchlaufes) als Continuation (Fortsetzung) dar. Dann kann im Fehlerfall Continuation verworfen werden und der Fehler direkt zurückgegeben werden. Programmierstil: Beispiel: Continuation Passing Style (CPS) Continuation :: Float -> Maybe Float eval :: Exp -> (Float -> Maybe Float) -> Maybe Float eval (Num x) c = c x eval (e1 :+: e2) c = eval e1 (\v1 -> eval e2 (\v2 -> c (v1+v2))) eval (e1 :/: e2) c = eval e1 (\v1 -> eval e2 (\v2 -> if vs == 0 then Nothing else c (v1/v2))) Dann: eval (Num 1 :/: (Num 1 :+: Num (-1))) Just Nothing Oder besser: eval (e1 :/: e2) c = eval e2 (\v2 -> if vs == 0 then Nothing else eval e1 (\v1 -> c (v1/v2))) Weitere Berechnung als Argument kann modiziert werden. Endrekursiver Aufruf (wenig Stackverbrauch) 37
38 Nützliche für Exceptions, mehrfache Baumdurchläufe, Coroutining Auch S- und L-attributierte Grammatiken können mittels CPS implementiert werden. Beispiel: S-Attributierung zur Berechnung von binären Gleitkommazahlen: B 0 1, v.0 = 0 v.0 = 1 L B LB, v.0 = v.1, l.0 = 1 v.0 = 2 v.1 + v.2, l.0 = l N L L.L, v.0 = v.1 v.0 = v.1 + v.3/2 l.3 be :: B -> (Float -> a) -> a be O c = c 0 be I c = c 1 le :: L -> (Float -> Float -> a) -> a le (L1 b) c = be b (\v1 -> c v1 1) -- = be b (flip c 1) le (L2 l b) c = le l (\v1 l 1 -> be b (\v2 -> c (2*v1+v2) (l1+1))) ne :: N -> (Float -> a) -> a ne (N1 l) c = le l (\v1 _ -> c v1) ne (N2 l l') c = le l (\v1 _ -> le l' (\v3 l3 -> c (v1+v3/2**(-l3)))) Dann: ne testbin id Parserkombinatoren Def: Gegeben ist eine kontextfreie Grammatik G = N, Σ, P, S und ein Wort W Σ. Ein Programm, welches w L(G) entscheidet, heiÿt Parser für G. Zusätzlich sollte ein Parser noch eine Ausgabe liefern, welche abstrakte Informationen über die Eingabe enthält (z.b. Linksableitung, abstrakter Syntaxbaum (AST)). Unterschiedliche Ansätze zur Implementierung von Parsern: Tools wie yacc generieren Parser aus einer Grammatik Rekursive Abstiegsparser, z.b. Pascal [Wirth], XML Kombinatoren zum Aufbau der Grammatik 14.1 Eine Kombinatorbibliothek für Parser [Swiestra '99] Abstrakt: type Parser s a = -- s Tokentyp, a Ergebnistyp (z.b. AST) [s] -> [(a,[s])] Kombinatoren zum Aufbau von Parsern: 38
39 psucceed :: a -> Parser s a psucceed v ts = [(v,ts)] pfail :: Parser s a pfail ts = [] ppred :: (s -> Bool) -> Parser s s ppred p (t:ts) p t = [(t,ts)] otherwise = [] psym :: Eq s => s -> Parser s s psym t = ppred (t==) infixl 3 < > -- muss direkt hinter den Imports stehen infixl 4 <*> infixl 4 <$> (< >) :: Parser s a -> Parser s a -> Parser s a (p < > q) ts = p ts ++ q ts (<*>) :: Parser s (a -> b) -> Parser s a -> Parser s b p <*> q ts = [ (v1 v2,ts2) (v1,ts1) <- p ts, (v2,ts2) <- q ts1 ] (<$>) :: (a -> b) -> Parser s a -> Parser s b f <$> p = psucc f <*> p Bsp: Parser für {a n b n n N} Grammatik: S asb ε anbn :: Parser Char Int anbn = ((((\_ n _ -> n+1) <$> psym 'a') <*> anbn) <*> psym 'b') < > psucceed 0 Dann: anbn "aabb" [(2,""),(0,"aabb")] Vorteil: Grammatik direkt als Programm groÿe Sprachklasse erkennbar ( k N Ll(k)) Grammatik dynamisch anpaÿbar (<*) :: Parser s a -> Parser s b -> Parser s a p <* q = (\x _ -> x) <$> p <*> q (*>) :: Parser s a -> Parser s b -> Parser s b (<**>) :: Parser s a -> Parser s (a -> b) -> Parser s b check :: (a -> Bool) -> Parser s a -> Parser s a check pred p ts = filter (pred. fst) (p ts) Dann: anbn = (1+) <$> psym 'a' *> anbn *< psym 'b' < > psucceed 0 39
40 Bsp: XML-Parser (ohne Attribute) data XML = Tag String [XML] Text String pxmls :: Parser Char [XML] pxmls = (:) <$> pxml <*> pxmls < > psucceed [] pxml :: Parser Char XML pxml = flip Tag [] <$> poctag < > (\otag,xmls,_) -> Tag otag XMLs) <$> check (\(otag,_,ctag) -> otag == etag) ((,,) <$> po < > Text <$> ptext ptext :: Parser Char String ptext = (:) <$> ppred ('<'/=) <*> ptext < > psucceed "" potag :: Parser Char String potag = psym '<' *> pident <* psym '>' pctag :: Parser Char String pctag = psym '</' *> psym '/' *> pident <* psym '>' pident :: Parser Char String pident = (:) <$> ppred ('>'/=) <*> pident < > psucceed "" Zusammenhang mit attributierten Grammatiken: S A B, s.0 = f(s1, s2) ps = (\s1 s2 -> f s1 s2) <$> pa <*> pb Schöner wäre es, wenn pctag das Parserergebnisses von potag überprüfen würde, also als Parameter bekommt. pctag' :: Parser Char () pctag' "" = psucceed () pctag' (c:cs) = psym c '</' *> pctag' cs (<->>) :: Parser s a -> (a -> Parser s b) -> Parser s b (p <->> q f) ts = [ res (pv,ts1) <- p ts, res <- q f pv ts 1 ] Dann: pxml = flip Tag [] <$> poctag < > potag <->> (\tag -> pxmls <* pctag' tag) < > Text <$> ptext Somit nicht nur kontextfreie Parser spezizierbar, sondern auch Parser für kontestsensitive Eigenschaften. Nicht erst über Attributierung testen Monadische Parserkombinatoren Sind Parser auch Monaden? bind vorhanden: <->> neutrales Element vorhanden: psucceed Assoziativität gilt auch 40
41 Also ja. Implementierung: newtype Parser s a = P ([s] -> [(a,[s])]) instance Monad (Parser s) where return v = Parser (\ts -> [(v,ts)]) P p >>= q f = P (\ts -> [ res (pv,ts1) <- p ts, let P q = q f pv, res <- q ts1 ]) fail _ = P (\ts -> []) instance MonadPlus (Parser s) where (++) = (< >) zero = P (const []) (< >) = Parser s a -> Parser s a -> Parser s a (P p) < > (P q) = P (\ts -> p ts ++ q ts) psym :: Eq s -> s -> Parser s s psym s = ppred (x==) ppred :: (s -> Bool) -> Parser s s ppred pred = P (\ts -> case ts of t:ts' pred t -> [(t,ts')] _ -> []) Dann monadische Denition für {a n b n n N}: anbn = do psym 'a' x <- anbn psym 'b' return (x+1) < > return 0 Monadenstarter: runparser :: Parser s a -> [s] -> [(a,[s])] runparser (P p) ts = p ts Dann: runparser anbn "aabb" [(2,""),(0,"aabb")] Analog für XML: pxmls = do xml <- pxml xmls <- pxmls return (xml.xmls) < > return [] pxml = do octag <- poctag return (Tag octag []) < > do otag <- potag xmls <- pxmls -- ctag <- pctag 41
42 -- guard (otag == ctag) pctag' otag return (Tag otag xmls) < > do t <- ptext return (Text t) Bem: Parserkombinatoren implementieren eine sogenannte top-down-analyse. Hier gibt es Probleme mit Linksrekursion: S Sa ε Mit Parserkombinatoren: s = (+1) <$> s <* a < > psucceed 0 Nichttermination, da Parser von links nach rechts probiert werden. Lösung: Übersetzung der Linksrekursion in Rechtsrekursion (S as ε), allgemeines Verfahren kompliziert. Optimierung: brauchen. Deshalb: deterministische Variante < > oft inezient, da (nicht ausgewertete) Alternativen Speicher (< >) :: Parser s a -> Parser s a -> Parser s a (p < > q) ts = case p ts of [] -> q ts ts -> ts -- oder sogar: -- (t:ts) -> [t] Entsprechend für monadische Parser Weitere Features von Parserkombinatoren: Exceptions zur Fehlerbehandlung Zustandsmonade (globalen Zustand während des Parsens) Zeileninformationen zur besseren Fehlergenerierung Korrektur der Eingabe [Swierstra '99] Weitere Infos: haskell.org -> Libraries+Tools -> Parser 14. Algorithmen und Datenstrukturen Bisher kennengelernt: Listen und Bäume (insbesondere Suchbäume) Was sind geeignete Algorithmen und Datenstrukturen für häuge Fragestellungen? 14.1 Listen Listen sind gut, wenn Daten gesammelt werden und anschlieÿend jedes Element verwendet wird. Bsp: fac n = foldr (*) 1 [0..n] 42
43 Konkatenation von Listen ist linear im ersten Argument, deshalb unschön: xs ++ [x]. Aber manchmal läÿt sich dies nur schwer vermeiden (z.b. bei Rekursion). data Tree = Node Tree Tree Leaf Int treetolist :: Tree -> [Int] treetolist (Leaf n) = [n] treetolist (Node tl tr) = treetolist tl ++ treetolist tr Kein linearer Aufwand, da erste Arumente von ++ mit Baumgröÿe wachsen. Bei entartetem Baum O(n 2 ) mit n Anzahl der Knoten/Blätter. Verbesserung durch CPS? treetolist :: Tree -> (Int -> [Int]) -> [Int] treetolist (Leaf n) c = c n treetolist (Node tl tr) c = treetolist tl (: (treetolist tr c)) Dann treetolist t (:[]) linear in t. Aber wenig kompositionell, wie ähnliches Beispiele show zeigt: show :: Show a => a -> String Bei geschachtelten Typen müssen also unterschiedliche Show-Instanzen (für die einzelnen (Teil-)Typen) komponiert werden: Show [Tree] showlist, showtree, showint Concatenations müÿten durch alle Show-Instanzen durchgereicht werden (so nicht in Haskell deniert): class ShowC a where showc :: a -> (String -> String) -> String instance ShowC Tree where showc (Leaf n) = showc n (\nstr -> "Leaf " ++ c nstr) showc (Node tl tr) c = showc tl (++ (showc tr c)) Bem: Wir verwenden zwar (++), aber dies wird nur mit kurzen Listen aufgerufen. instance ShowC a => ShowC [a] where showc [] c = c "[]" showc (x:xs) c = showc x (++ ':' : showc xs c) Dann: Umwandlung von Datenstrukturen in Liste in linearer Zeit. Bessere (kompositionellere?) Alternative: Akkumulatortechnik treetolist :: Tree -> [Int] -> [Int] treetolist (Leaf n) ns = n:ns treetolist (Node tl tr) ns = treetolist tl (treetolist tr ns) Ähnlich unkompositionell wie CPS, aber: 43
44 treetolist :: Tree -> [Int] -> [Int] treetolist (Leaf n) = (:) n treetolist (Node tl tr) = treetolist tl. treetolist tr ist genauso kompositionell wie die Lösung mit (++)! (.) statt (++), (:) n statt [n], partielle Applikation. Solche Listen nennt man funktionale Listen Geschönte Typsignatur durch: type IntListF = [Int] -> [Int] treetolist :: Tree -> IntListF Auch die Klasse Show verwendet funktionale Listen: type ShowS = String -> String class Show a where showsprec :: Int -> a -> ShowS showsprec _ x = (++(show x)) show :: a -> String show x = showsprec 0 x "" instance Show Tree where showsprec p (Leaf n) = showstring "Leaf ". showsprec p n showsprec p (Node tl tr) = showstring "Node (". showsprec p tl. showstring ") (". showsprec p tr. showchar ')' wobei: showchar :: Char -> ShowS showchar = (:) showstring = String -> ShowS ShowString = (++) Listen: instance Show a => Show [a] where showsprec _ [] = showstring "[]" showsprec p (x:xs) = showsprec p x. showchar ':'. showsprec p xs Auÿerdem: shows = showsprec 0 showlist vordeniert als z.b. "[1,2,3]" für [1,2,3]::[Int], welche für Char- Instanz überschrieben wird mit "Huhn" für ['H','u','h','n']::[Char] 44
45 14.2 Stacks (LIFO) Zur Implementierung von Stacks eignen sich Listen ausgezeichnet. push = (:) pop = tail top = head 14.3 Queues (FIFO) Erste Implementierung: Liste enter x q = q ++ [x] remove = tail top = head Für kleine Queues gut. Aber für gröÿere Quees weniger, da enter linear in Queuegröÿe. Verbesserung: verwende zwei Listen: data Queue a = Q [a] [a] enter :: a -> Queue a -> Queue a enter x (Q outs ins) = Q (outs (x:ins)) top :: Queue a -> Maybe a top (Q [] []) = Nothing top (Q [] ins) = Just (last ins) top (Q (o:outs) _) = Just o remove :: Queue a -> Queue a remove (Q [] []) = Q [] [] remove (Q [] ins) = remove (Q (reverse ins) []) remove (Q (_:outs) ins) = Q outs ins Schöner (top und remove in einer Operation): takeq :: Queue a -> Maybe (a, Queue a) takeq (Q [] []) = Nothing takeq (Q [] ins) = takeq (Q (reverse ins) []) takeq (Q (o:outs) ins) = Just (o, Q outs ins) Laufzeit: in den meisten Fällen konstant, Ausnahmen: leere out-liste, dann linear in Queuegröÿe Aber: Je gröÿer die Queue ist, desto seltener muÿ reverse ausgeführt werden amortisierte Laufzeit konstant 14.4 Arrays Haskell98 stellt Arrays im Modul Array zur Verfügung. Interface: array :: Ix a => (a,a) -> [(a,b)] -> Array a b -- Ix a: a kann als Index verwendet werden 45
46 Bsp: array (1,5) [(i,i+1) i <- [1..5]] listarray :: Ix a => (a,a) -> [b] -> Array a b listarray (1,5) [i+1 i <- [1..5]] == array (1,5) [(i,i+1) i <- [1..5]] (!) :: Ix a => Array a b -> a -> b -- Elementzugriff (//) :: Ix a => Array a b -> [(a,b)] -> Array a b -- Elemente einfügen Gut für Daten, die häug nachgeschlagen werden müssen, schlecht bei vielen kleinen Änderungen. Destruktive Updates nicht möglich, da altes Array auch noch verwendet werden kann (referentielle Transparenz). Alternative Implementierung: (hier nur mit Int-Indizes) Braun-Bäume data ArrayB b = Entry b (ArrayB b) (ArrayB b) Idee: Dividiere Index sukzessive durch 2, bis Ergebnis 0; wenn Ergebnis ungerade, steige links ab, sonst rechts. (!) :: ArrayB b -> Int -> b (Entry v al ar)! n n == 0 = v even n = ar! (n `div` 2-1) otherwise = al! (n `div` 2) update :: ArrayB b -> Int -> b -> ArrayB b update (Entry v al ar) n v' n == 0 = Entry v' al ar even n = Entry v al (update ar (n `div` 2-1) v') otherwise = Entry v (update al (n `div` 2) v') ar) Sowohl (!) als auch update logarithmisch im Index. Bei update wird nicht gesamte Datenstruktur kopiert, sondern nur Spine des geänderten Wertes emptyarrayb :: ArrayB b emptyarrayb = Entry (error "ArrayB access to non-instantiated entry") emptyarrayb emptyarrayb ArrayBs können beliebig groÿ werden, keine Dimensionsbeschränkung. Aber: je gröÿer der Index, desto inezienter Bei vollständiger Ausnutzung des Indexbereiches n 2 ausgeglichener binärer Baum der Tiefe n + 1. Vorteil gegenüber destruktivem Update: alte+neue Version verfügbar In imperativen Sprachen: meist Kopie des Arrays Zeit/Platz linear Berechnung der Raumposition erfordert viel Speicher, Optimierung mittels nichtgeboxter Werte, die direkt in Argumentposition statt im Heap gespeichert werden. 46
47 listtoarrayb :: [b] -> ArrayB b listtoarrayb [] = emptyarrayb listtoarrayb (x:xs) = let (ys,zs) = split xs in Entry x (listtoarrayb ys) (listtoarrayb zs) Laufzeit immernoch O(n log n), aber viel bessere Konstante, lineare Implementierung möglich, aber erst bei sehr groÿen Arraygröÿen ezienter nicht relevant für Praxis Höhenbalancierte Suchbäume Nachteil von ArrayBs: Indizes müssen auf Ints 0... n abgebildet werden können. Problematisch bei komplizierten Schlüsseln, bei Übersetzung in Ints entstehen groÿe Löcher. Hashing ist nur begrenzt eine Lösung, da das Finden guter Hashfunktionen schwierig ist. Lösung: Suchbäume Ezient für zufällige Schlüsselverteilung, insert, delete, lookup in O(log n) mit n Zahl der Einträge im Baum. Aber inezient im worst case: O(n). Verbesserung: Höhenbalancierung Gewährleistete Eigenschaft: die Höhe zweier benachbarter Teilbäume unterscheidet sich maximal um 1. Somit Tiefe des Baumes maximal 2*log(n+1) und insert,delete,lookup immer in O(log n). Implementierung siehe InfII, aber eleganter als in imperativen PS Tries Ein alternativer Ansatz zu höhenbalancierten Bäumen. Idee von Thue zur Repräsentation von String-Mengen. Bsp: String-Menge {ear,earl,east,easy,eye} (Baum) Tries sind Bäume, die Schlüssel gemäÿ dieser Idee auf Werte abbilden. type String = [Char] data MapString a = TrieStr (Maybe a) (MapChar (MapStr a)) type MapChar a = [(Char,a)] lookupchar :: Char -> MapChar a -> Maybe a lookupchar = lookup lookupstr :: String -> MapStr a -> Maybe a lookupstr "" (TrieStr mv _) = mv lookupstr (c:cs) (TrieStr _ tcs) = maybe Nothing (lookupstr cs) (lookupchar c tcs) maybe :: b -> (a -> b) -> Maybe a -> b 47
48 maybe n j Nothing = n maybe n j (Just v) = j v do tstr <- lookupchar c tcs lookupstr cs tcs insertstr :: String -> a -> MapStr a -> MapStr a insertstr "" v (TrieStr _ tcs) = TrieStr (Just v) tcs insertstr (c:cs) v (TrieStr v' tcs) = TrieStr v' (case lookupchar c tcs of Nothing -> (c, insertstr cs v emptytriestr):tcs Just tstr -> update c (insertstr cs tstr) tcs) emptytriestr :: MapStr a emptytriestr = TrieStr Nothing [] update :: Eq a => a -> b -> [(a,b)] -> [(a,b)] update k v [] = [(k,v)] update k v ((k',v'):kvs) k == k' = (k,v):kvs otherwise = (k',v'):update kv kvs Beim Löschen von Schlüsseln soll der Baum aufgeräumt werden, um keinen Speicher zu verschwenden. deletestr :: String -> MapStr a -> MapStr a deletestr "" (TrieStr _ tcs) = TrieStr Nothing tcs deletestr (c:cs) (TrieStr v tcs) = case lookupchar c tcs of Nothing -> TrieStr v tcs Just tstr -> case deletestr cs tstr of TrieStr Nothing [] -> TrieStr v (filter ((/=c).fst) tcs tstr' -> TrieStr v (update c tstr' tcs) Beachte: Wenn filter [] und v==nothing, dann wird Knoten in übergeordnetem deletestr gelöscht. Verallgemeinerung für beliebige Datenstrukturen möglich? Beobachtung: Trie-Datenstruktur deckt für jeden Konstruktor einen Baumabstieg mittels eines Arguments ab. Argumente des Konstruktors werden durch entsprechende Tries nacheinander einsortiert. Bei 0-stelligen Konstruktoren endet der aktuelle Schlüssel - hier wird der Wert eingetragen. Jetzt für andere DS: data Bin = Empty I Bin O Bin inttobin :: Int -> Bin inttobin 0 = Empty inttobin n = (if even n then O else I) (inttobin (n `div` 2)) Vergleich von Braunbäumen und Tries für Binärzahlen: (stu) Exakt gleiche Struktur! Nur Umwandlung Int nach Bin korrigieren. Alternative zu insert/delete bei Tries: update 48
49 (code) Noch werden in dieser Implementation die Tries beim Löschen nicht aufgeräumt. Lösung: MapBin a -> Maybe (MapBin a) Weitere Verallgemeinerung: Verwende Bäume als Schlüssel data Tree = Leaf String Node Tree Tree... Laufzeit: lookup, insert, delete sind linear in der Gröÿe des Schlüssels. Im Gegensatz dazu sind höhenbalancierte Suchbäume logarithmisch in der Anzahl der Einträge im Suchbaum Graphen Mathematisch sind Graphen def. als G = (V, E) mit V Menge von Knoten und E V V von Kanten. Zusätzlich fügt man häug noch Beschriftungen zu Knoten und Kanten hinzu. Einfachste Codierung in Haskell: type Graph a b = (Nodes a, Edges b) type NodeId = Int type Nodes a = [(NodesId,a)] type Edges b = [(NodeId,b,NodeId)] Zunächst nur Überlegungen zur Schnittstelle, Ezienzbetrachtung zur Implementation später. Viele (imperative) Graphalgorithmen arbeiten mit Markierungen. Ähnliches wäre auch in unserem Framework möglich, z.b. durch Erweiterung um boolesche Komponente der Markierung. Allerdings entspricht das nicht der üblichen induktiven Programmierung in funktionalen Sprachen. Schöner wäre eine induktive Sicht der Graphen, wie: leere Graphen, Konstruktor emptygraph Graph, der aus einem Knoten (mit seinem Kontext, also den ein- und ausgehenden Knoten) und einem Restgraph besteht; Konstruktor &v rekursiv die zugehörige NodeId ist. type Context a b = ([(NodeId,b)],a,[(NodeId,b)]) Dann können wir die Tiefensuche auf Graphen wie folgt denieren: dfs :: [NodeId] -> Graph a b -> [NodeId] dfs [] _ = [] dfs (v:vs) ((_,_,succs) &v g) = v:dfs (map fst succs ++ vs) g dfs (v:vs) g = dfs vs g 49
50 Problematisch ist natürlich das doppelte Vorkommen von v in den Pattern (Nicht- Linearität) und der parametrische Konstruktor. Als Lösung kann dieses Matching mit Hilfe einer Funktion umgesetzt werden: match :: NodeId -> Graph a b -> Maybe (Context a b, Graph a b) Dann: dfs [] _ = [] dfs (v:vs) g = case match v g of Nothing -> dfs vs g Just ((_,_,succs),g') -> v:dfs vs (map fst succs ++ vs) Wie kann match deniert werden? Suche des Knotens und Aufsammeln der Vorgänger- und Nachfolgeknoten: match n (nodes,edges) = do a <- lookup n nodes return (([(n,b) (n,b,n') <- edges, n' == n], a, [(n,b) (n',b,m) <- edges, n' == n]), (filter ((/=n) fst) nodes, [(n,b,n') (m,b,m') <- edges, m /= n, m' /= n])) Konstruktion von Graphen: addnode :: NodeId -> a -> Graph a b -> Graph a b = maybe ((n,a):nodes,edges) (error ("Node" ++ show n ++ "already in Graph")) (lookup n nodes) addedge :: NodeId -> b -> NodeId -> Graph a b -> Graph a b addedge n b m (nodes,edges) = maybe... Bem: Hier soll es mehr auf die Idee der Schnittstelle ankommen, Repräsentation des Graphen so...ziert Eziente Darstellung: Braunbäume oder höhenbalancierte Suchbäume, die NodeIds auf Vorgänger-/Nachfolgerlisten abbilden. Hierdurch Implementierung von match komplizierter, aber ezient möglich. Dann: Knoten/Kanten hinzufügen und matchen logarithmisch in Graphgröÿe. Weitere Verbesserungsmöglichkeiten: NodeIds abstrakt in der Graphdatenstruktur generieren Monadische Variante mit Graph als Zustand, dann z.b. möglich: do n <- addnode "a" m <- addnode "b" AddEdge n 42 m 50
51 15. Domain Specic Language Eine Beispielanwendung: CGI-Programmierung mit Haskell: Sudoku 16. Nebenläuge Programmierung Moderne PS stellen Konzepte zur nebenläugen Programmierung zur Verfügung. Nützlich bei reaktiven Systemen, GUI-Programmiererung, verteilte Programmierung, aber auch zur eleganten Formulierung von Algorithmen Vorteile: Threads (oder Prozesse) sind orthogonales Modularisierungskonzept zu Funktionen/Prozeduren Erlauben quasiparallele Ausführung von Berechnungen/Aktionen Aber: durch Nebenläugkeit haben wir meist keine Ezienzsteigerung (evtl bei Mehrprozessorsystemen, aber oft nicht unterstützt) Idee: Im nebenläugen System werden mehrere Programme (Threads) gleichzeitig ausgeführt Wichtige Aspekte: Generierung neuer Threads Terminierung von Threads Synchronisation Kommunikation 16.1 Concurrent Haskell Als Erweiterung von Haskell'98 zur nebenläugen Programmierung hat sich Concurrent Haskell bewährt. Threads sind Haskell-Programme vom Typ IO (). Generierung neuer Threads: forkio :: IO () -> IO ThreadID -- in HUGS: IO () -> IO () Terminierung anderer Threads: killthread :: ThreadID -> IO () Abruf der eigenen ThreadID: mythreadid :: IO ThreadID 51
52 Alternativ können Threads durch Programmende terminieren. Synchronisation mittels veränderbarer Variablen (mutable variables, MVar) Ähnlich wie IORefs, aber zusätzlich Synchronisation: MVar kann leer oder gefüllt sein. data MVar a -- abstract Generierung einer MVar: newemptymvar :: IO (MVar a) newmvar :: a -> IO (MVar a) takemvar :: MVar a -> IO a liest Wert aus gefülltem MVar, anschlieÿend MVar leer. Ausführen der Threads suspendiert, falls MVar leer. Wiedererweckung, wenn MVar gefüllt wird. putmvar :: MVar a -> a -> IO () schreibt Wert in leere MVar. Ausführender Thread suspendiert, falls MVar gefüllt. Wiedererweckung, wenn MVar geleert wird. Scheduling: Zwei wichtige Varianten: präemptives: Scheduler teilt Rechenzeit zwischen Prozessen auf, in ghc implementiert kooperatives: Threadwechsel nur nach Suspension oder explizit durchyield :: IO (), in hugs implementiert Wann verhält sich ein nebenläuges Programm korrekt? Wenn es sich unter allen möglichen Umständen korrekt verhält. Manchmal auch Einschränkung auf alle fairen Scheduler. Weitere Funktionen auf MVars: isemptymvar :: MVar a -> IO Bool trytakemvar :: MVar a -> IO (Maybe Bool) trytakemvar m = do b <- isemptymvar m if b then return Nothing else takemvar m >>= return Just Kann blockieren, da isemptymvar und takemvar nicht atomar ausgeführt werden müssen. Aber trytakemvar atomar vordeniert. Analog: tryputmvar :: MVar a -> a -> IO Bool 52
53 readmvar :: MVar a -> IO a Wie takemvar, aber ohne leeren swapmvar :: MVar a -> a -> IO a verändere MVar und liefere alten Wert, atomar. Sind MVars adäquat zur Synchronisation? Recht einfach zu verwenden als Semaphoren, aber Semaphoren können mit MVars simuliert werden: data QSem = QSem (MVar (Int,[MVar ()])) newqsem :: Int -> IO QSem neqqsem n = do sem <- newmvar (n,[]) return (QSem sem) waitqsem :: QSem -> IO () waitqsem :: (QSem sem) = do (avail,blocked) <- takemvar sem if avail > 0 then putmvar sem (QSem (avail-1,[])) else do block <- newemptymvar putmvar sem (QSem (0,blocked++[block])) takemvar block signalqsem :: QSem -> IO () signalqsem (QSem sem) = do (avail,blocked) <- takemvar sem case blocked of [] -> putmvar sem (QSem (avail+1,[])) (block:blocked) -> do putmvar block () putmvar sem (QSem (0,blocked)) MVars können auch als einelementiger Kanal (Puer) gesehen werden. Hiermit ergibt sich naher Bezug zum Message Passing (Kommunikation durch Nachrichtenaustausch). Aus Programmiersicht wäre beim Message Passing natürlich ein unbeschränkter Kanal wünschenswert. Mit MVars implementierbar? Als MVar über Queue möglich, dann aber kein gleichzeitiger lesender und schreibender Zugri auf nichtleeren Kanal möglich. Andere Implementierung: Verwende MVars wie Zeiger und implementiere Kanal direkt. data Chan a = Chan (MVar (Stream a)) -- read end (MVar (Stream a)) -- write end type Stream a = MVar (ChItem a) data ChItem a = ChItem a (Stream a) newchan :: IO (Chan a) newchan = do hole <- newemptymvar read <- newmvar hole write <- newmvar hole return (Chan read write) 53
54 writechan :: Chan a -> a -> IO a writechan (Chan _ write) v = do newhole <- newemptymvar oldhole <- takemvar write putmvar write newhole putmvar oldhole (ChItem v newhole) readchan :: Chan a -> IO a readchan (Chan read _) = do readend <- takemvar read ChItem v newreadend <- takemvar readend putmvar read newreadend return v isemptychan :: Chan a -> IO Bool isemptychan (Chan read write) = withmvar read $ \r -> do w <- readmvarwrite return (r == w) Beachte: Falls ein anderer Thread mit readchan suspendiert ist, blockiert auch isemptychan. So in ghc implementiert. Weitere Funktionen: dupchan :: Chan a -> IO (Chan a) sowas wie Broadcast-Channel; Ergebniskanal zunächst leer, aber alle Werte, die danach in Chan geschrieben werden, landen in beiden Channels. ungetchan :: Chan a -> a -> IO () Wert am read-ende in Chan einfügen. getchan :: Chan a -> IO [a] -Strom aller Chan-Nachrichten writelisttochan :: Chan a -> [a] -> IO () writelisttochan = mapm_ writechan Nachteil an Concurrent Haskell: lock-basierte Synchronisation kann in gröÿeren Systemen kompliziert werden, Code z.t. schwer durchschaubar und wenig skalierbar. Komposition von Komponenten schwierig, wenn Interna nicht bekannt Memory Transactions Aus dem DB-Bereich bekannt: Transaktionen. DB-Transaktionen werden deniert und dann atomar durchgeführt, oder verworfen bzw neu gestartet, wenn Konikte auftreten. Keine expliziten Locks. Kann dieses Konzept zur nebenläugen Programmierung eingesetzt werden? Ja, Software Transactional Memory (STM, in ghc ab Version 6.2): Control Concurrent STM 54
55 Interface: data STM a -- abstract instance Monad STM throw, catch -- später atomically :: STM a -> IO a -- starte Transaktion retry :: STM a -- Verwurf einer Transaktion orelse :: STM a -> STM a -> STM a -- alternative Transaktion data TVar a -- abstract newtvar :: a -> STM (TVar a) readtvar :: TVar a -> STM a writetvar :: TVar a -> a -> STM a -- wie IORefs, aber in STM-Monade Beachte: IN STM-Monade können keine IO-Aktionen ausgeführt werden. Somit keine Eekte auf die Welt - Verwerfen/Neustarten von STM-Aktionen möglich. Beachte: Transaktionen können (sequentiell) komponiert werden, ohne zu wissen, wie ihr eigentliches Kommunikationsverhalten ist. Der Programmierer weiÿ nur, daÿ sie entweder ganz ausgeführt werden oder ein Problemfall die ganze Transaktion neugestartet hat. Weitere Möglichkeit der Komposition: Alternativen. Erlang Funktionale Sprache, von Ericsson entwickelt. Prolog Erlang (prologähnliche Syntax) Ungetypt, bis auf schwaches Laufzeittypsystem für Basistypen Strikte Auswertung Bsp: fac(0) -> 1; fac(n) when N > 0 -> N * fac (N-1). In erl: > c(fac). > fac:fac(5). 120 > Einfacher Termaufbau (mit Applikationsklammern auf rechter Seite). Statt let-ausdrücken bind-once-variable und Sequenzen: fac(n) when n > 0 -> N1 = N-1, F1 = fac(n-1), N1*F1. 55
56 Datenstrukturen: Atome: a, b, ok,..., 0, 1,..., hallo, 'Hallo' Listen: [e l] statt e:l, wobei e und l aber beliebige Datenstrukturen sein können. Aber vordenierte Funktionen erwarten, daÿ l wieder Liste ist. Leere Liste []. Abkürzung: [e1,e2,e3] Tupel: {e1,...,en} Pattern Matching: pat=e, wobei pat Konstruktorterm mit Variable ist Semantik: Reduziere e zur Normalform v (reiner Konstruktorterm), matche pat gegen v, falls paÿt Substitution, die auf gesamte verbleibende rechte Seite angewandt wird (inklusive anderer Pattern) Verzweigung: case e of pat1 -> e1 ; patn -> en end 16.1 Nebenläuge Programmierung Eine Erlang-Umgebung wird als Erlang-Knoten bezeichnet, auf dem mehrere Prozesse nebenläug ausgeführt werden können. Starten neuer Prozesse: spawn(module,func,[v1,...,vn]) generiert Prozeÿ, der beginnt, module:func(v1,...,vn) zu berechnen, funktionales Ergebnis: pid des neuen Prozesses. Senden von Nachrichten an Prozess: pid! v Empfangen von Nachrichten: receive pat1 -> e1 ; patn -> en after t -> e end self(): Konstante mit pid des aktuellen Prozesses 56
57 Bsp: Datenbank-Server zum Speichern von Schlüssel-Wert-Paaren: -module(database). -export([start/0]). start() -> database([]). database(es) -> receive {allocate,key,p} -> case lookup(key,es) of nothing -> P! free, receive {value,v,p} -> database([{key,v} Es]) end; {just,v} -> P! allocated, database(es) end; {lookup,key,p} -> P! lookup(key,es), database(es) end Verteilte Programmierung Starte mehrere Knoten in Informatik: erl -name willi erzeugt Knoten remote-start von Threads: spawn(knoten,module,func,[v1,...,vn]) Wie spawn, Sicherung durch Cookies. pid ist netzwerkweit eindeutig! Für Erstkontakte in oene verteilte Systeme: Registrierung von speziellen Prozessen: register(name,pid). Senden an registrierten Prozess: {name,knoten}! v Sollte nur für Erstkontakt verwendet werden, danach pid-austausch, um z.b. Skalierbarkeit zu ermöglichen. 57
Einführung in Haskell
Einführung in Haskell Axel Stronzik 21. April 2008 1 / 43 Inhaltsverzeichnis 1 Allgemeines 2 / 43 Inhaltsverzeichnis 1 Allgemeines 2 Funktions- und Typdefinitionen 2 / 43 Inhaltsverzeichnis 1 Allgemeines
Der λ-kalkül. Frank Huch. Sommersemester 2015
Der λ-kalkül Frank Huch Sommersemester 2015 In diesem Skript werden die Grundlagen der Funktionalen Programmierung, insbesondere der λ-kalkül eingeführt. Der hier präsentierte Stoff stellt einen teil der
Funktionale Programmierung
Funktionale Programmierung Jörg Kreiker Uni Kassel und SMA Solar Technology AG Wintersemester 2011/2012 2 Teil II Typen mit Werten und Ausdruck, sogar listenweise 3 Haskell Programme Programm Module ein
Was bisher geschah. deklarative Programmierung. funktionale Programmierung (Haskell):
Was bisher geschah deklarative Programmierung funktional: Programm: Menge von Termgleichungen, Term Auswertung: Pattern matsching, Termumformungen logisch: Programm: Menge von Regeln (Horn-Formeln), Formel
Funktionale Programmierung mit Haskell
Funktionale Programmierung mit Haskell Dr. Michael Savorić Hohenstaufen-Gymnasium (HSG) Kaiserslautern Version 20120622 Überblick Wichtige Eigenschaften Einführungsbeispiele Listenerzeugung und Beispiel
Funktionale Programmierung
Funktionale Programmierung Mitschrift von www.kuertz.name Hinweis: Dies ist kein offizielles Script, sondern nur eine private Mitschrift. Die Mitschriften sind teweilse unvollständig, falsch oder inaktuell,
Einführung in die funktionale Programmierung
Einführung in die funktionale Programmierung Prof. Dr. Manfred Schmidt-Schauÿ Künstliche Intelligenz und Softwaretechnologie 26. Oktober 2006 Haskell - Einführung Syntax Typen Auswertung Programmierung
Funktionale Programmierung ALP I. Funktionen höherer Ordnung. Teil 2 SS 2013. Prof. Dr. Margarita Esponda. Prof. Dr.
ALP I Funktionen höherer Ordnung Teil 2 SS 2013 Funktionen höherer Ordnung Nehmen wir an, wir möchten alle Zahlen innerhalb einer Liste miteinander addieren addall:: (Num a) => [a -> a addall [ = 0 addall
Programmiersprachen und Übersetzer
Programmiersprachen und Übersetzer Sommersemester 2010 19. April 2010 Theoretische Grundlagen Problem Wie kann man eine unendliche Menge von (syntaktisch) korrekten Programmen definieren? Lösung Wie auch
Theoretische Grundlagen der Informatik
Theoretische Grundlagen der Informatik Vorlesung am 12.01.2012 INSTITUT FÜR THEORETISCHE 0 KIT 12.01.2012 Universität des Dorothea Landes Baden-Württemberg Wagner - Theoretische und Grundlagen der Informatik
Paradigmen der Programmierung
SS 11 Prüfungsklausur 25.07.2011 Aufgabe 5 (6+9 = 15 Punkte) a) Bestimmen Sie jeweils den Typ der folgenden Haskell-Ausdrücke: ( 1, 2 :"3", 4 < 5) :: (Char, String, Bool) [(last, tail), (head, take 5)]
Programmierkurs Java
Programmierkurs Java Dr. Dietrich Boles Aufgaben zu UE16-Rekursion (Stand 09.12.2011) Aufgabe 1: Implementieren Sie in Java ein Programm, das solange einzelne Zeichen vom Terminal einliest, bis ein #-Zeichen
Grundbegriffe der Informatik
Grundbegriffe der Informatik Einheit 15: Reguläre Ausdrücke und rechtslineare Grammatiken Thomas Worsch Universität Karlsruhe, Fakultät für Informatik Wintersemester 2008/2009 1/25 Was kann man mit endlichen
Zweite Möglichkeit: Ausgabe direkt auf dem Bildschirm durchführen:
Ein- und Ausgabe Zweite Möglichkeit: Ausgabe direkt auf dem Bildschirm durchführen: fun p r i n t T r e e printa t = c a s e t o f Leaf a => ( p r i n t Leaf ; printa a ) Node ( l, a, r ) => ( p r i n
Adressen. Praktikum Funktionale Programmierung Organisation und Überblick. Termine. Studienleistung
Adressen Adressen, Termine Studienleistung Praktikum Funktionale Programmierung Organisation und Überblick Dr. David Sabel Büro und Email Raum 216, Robert-Mayer-Str. 11-15 [email protected]
Grundlegende Datentypen
Funktionale Programmierung Grundlegende Datentypen Fakultät für Informatik und Mathematik Hochschule München Letzte Änderung: 14.11.2017 15:37 Inhaltsverzeichnis Typen........................................
4. Jeder Knoten hat höchstens zwei Kinder, ein linkes und ein rechtes.
Binäre Bäume Definition: Ein binärer Baum T besteht aus einer Menge von Knoten, die durch eine Vater-Kind-Beziehung wie folgt strukturiert ist: 1. Es gibt genau einen hervorgehobenen Knoten r T, die Wurzel
Funktionale Programmierung Grundlegende Datentypen
Grundlegende Datentypen Prof. Dr. Oliver Braun Fakultät für Informatik und Mathematik Hochschule München Letzte Änderung: 06.11.2017 16:45 Inhaltsverzeichnis Typen........................................
ALP I. Funktionale Programmierung
ALP I Funktionale Programmierung Sortieren und Suchen (Teil 1) WS 2012/2013 Suchen 8 False unsortiert 21 4 16 7 19 11 12 7 1 5 27 3 8 False sortiert 2 4 6 7 9 11 12 18 21 24 27 36 Suchen in unsortierten
Binäre Bäume. 1. Allgemeines. 2. Funktionsweise. 2.1 Eintragen
Binäre Bäume 1. Allgemeines Binäre Bäume werden grundsätzlich verwendet, um Zahlen der Größe nach, oder Wörter dem Alphabet nach zu sortieren. Dem einfacheren Verständnis zu Liebe werde ich mich hier besonders
Funktionale Programmierung mit Haskell
Funktionale Programmierung mit Haskell Prof. Dr. Hans J. Schneider Lehrstuhl für Programmiersprachen und Programmiermethodik Friedrich-Alexander-Universität Erlangen-Nürnberg Sommersemester 2011 I. Die
Theoretische Grundlagen des Software Engineering
Theoretische Grundlagen des Software Engineering 11: Abstrakte Reduktionssysteme [email protected] Reduktionssysteme Definition: Reduktionssystem Ein Reduktionssystem ist ein Tupel (A, ) Dabei gilt: A
Der Aufruf von DM_in_Euro 1.40 sollte die Ausgabe 1.40 DM = 0.51129 Euro ergeben.
Aufgabe 1.30 : Schreibe ein Programm DM_in_Euro.java zur Umrechnung eines DM-Betrags in Euro unter Verwendung einer Konstanten für den Umrechnungsfaktor. Das Programm soll den DM-Betrag als Parameter verarbeiten.
1. Probeklausur zu Programmierung 1 (WS 07/08)
Fachschaft Informatikstudiengänge Fachrichtung 6.2 Informatik Das Team der Bremser 1. Probeklausur zu Programmierung 1 (WS 07/08) http://fsinfo.cs.uni-sb.de Name Matrikelnummer Bitte öffnen Sie das Klausurheft
Einfache Ausdrücke Datentypen Rekursive funktionale Sprache Franz Wotawa Institut für Softwaretechnologie [email protected]
Inhalt SWP Funktionale Programme (2. Teil) Einfache Ausdrücke Datentypen Rekursive funktionale Sprache Franz Wotawa Institut für Softwaretechnologie [email protected] Interpreter für funktionale Sprache
KONSTRUKTION VON ROT-SCHWARZ-BÄUMEN
KONSTRUKTION VON ROT-SCHWARZ-BÄUMEN RALF HINZE Institut für Informatik III Universität Bonn Email: [email protected] Homepage: http://www.informatik.uni-bonn.de/~ralf Februar, 2001 Binäre Suchbäume
Scala kann auch faul sein
Scala kann auch faul sein Kapitel 19 des Buches 1 Faulheit Faulheit ( lazy evaluation ) ist auch in C oder Java nicht unbekannt int x=0; if(x!=0 && 10/x>3){ System.out.println("In if"); } Nutzen der Faulheit?
Motivation. Formale Grundlagen der Informatik 1 Kapitel 5 Kontextfreie Sprachen. Informales Beispiel. Informales Beispiel.
Kontextfreie Kontextfreie Motivation Formale rundlagen der Informatik 1 Kapitel 5 Kontextfreie Sprachen Bisher hatten wir Automaten, die Wörter akzeptieren Frank Heitmann [email protected]
Grundlagen der Informatik. Prof. Dr. Stefan Enderle NTA Isny
Grundlagen der Informatik Prof. Dr. Stefan Enderle NTA Isny 2 Datenstrukturen 2.1 Einführung Syntax: Definition einer formalen Grammatik, um Regeln einer formalen Sprache (Programmiersprache) festzulegen.
II. Grundlagen der Programmierung. 9. Datenstrukturen. Daten zusammenfassen. In Java (Forts.): In Java:
Technische Informatik für Ingenieure (TIfI) WS 2005/2006, Vorlesung 9 II. Grundlagen der Programmierung Ekkart Kindler Funktionen und Prozeduren Datenstrukturen 9. Datenstrukturen Daten zusammenfassen
Funktionale Programmierung
Funktionale Programmierung Frank Huch Sommersemester 2008 Dieses Skript entsteht auf der Basis eines Skript, welches von Mihhail Aizatulin und Klaas Ole Kürtz (www.kuertz.name) im Sommersemester 2005 zu
Grundlagen von Python
Einführung in Python Grundlagen von Python Felix Döring, Felix Wittwer November 17, 2015 Scriptcharakter Programmierparadigmen Imperatives Programmieren Das Scoping Problem Objektorientiertes Programmieren
Algorithmen und Programmieren 1 Funktionale Programmierung - Musterlösung zur Übungsklausur -
Algorithmen und Programmieren 1 Funktionale Programmierung - Musterlösung zur Übungsklausur - Punkte: A1: 30, A2: 20, A3: 20, A4: 20, A5: 10, A6: 20 Punkte: /120 12.02.2012 Hinweis: Geben Sie bei allen
Objektorientierte Programmierung. Kapitel 12: Interfaces
12. Interfaces 1/14 Objektorientierte Programmierung Kapitel 12: Interfaces Stefan Brass Martin-Luther-Universität Halle-Wittenberg Wintersemester 2012/13 http://www.informatik.uni-halle.de/ brass/oop12/
DATENSTRUKTUREN UND ZAHLENSYSTEME
DATENSTRUKTUREN UND ZAHLENSYSTEME RALF HINZE Institute of Information and Computing Sciences Utrecht University Email: [email protected] Homepage: http://www.cs.uu.nl/~ralf/ March, 2001 (Die Folien finden
Vorkurs C++ Programmierung
Vorkurs C++ Programmierung Klassen Letzte Stunde Speicherverwaltung automatische Speicherverwaltung auf dem Stack dynamische Speicherverwaltung auf dem Heap new/new[] und delete/delete[] Speicherklassen:
Programmieren in Haskell Einführung
Programmieren in Haskell Einführung Peter Steffen Universität Bielefeld Technische Fakultät 16.10.2009 1 Programmieren in Haskell Veranstalter Dr. Peter Steffen Raum: M3-124 Tel.: 0521/106-2906 Email:
Fragen. f [ ] = [ ] f (x : y : ys) = x y : f ys f (x : xs) = f (x : x : xs) Wozu evaluiert f [1, 2, 3] (Abkürzung für f (1 : 2 : 3 : [ ]))?
Fragen f [ ] = [ ] f (x : y : ys) = x y : f ys f (x : xs) = f (x : x : xs) Wozu evaluiert f [1, 2, 3] (Abkürzung für f (1 : 2 : 3 : [ ]))? Wozu evaluiert [f [ ], f [ ]]? Weiteres Beispiel: f [ ] y = [
Programmieren in C. Macros, Funktionen und modulare Programmstruktur. Prof. Dr. Nikolaus Wulff
Programmieren in C Macros, Funktionen und modulare Programmstruktur Prof. Dr. Nikolaus Wulff Der C Präprozessor Vor einem Compile Lauf werden alle Präprozessor Kommandos/Makros ausgewertet. Diese sind
Kapitel 7 des Buches, von Java-Selbstbau nach Scala-Library portiert. 2014-11-14 Christoph Knabe
Anfragen für Listen Kapitel 7 des Buches, von Java-Selbstbau nach Scala-Library portiert. 2014-11-14 Christoph Knabe 1 MapReduce-Verfahren Google u.a. verwenden Map-Reduce-Verfahren zur Verarbeitung riesiger
Einführung in die Java- Programmierung
Einführung in die Java- Programmierung Dr. Volker Riediger Tassilo Horn riediger [email protected] WiSe 2012/13 1 Wichtig... Mittags keine Pommes... Praktikum A 230 C 207 (Madeleine + Esma) F 112 F 113
Programmierung und Modellierung
Programmierung und Modellierung Terme, Suchbäume und Pattern Matching Martin Wirsing in Zusammenarbeit mit Moritz Hammer SS 2009 2 Inhalt Kap. 7 Benutzerdefinierte Datentypen 7. Binärer Suchbaum 8. Anwendung:
5 DATEN. 5.1. Variablen. Variablen können beliebige Werte zugewiesen und im Gegensatz zu
Daten Makro + VBA effektiv 5 DATEN 5.1. Variablen Variablen können beliebige Werte zugewiesen und im Gegensatz zu Konstanten jederzeit im Programm verändert werden. Als Variablen können beliebige Zeichenketten
CGI Programmierung mit Ha. Markus Schwarz
CGI Programmierung mit Ha Markus Schwarz Überblick Was ist funktionale Programmierung Einführung in Haskell CGI-Programmierung mit Haskell Ein etwas größeres Beispiel Was ist funktionale Programm Ein Programm
Einführung in das Programmieren Prolog Sommersemester 2006. Teil 2: Arithmetik. Version 1.0
Einführung in das Programmieren Prolog Sommersemester 2006 Teil 2: Arithmetik Version 1.0 Gliederung der LV Teil 1: Ein motivierendes Beispiel Teil 2: Einführung und Grundkonzepte Syntax, Regeln, Unifikation,
Programmieren in C. Rekursive Funktionen. Prof. Dr. Nikolaus Wulff
Programmieren in C Rekursive Funktionen Prof. Dr. Nikolaus Wulff Rekursive Funktionen Jede C Funktion besitzt ihren eigenen lokalen Satz an Variablen. Dies bietet ganze neue Möglichkeiten Funktionen zu
Wintersemester Maschinenbau und Kunststofftechnik. Informatik. Tobias Wolf http://informatik.swoke.de. Seite 1 von 18
Kapitel 3 Datentypen und Variablen Seite 1 von 18 Datentypen - Einführung - Für jede Variable muss ein Datentyp festgelegt werden. - Hierdurch werden die Wertemenge und die verwendbaren Operatoren festgelegt.
2.11 Kontextfreie Grammatiken und Parsebäume
2.11 Kontextfreie Grammatiken und Parsebäume Beispiel: Beispiel (Teil 3): Beweis für L(G) L: Alle Strings aus L der Länge 0 und 2 sind auch in L(G). Als Induktionsannahme gehen wir davon aus, dass alle
Kapitel 2: Formale Sprachen Kontextfreie Sprachen. reguläre Grammatiken/Sprachen. kontextfreie Grammatiken/Sprachen
reguläre Grammatiken/prachen Beschreibung für Bezeichner in Programmiersprachen Beschreibung für wild cards in kriptsprachen (/* reguläre Ausdrücke */)?; [a-z]; * kontextfreie Grammatiken/prachen Beschreibung
Typdeklarationen. Es gibt in Haskell bereits primitive Typen:
Typdeklarationen Es gibt in bereits primitive Typen: Integer: ganze Zahlen, z.b. 1289736781236 Int: ganze Zahlen mit Computerarithmetik, z.b. 123 Double: Fließkommazahlen, z.b. 3.14159 String: Zeichenketten,
Binäre Suchbäume (binary search trees, kurz: bst)
Binäre Suchbäume (binary search trees, kurz: bst) Datenstruktur zum Speichern einer endlichen Menge M von Zahlen. Genauer: Binärbaum T mit n := M Knoten Jeder Knoten v von T ist mit einer Zahl m v M markiert.
Praktikum Funktionale Programmierung Teil 1: Lexen und Parsen
Praktikum Funktionale Programmierung Teil 1: Lexen und Parsen Professur für Künstliche Intelligenz und Softwaretechnologie Sommersemester 2009 Überblick Teil 1: Lexen und Parsen Die Sprache LFP +C Professur
Theoretische Informatik I
Theoretische Informatik I Einheit 2.4 Grammatiken 1. Arbeitsweise 2. Klassifizierung 3. Beziehung zu Automaten Beschreibungsformen für Sprachen Mathematische Mengennotation Prädikate beschreiben Eigenschaften
Software Engineering Klassendiagramme Assoziationen
Software Engineering Klassendiagramme Assoziationen Prof. Adrian A. Müller, PMP, PSM 1, CSM Fachbereich Informatik und Mikrosystemtechnik 1 Lesen von Multiplizitäten (1) Multiplizitäten werden folgendermaßen
1. Man schreibe die folgenden Aussagen jeweils in einen normalen Satz um. Zum Beispiel kann man die Aussage:
Zählen und Zahlbereiche Übungsblatt 1 1. Man schreibe die folgenden Aussagen jeweils in einen normalen Satz um. Zum Beispiel kann man die Aussage: Für alle m, n N gilt m + n = n + m. in den Satz umschreiben:
Beispiele: (Funktionen auf Listen) (3) Bemerkungen: Die Datenstrukturen der Paare (2) Die Datenstrukturen der Paare
Beispiele: (Funktionen auf Listen) (3) Bemerkungen: 5. Zusammenhängen der Elemente einer Liste von Listen: concat :: [[a]] -> [a] concat xl = if null xl then [] else append (head xl) ( concat (tail xl))
Einführung in die Programmierung
: Inhalt Einführung in die Programmierung Wintersemester 2008/09 Prof. Dr. Günter Rudolph Lehrstuhl für Algorithm Engineering Fakultät für Informatik TU Dortmund - mit / ohne Parameter - mit / ohne Rückgabewerte
Übung 9 - Lösungsvorschlag
Universität Innsbruck - Institut für Informatik Datenbanken und Informationssysteme Prof. Günther Specht, Eva Zangerle Besprechung: 15.12.2008 Einführung in die Informatik Übung 9 - Lösungsvorschlag Aufgabe
Datentypen. Agenda für heute, 4. März, 2010. Pascal ist eine streng typisierte Programmiersprache
Agenda für heute, 4. März, 2010 Zusammengesetzte if-then-else-anweisungen Datentypen Pascal ist eine streng typisierte Programmiersprache Für jeden Speicherplatz muss ein Datentyp t (Datenformat) t) definiert
TECHNISCHE UNIVERSITÄT MÜNCHEN FAKULTÄT FÜR INFORMATIK
TECHNISCHE UNIVERSITÄT MÜNCHEN FAKULTÄT FÜR INFORMATIK WS 11/12 Einführung in die Informatik II Übungsblatt 2 Univ.-Prof. Dr. Andrey Rybalchenko, M.Sc. Ruslán Ledesma Garza 8.11.2011 Dieses Blatt behandelt
15 Optimales Kodieren
15 Optimales Kodieren Es soll ein optimaler Kodierer C(T ) entworfen werden, welcher eine Information (z.b. Text T ) mit möglichst geringer Bitanzahl eindeutig überträgt. Die Anforderungen an den optimalen
Datenstrukturen & Algorithmen
Datenstrukturen & Algorithmen Matthias Zwicker Universität Bern Frühling 2010 Übersicht Binäre Suchbäume Einführung und Begriffe Binäre Suchbäume 2 Binäre Suchbäume Datenstruktur für dynamische Mengen
Deklarationen in C. Prof. Dr. Margarita Esponda
Deklarationen in C 1 Deklarationen Deklarationen spielen eine zentrale Rolle in der C-Programmiersprache. Deklarationen Variablen Funktionen Die Deklarationen von Variablen und Funktionen haben viele Gemeinsamkeiten.
Wiederholung ADT Menge Ziel: Verwaltung (Finden, Einfügen, Entfernen) einer Menge von Elementen
Was bisher geschah abstrakter Datentyp : Signatur Σ und Axiome Φ z.b. ADT Menge zur Verwaltung (Finden, Einfügen, Entfernen) mehrerer Elemente desselben Typs Spezifikation einer Schnittstelle Konkreter
Java Kurs für Anfänger Einheit 5 Methoden
Java Kurs für Anfänger Einheit 5 Methoden Ludwig-Maximilians-Universität München (Institut für Informatik: Programmierung und Softwaretechnik von Prof.Wirsing) 22. Juni 2009 Inhaltsverzeichnis Methoden
Formale Sprachen und Grammatiken
Formale Sprachen und Grammatiken Jede Sprache besitzt die Aspekte Semantik (Bedeutung) und Syntax (formaler Aufbau). Die zulässige und korrekte Form der Wörter und Sätze einer Sprache wird durch die Syntax
Objektorientierte Programmierung
Objektorientierte Programmierung 1 Geschichte Dahl, Nygaard: Simula 67 (Algol 60 + Objektorientierung) Kay et al.: Smalltalk (erste rein-objektorientierte Sprache) Object Pascal, Objective C, C++ (wiederum
Übungskomplex Felder (1) Eindimensionale Felder Mehrdimensionale Felder
Übungskomplex Felder (1) Eindimensionale Felder Mehrdimensionale Felder Hinweise zur Übung Benötigter Vorlesungsstoff Ab diesem Übungskomplex wird die Kenntnis und praktische Beherrschung der Konzepte
Einführung in die funktionale Programmierung
Einführung in die funktionale Programmierung Prof. Dr. Manfred Schmidt-Schauÿ Künstliche Intelligenz und Softwaretechnologie 20. November 2006 Monaden und I/O Monade ist ein Datentyp für (sequentielle)
Grundlagen der Informationverarbeitung
Grundlagen der Informationverarbeitung Information wird im Computer binär repräsentiert. Die binär dargestellten Daten sollen im Computer verarbeitet werden, d.h. es müssen Rechnerschaltungen existieren,
IT-Basics 2. DI Gerhard Fließ
IT-Basics 2 DI Gerhard Fließ Wer bin ich? DI Gerhard Fließ Telematik Studium an der TU Graz Softwareentwickler XiTrust www.xitrust.com www.tugraz.at Worum geht es? Objektorientierte Programmierung Konzepte
Klausurteilnehmer. Wichtige Hinweise. Note: Klausur Informatik Programmierung, 17.09.2012 Seite 1 von 8 HS OWL, FB 7, Malte Wattenberg.
Klausur Informatik Programmierung, 17.09.2012 Seite 1 von 8 Klausurteilnehmer Name: Matrikelnummer: Wichtige Hinweise Es sind keinerlei Hilfsmittel zugelassen auch keine Taschenrechner! Die Klausur dauert
Programmierung 2. Übersetzer: Code-Erzeugung. Sebastian Hack. Klaas Boesche. Sommersemester 2012. [email protected]. [email protected].
1 Programmierung 2 Übersetzer: Code-Erzeugung Sebastian Hack [email protected] Klaas Boesche [email protected] Sommersemester 2012 Bytecodes Der Java Übersetzer erzeugt keine Maschinensprache
Entwicklung eines korrekten Übersetzers
Entwicklung eines korrekten Übersetzers für eine funktionale Programmiersprache im Theorembeweiser Coq Thomas Strathmann 14.01.2011 Gliederung 1 Einleitung
Folge 19 - Bäume. 19.1 Binärbäume - Allgemeines. Grundlagen: Ulrich Helmich: Informatik 2 mit BlueJ - Ein Kurs für die Stufe 12
Grundlagen: Folge 19 - Bäume 19.1 Binärbäume - Allgemeines Unter Bäumen versteht man in der Informatik Datenstrukturen, bei denen jedes Element mindestens zwei Nachfolger hat. Bereits in der Folge 17 haben
Motivation Von Funktoren, Komposition, Applikation Zu Monoiden Die Monade Zusammenfassung. Monaden für alle. Franz Pletz
Monaden für alle Franz Pletz Chaos Computer Club München 13. Juni 2010, GPN10 Wieso, weshalb, warum? einige von euch haben sich sicher schon mal Haskell angeschaut und sind an Monaden
Das Typsystem von Scala. L. Piepmeyer: Funktionale Programmierung - Das Typsystem von Scala
Das Typsystem von Scala 1 Eigenschaften Das Typsystem von Scala ist statisch, implizit und sicher 2 Nichts Primitives Alles ist ein Objekt, es gibt keine primitiven Datentypen scala> 42.hashCode() res0:
Funktionale Programmierung mit Haskell. Jan Hermanns
Funktionale Programmierung mit Haskell Jan Hermanns 1 Programmiersprachen imperativ deklarativ konventionell OO logisch funktional Fortran Smalltalk Prolog Lisp C Eiffel ML Pascal Java Haskell 2 von Neumann
Übungsblatt 3: Algorithmen in Java & Grammatiken
Humboldt-Universität zu Berlin Grundlagen der Programmierung (Vorlesung von Prof. Bothe) Institut für Informatik WS 15/16 Übungsblatt 3: Algorithmen in Java & Grammatiken Abgabe: bis 9:00 Uhr am 30.11.2015
Die Definition eines Typen kann rekursiv sein, d.h. Typ-Konstruktoren dürfen Elemente des zu definierenden Typ erhalten.
4.5.5 Rekursive Typen Die Definition eines Typen kann rekursiv sein, d.h. Typ-Konstruktoren dürfen Elemente des zu definierenden Typ erhalten. datatype IntList = Nil Cons o f ( i n t IntList ) ; Damit
Praktische Informatik 3: Funktionale Programmierung Vorlesung 4 vom : Typvariablen und Polymorphie
Rev. 2749 1 [28] Praktische Informatik 3: Funktionale Programmierung Vorlesung 4 vom 04.11.2014: Typvariablen und Polymorphie Christoph Lüth Universität Bremen Wintersemester 2014/15 2 [28] Fahrplan Teil
Informatik-Seminar Thema: Monaden (Kapitel 10)
Informatik-Seminar 2003 - Thema: Monaden (Kapitel 10) Stefan Neumann 2. Dezember 2003 Inhalt Einleitung Einleitung Die IO()-Notation Operationen Einleitung Gegeben seien folgende Funktionen: inputint ::
Objektbasierte Entwicklung
Embedded Software Objektbasierte Entwicklung Objektorientierung in C? Prof. Dr. Nikolaus Wulff Objektbasiert entwickeln Ohne C++ wird meist C im alten Stil programmiert. => Ein endlose while-schleife mit
Java Einführung Operatoren Kapitel 2 und 3
Java Einführung Operatoren Kapitel 2 und 3 Inhalt dieser Einheit Operatoren (unär, binär, ternär) Rangfolge der Operatoren Zuweisungsoperatoren Vergleichsoperatoren Logische Operatoren 2 Operatoren Abhängig
Kapiteltests zum Leitprogramm Binäre Suchbäume
Kapiteltests zum Leitprogramm Binäre Suchbäume Björn Steffen Timur Erdag überarbeitet von Christina Class Binäre Suchbäume Kapiteltests für das ETH-Leitprogramm Adressaten und Institutionen Das Leitprogramm
Einführung in die Informatik 2
Technische Universität München Fakultät für Informatik Prof. Tobias Nipkow, Ph.D. Lars Noschinski, Dr. Jasmin Blanchette, Dmitriy Traytel Wintersemester 2012/13 Lösungsblatt Endklausur 9. Februar 2013
Idee: Wenn wir beim Kopfknoten zwei Referenzen verfolgen können, sind die Teillisten kürzer. kopf Eine Datenstruktur mit Schlüsselwerten 1 bis 10
Binäre Bäume Bäume gehören zu den wichtigsten Datenstrukturen in der Informatik. Sie repräsentieren z.b. die Struktur eines arithmetischen Terms oder die Struktur eines Buchs. Bäume beschreiben Organisationshierarchien
Grammatiken in Prolog
12. Grammatiken in Prolog 12-1 Grammatiken in Prolog Allgemeines: Gedacht zur Verarbeitung natürlicher Sprache. Dort braucht man kompliziertere Grammatiken als etwa im Compilerbau, andererseits sind die
