Deklarative Programmierung

Werbung
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
Herunterladen