Was bisher geschah I Algebraische Datentypen I Pattern Matching I Funktionen höherer Ordnung I Rekursionsschemata map, fold über Zahlen, Listen, Bäumen, ... I strukturelle Induktion I Polymorphie, Typklassen I Lazy evaluation I Streams, Musik I Monaden (return, bind), Beispiele 84 Interaktive Programme I Berechnungen in Haskell sind nebenwirkungsfrei. (Diese Eigenschaft soweit wie möglich beibehalten) I Ein- und Ausgabe sind Nebenwirkungen. (Nebenwirkungen zulassen, wo sie unvermeidbar sind) I Bei Ein- und Ausgabeaktionen ist die Ausführungsreihenfolge wichtig. Idee: I Isolation der Programmteile mit Nebenwirkungen (Operation return ) I sequentielle Verknüpfung von Ausdrücken mit Nebenwirkungen (Operation bind >>= ) 85 IO-Monade class Monad m where return :: a -> m a ( >>= ) :: m a -> (a -> m b) -> m b für m = IO: return :: a -> IO a ( >>= ) :: IO a -> (a -> IO b) -> IO b instance Monad IO where return v = \w -> (v, w) f >>= g = \w -> case f w of (v, w’) -> g v w’ 86 Abstraktes Modell für IO I IO a = Aktion mit Resultat :: a und Nebenwirkung I ein ausführbares Haskell-Programm enthält module Main where main :: IO () main = ... diese Aktion wird (berechnet und) ausgeführt. oft zusätzlich notwendig: import System.IO main = do hSetBuffering stdout NoBuffering ... 87 Konkretes Modell für IO: Zustand Typ für Aktionen, Änderung des Weltzustandes data World = ... data IO = World -> World data IO a = IO { World -> (a, World) } das Welt-Objekt bezeichnet die Welt außerhalb des Programmes Problem: f :: World -> ( World, World ) f w = ( deleteFile "foo" w, putStr "bar" w ) mögliche Lösungen in verschiedenen Sprachen: I Haskell: Typ World ist privat, öffentlich ist nur IO I Clean: Typ World ist öffentlich, aber unique 88 Ein- und Ausgabe putStrLn "Hello" hat den Typ IO () Zustandsänderung ohne Resultat putStrLn hat den Typ String -> IO () Funktion, Funktionswert ist eine Aktion (die zu einer Zustandsänderung führen kann) putStr, putStrLn nur für Strings putStr :: String -> IO () print für beliebige Typen (mit show) print x = putStrLn (show x) getLine hat den Typ IO String Zustandsänderung mit Resultat :: String 89 Ein- und Ausgabe Für IO-Aktion ist die Reihenfolge wichtig. muss in Haskell angegeben werden do-Notation do A B C mit Anweisungen A,B,C 90 Ein- und Ausgabe-Anweisungen Eingabe: x <- A mit Aktion A (erzeugt Variablenbindung) Beispiel (Kombination): echoline :: IO () echoline = do input <- getLine putStr input 91 Häufige IO-Aktionen abfrage :: String -> IO String abfrage s = do putStrLn s getLine mit getLine :: IO String getLine = do x <- getChar if x == ’\n’ then return [] else do xs <- getLine return xs Verwendung: main :: IO () main = do name <- abfrage "Name:" putStrLn ("Hello " ++ name) 92 Hakell-Compiler ghc Programm hello.hs: module Main where main = putStrLn "Hello" mit ghc compilieren ghc -o hello hello.hs und ausführen: ./hello 93 Noch ein Beispiel module Main where sumList :: [Int] -> Int sumList [] = 0 sumList (x:xs) = x + sumList xs main = print ( sumList [1 .. 10] ) compilieren mit ghc und aufrufen: ghc -o sumlist sumlist.hs ./sumlist oder interpretieren (wie bisher) mit ghci: :l Main main 94 Ein- und Ausgaben von Werten getNumber :: IO Int getNumber = do putStr "Zahl eingeben: " readLn z.B. in main :: IO () main = do n1 <- getNumber n2 <- getNumber putStr "Summe: " print (n1 + n2) analog: getNumber :: IO Int 95 Rückgabewerte get2 :: IO (Int, Int) get2 = do n1 <- getNumber n2 <- getNumber return (n1, n2) return :: a -> IO a main = do x <- get2 print x 96 Rekursion getNums :: IO [Int] getNums = do n <- readLn if n == 0 then return [] else do ns <- getNums return ([n] ++ ns) main :: IO () main = do putStrLn "Zahlen eingeben (0 für Ende)" nums <- getNums putStr "Summe = " ++ show (sum nums) 97 Beipiel Datei-Ein- und Ausgabe readFile :: FilePath -> IO String putStrLn :: String -> IO () Wegen der Monad-Instanz: do-Notation do cs <- readFile "foo.bar" putStrLn cs main :: IO () main = do putStr " Filename:" fname <- getLine cont <- readFile fname putStr cont 98 Übersetzer für Programme Höhere Programmiersprachen (z.B. Java, Haskell, C) erfordern Übersetzung von Quell- in Maschinen- oder Byte-Code Beispiel: Übersetzung von Java-Programmen Quellcode ↓ Zwischendarstellung (attributierter Syntaxbaum) ↓ Java-Bytecode Übersetzung in zwei Phasen (oft miteinander verschränkt): 1. Analyse-Phase (Front-End): Transformation des Quellcodes in eine Zwischendarstellung 2. Synthese-Phase (Back-End): Transformation der Zwischendarstellung in Maschinen- oder Bytecode 99 Analyse-Phase Quellcode Scanner Parser −→ Folge von Token −→ Syntaxbaum lexikalische Analyse (Scanner) lineare Analyse des Quelltextes, Aufteilung in Einheiten (Token) z.B. Schlüsselwörter, Bezeichner, Zahlen reguläre Sprachen, endliche Automaten syntaktische Analyse (Parser) hierarchische Struktur des Quelltextes z.B. Ausdrücke, Verzweigungen, Schleifen kontextfreie Sprachen, Kellerautomaten semantische Analyse Annotationen im Syntaxbaum, z.B. Typprüfungen 100 Anwendung ähnlicher Methoden I Übersetzung von Daten zwischen verschiedenen Formaten I automatische Code-Generierung I Verarbeitung von Domain-spezifischen Sprachen I Textformatierung I kontextabhängige Hilfe in Entwicklungsumgebungen I statische Analyse zur Fehlersuche in Programmen I Interpreter I graphische Editoren (z.B. für UML-Diagramme) mit Programmerzeugung 101 Parser zum Beispiel für I arithmetische und logische Ausdrücke I HTML-Code im Web-Browser I Haskell-Code in GHC / GHCI type Parser = String -> Tree parse :: Parser i.A. nur teilweise Verarbeitung der Eingabe type Parser = String -> (Tree, String) Ergebnis: 1. Syntaxbaum (o.Ä.) des verarbeiteten Teiles der Eingabe 2. unverarbeiteter Teil der Eingabe 102 Nichtdeterminismus Behandlung (temporär) uneindeutiger Ableitungen mehrdeutige Grammatiken, z.B. verschiedene Ableitungen für 5−3−2 E ::= n | E − E mit n ∈ N Nichtdeterministische Ausgabe (Ergebnisliste) type Parser = String -> [(Tree, String)] Ergebnis: Liste möglicher Syntaxbäume I []: Fehler I [ x ]: eindeutig interpretierbar (Erfolg) I [ x, y, .. ]: mögliche Interpretationen 103 Typabstraktion Idee: verschiedene Typen von I „Eingabesymbolen“ c und I und „Syntaxbäumen“ a als Datentyp: data Parser c a = Parser ( [c] -> [(a, [c])] ) parse :: Parser c a -> [c] -> [(a, [c])] parse ( Parser f ) s = f s (oft Char für c) A Parser for Things is a functions from Strings to Lists of Pairs of Things and Strings! 104 Parser Konstruktion komplexer Parser durch: I I Elementare Parser Operationen zur Kombination von Parsern: I I I sequentielle Kombination parallele Kombination (Auswahl) Iteration Man bemerke die Analogie zu regulären Ausdrücken E ::= ∅ | ε | a | EE | E + E | E ∗ mit a ∈ A und E ∈ RegExp(A) 105 Elementare Parser data Parser c a = Parser ( [c] -> [(a, [c])] ) I return (immer erfolgreich und eindeutig) return :: a -> Parser c a return v = Parser $ \x -> [(v, x)] I reject (nie erfolgreich) reject :: Parser c a reject = Parser $ \_ -> [] I item (vearbeitet das erste Symbol der Eingabe) item :: Parser c c item = Parser $ \x -> case x of [] -> [] (x : xs) -> [( x, xs )] I eof (nur bei leerer Eingabe erfolgreich) 106 Anwendung data Parser c a = Parser ( [c] -> [(a, [c])] ) parse :: Parser c a -> [c] -> [(a, [c])] parse p eingabe = p eingabe Beispiele: I parse (return 1) "abc" I parse reject "abc" I parse item "abc" I parse item "" 107 Test des ersten Symbols I satisfy (akzeptiert das erste Symbol, falls es die Bedingung pred erfüllt) satisfy :: (c -> Bool) -> Parser c c satisfy pred = do x <- item if pred x then return x else reject I expect (akzeptiert das erste Symbol, falls es genau das erwartete ist) expect :: Eq c => c -> Parser c c expect c = satisfy ( == c ) Beispiele: I parse (expect ’a’) "abc" I parse (expect ’a’) "" I parse (expect ’b’) "abc" I parse (satisfy isDigit) "1a4" 108 Sequentielle Verknüpfung Verkettung von Sprachen: L ◦ L0 = {uv | u ∈ L ∧ v ∈ L0 } seq :: Parser c a -> ( a -> Parser c b) -> Parser c b kennen wir schon als bind-Operation für Monaden >>= :: Parser c a -> ( a -> Parser c b) -> Parser c b p >>= f = Parser $ \ s -> do ( v, t ) <- parse p s parse (f v) t Anwendung: p1 >>= ( \v1 -> p2 >>= \v2 -> ... (\vn -> return (f v1 v2 ... vn))...) oder in do-Notation: do v1 <- p1 v2 <- p2 ... vn <- pn return (f v1 v2 ... vn) 109 Beispiele p :: Parser Char (Char, Char) p = do x <- item item y <- item return (x,y) parse p "abcde" parse p "a" parens :: Parser Char a -> Parser Char a parens p = do x <- item expect (’(’) y <- p expect (’)’) return y parse (parens item) "(r)" parse (parens p) "(abcd)" parse (parens p) "(abc)" 110 Parallele Verknüpfung (nichtdeterministisch) Vereinigung von Sprachen: L ∪ L0 (+++) :: Parser c a -> Parser c a -> Parser c a p +++ q = Parser $ \ s -> ( parse p s ) ++ ( parse q s ) Beispiel: s :: Parser Char () s = do { expect ’a’ ; s ; expect ’b’ ; s } +++ return () parse (do s ; eof) "abab" 111 Iteration bekannt aus LV Theoretische Informatik: L∗ = {ε} ∪ L+ L+ = L ◦ L∗ many’ :: Parser c a -> Parser c [a] many’ p = many1’ p +++ return [] many1’ :: Parser c a -> Parser c [a] many1’ p = do x <- p xs <- many’ p return (x : xs) Beispiel: nat’ :: Parser Char Integer nat’ = do xs <- many1’ (satisfy isDigit) return (read xs) 112 Akzeptanz formaler Sprachen s :: Parser Char () akzeptiert alle von der Grammatik mit den Regeln S → aSbS | ε erzeugten Wörter s = do { expect ’a’ ; s ; expect ’b’ ; s } +++ return () Beispiel: parse (do s ; eof) "abaabb" 113 Alternative Verknüpfung (deterministisch) (<|>) :: Parser c a -> Parser c a -> Parser c a p <|> q = Parser $ \ s -> case ( parse p s ) of [] -> ( parse q s ) x : xs -> return x Determinismus meist gewünscht, deshalb übliche Definitionen von many und many1: many :: Parser c a -> Parser c [a] many p = many1 p <|> return [] many1 :: Parser c a -> Parser c [a] many1 p = do x <- p xs <- many p return (x : xs) 114 Beispiel: Arithmetische Ausdrücke Grammatik (mit Operator-Präferenzen): E ::= P(+P)∗ P F ::= F (∗F )∗ ::= (E) | nat z.B. 3 + 4 ∗ (5 + 2) mit Parsec-Bibliothek ( import Text.Parsec): expr :: Parsec String () Integer expr = do xs <- sepBy1 produkt ( satisfy ( == ’+’ )) return $ sum xs produkt = do xs <- sepBy1 factor ( satisfy ( == ’*’ )) return $ product xs factor = parens expr <|> nat Anwendung: parse expr "egal" "3+4*(5+2)" 115 Beispiel: Prolog-Terme Beispiele für Prolog-Terme: vater(X), foo, Foo, foo(bar) Haskell-Datentyp: data Term = Var String | App String [ Term ] pterm :: Parsec String () Term pterm = do v <- var ; return $ Var v <|> do f <- fun ( do args <- parens $ sepBy pterm ( string "," return $ App f args ) <|> ( return $ App f [] ) fun :: Parsec String () String fun = withFirst isLower var :: Parsec String () String var = withFirst isUpper 116 Beispiel: Prolog-Atome Beispiele für Prolog-Atome: liest(vater(X), krimis), foo, foo(bar) Haskell-Datentyp: data Atom = Atom String [ Term ] patom :: Parsec String () Atom patom = do f <- fun ( do args <- parens $ sepBy pterm ( string "," ) return $ Atom f args ) <|> ( return $ Atom f [] ) 117 Beispiel: Prolog-Regeln Prolog-Regel und -Fakten: mag(tom, X) :- frau(X), liest (X, krimis). liest(mimi, krimis). Haskell-Datentyp: data Rule = Fact Atom | Rule Atom [ Atom ] prule :: Parsec String () Rule prule = do head <- patom ( do string ":-" body <- sepBy patom (string ",") string "." return $ Rule head body ) <|> ( do string "."; return $ Fact head ) 118