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 Sprache Haskell II. Fallstudien 8. Suchbäume und beliebige Graphen 9. Lambda-Interpretierer 10. Syntaxanalyse 11. Algebraische Spezifikation 12. Semantik operationeller Sprachen 13. Kategorien in Haskell 14. Compilieren funktionaler Programme c Hans J. Schneider 2011
Chomsky-Grammatik Definition: Eine Chomsky-Grammatik ist ein Quadrupel G = (T, N, P, S) mit T, N (endliche, disjunkte) Alphabete terminale und nichtterminale Symbole S N Startsymbol P endliche Teilmenge von V NV V, wobei V = T N ist. Produktionen Definition: Eine kontextfreie Grammatik ist eine Chomsky-Grammatik mit P N V Beispiel (BNF-Schreibweise) E ::= E + T E T T T ::= T F T/F F F ::= v n ( E ) Funktionale Programmierung mit Haskell 10.1
Direkte Ableitbarkeit Sei p = (u, v) P x p y : ( w, z V )(x = wuz y = wvz) Sei G = (T, N, P, S) x y : ( p P )(x G p y) Wenn keine Verwechslung möglich: x y statt x G y Beispiel: E E + T T + T T + T F v + T F... E E + T E + T F T + T F v + T F... Funktionale Programmierung mit Haskell 10.2
Ableitbarkeit Definition der Ableitbarkeit: x y : ( n 0)( w 0, w 1,..., w n V ) (w 0 = x ( 1 i n)(w i 1 w i ) w n = y) Andere Schreibweisen: Feste Anzahl von Schritten: In der Literatur auch: = n Menge der erzeugten Zeichenreihen: L(G) := {w S G w w T } Funktionale Programmierung mit Haskell 10.3
Ableitungsbaum Ableitungsbaum = Graphische Darstellung einer Ableitung bei kontextfreien Grammatiken E E + T T T F p 1 p 3 p 4 p 6 p 6 p 7 F F v p 7 p 8 v n Zusammenhang: Wurzel = Startsymbol Innere Knoten = Nichtterminale Symbole Blätter = Terminale Symbole Verkettung der Blätter = abgeleitete Zeichenreihe Funktionale Programmierung mit Haskell 10.4
Kontextfreie Grammatik in Haskell: Symbole Wir unterscheiden die terminalen und die nichtterminalen Symbole: data Symbol a b = T a N b deriving (Eq,Ord,Read) Wir können unterschiedliche Symbolvorräte wählen: Symbol Char Char verwendet sowohl für terminale als auch für nichtterminale Symbole Einzelzeichen. Symbol Char String verwendet für nichtterminale Symbole Zeichenketten. Bei der Ausgabe sollen die Konstruktoren entfallen: instance (Show a, Show b) => Show (Symbol a b) where show (T x) = show x show (N x) = show x Abfrage: nonterm :: Symbol a b -> Bool nonterm (T _) = False nonterm _ = True Funktionale Programmierung mit Haskell 10.5
Kontextfreie Produktionen in Haskell Produktionen: data Prod a b = Prod (Symbol a b) [Symbol a b] deriving Eq Aufteilung in linke und rechte Seite: lhs :: Prod a b -> (Symbol a b) lhs (Prod x ys) = x rhs :: Prod a b -> [Symbol a b] rhs (Prod x ys) = ys Drucken von Produktionen: instance (Show a, Show b) => Show (Prod a b) where show (Prod y zs) = show y ++ " ==> " ++ show zs Beispiel: Grammar> rhs prod1 ["expr", +,"expr"] Grammar> prod1 "expr" ==> "expr" + "expr" Funktionale Programmierung mit Haskell 10.6
Kontextfreie Grammatik in Haskell Man kann Grammatiken auf unterschiedliche Weise implementieren. Also ist es sinnvoll, eine Typklasse zu definieren: class Grammar grammar where startsymbol :: grammar a b -> Symbol a b prettyprintgrammar :: (Show a, Show b) => grammar a b -> IO () Einfache Implementierung: Grammatik als Liste von Produktionen: data ProdList a b = ProdList [Prod a b] Vereinfachung: Startsymbol = linke Seite der ersten Produktion instance Grammar ProdList where startsymbol (ProdList[]) = error "empty grammar" startsymbol (ProdList (r:rs)) = lhs r prettyprintgrammar g = putstr (prtgram g) where prtgram (ProdList []) = "" prtgram (ProdList (r:rs)) = (show r) ++ "\n" ++ (prtgram (ProdList rs)) Funktionale Programmierung mit Haskell 10.7
Kontextfreie Grammatik: Beispiel Einfaches Beispiel: grammar2 :: ProdList Char String grammar2 = ProdList [ Prod (N "expr") [N "expr", T +, N "term"], Prod (N "expr") [N "term"], Prod (N "term") [N "term", T *, N "fact"], Prod (N "term") [N "fact"], Prod (N "fact") [T n ], Prod (N "fact") [T v ], Prod (N "fact") [T (, N "expr", T ) ] ] Funktionale Programmierung mit Haskell 10.8
Bottom-Up-Aufbau von Bäumen Beispiel: T F = T T F F v F v n n Zusammenfassen von Bäumen mittels einer Produktion: reduce :: (Eq a, Eq b) => (Prod a b, [h (Symbol a b)]) -> h (Symbol a b) reduce((prod lhs rhs), subtrees) = if map root subtrees == rhs then ArbNode(lhs, subtrees) else error "production not applicable" StandArbTree wird Instanz von DerivTree. Funktionale Programmierung mit Haskell 10.9
Top-Down-Entwicklung von Bäumen Beispiel: E T E + T E T E + T T F Welches T ist gemäß der Produktion T T F zu ersetzen? Dewey-Indizierung: [] [1] [2] [1, 1] [3] [3, 1] [3, 2] [3, 3] Funktionale Programmierung mit Haskell 10.10
Expandieren an einer bestimmten Stelle Typdefinition: type DeweyIndex = [Int] expand :: (Eq a, Eq b) => (h (Symbol a b), DeweyIndex, Prod a b) -> h (Symbol a b) Implementieren der Expandierung: expand(t, [], (Prod lhs rhs)) = if (root t) == lhs then ArbNode(lhs, map newleaf rhs) else error "Production not applicable" expand(t, i:d, p) = if i > length (successors t) i < 1 then error "Wrong Dewey index" else ArbNode(root t, replace (successors t, i, p)) where replace(ti:r, 1, p) = expand(ti, d, p) : r replace(f:r, i, p) = f: replace(r, i-1, p) Funktionale Programmierung mit Haskell 10.11
Finden eines bestimmten Blattes findleftmost :: (Eq a, Eq b) => Symbol a b -> h (Symbol a b) -> DeweyIndex findleftmost x t = if found then d else error "Element not found" where (found, d) = find(x, t) find(x, t) = if (height t ) == 0 then (False, []) {- 2. Komponente ohne Bedeutung -} else if (height t ) == 1 then if (root t) == x then (True, []) else (False, []) else helper(x, successors t, 1) helper(x, [], i) = (False, []) helper(x, f:st, i) = if foundinthis then (True, i:d ) else helper(x, st, i+1) where (foundinthis, d ) = find(x, f) Funktionale Programmierung mit Haskell 10.12
Erzeugen einer Linksableitung Setze die Produktion soweit links wie möglich ein: expandleftmost(t, p) = expand(t, findleftmost x t, p) where x = lhs p Beispiele: t0 = starttree grammar2 t1 = expandleftmost(t0, Prod (N "expr") [N "expr", T +, N "term"]) Grammar> t0 ("expr") Grammar> t1 ("expr" ("expr") ( + ) ("term")) Grammar> expandleftmost(t1, Prod (N "expr") [N "expr", T +, N "term"]) ("expr" ("expr" ("expr") ( + ) ("term")) ( + ) ("term")) Funktionale Programmierung mit Haskell 10.13
Klasse der Ableitungsbäume class (Tree h) => DerivTree h where starttree :: Grammar a b -> h (Symbol a b) termnode :: Symbol a b -> h (Symbol a b) reduce :: (Eq a, Eq b) => (Prod a b, [h (Symbol a b)]) -> h (Symbol a b) expand :: (Eq a, Eq b) => (h (Symbol a b), DeweyIndex, Prod a b) -> h (Symbol a b) findleftmost :: (Eq a, Eq b) => Symbol a b -> h (Symbol a b) -> DeweyIndex expandleftmost :: (Eq a, Eq b) => (h (Symbol a b), Prod a b) -> h (Symbol a b) instance DerivTree StandArbTree where starttree g = ArbNode(startSymbol g, []) termnode (T c) = ArbNode(T c, []) termnode (N _) = error "termnode needs terminal symbol"... Funktionale Programmierung mit Haskell 10.14
Cocke-Kasami-Younger: Definitionen Chomsky-Normalform: Alle Produktionen sind von der Form: A ::= B C a Syntaxanalyse nach Cocke, Kasami und Younger: Gegeben: w = t 1 t 2... t n Zentrale Datenstruktur: Matrix m ij ( 0 i < j n)(m ij = {A A N A t i+1... t j }) w L(G) S m 0n Effektive Konstruktion der m ij : m i,i+1 := {A A ::= t i+1 } m ij := {A ( i < k < j)( B m ik )( C m kj )(A ::= BC)} Funktionale Programmierung mit Haskell 10.15
Cocke-Kasami-Younger: Rechenschema t 1 t 2 t n 0 1 m 0n n Funktionale Programmierung mit Haskell 10.16
Beispielgrammatik Übliche Formulierung (BNF): S ::= AB BB A ::= CC AB a B ::= BB CA b C ::= BA AA b Beispiel eines Wortes mit mehreren Ableitungen: A a S B B B A A S B B b b b a b Funktionale Programmierung mit Haskell 10.17
Beispielgrammatik in Haskell Wir speichern zur Vereinfachung die Grammatik als Liste der Produktionen: data Prod a b = Prod (Symbol a b) [Symbol a b] deriving Eq Haskell-Formulierung des Beispiels: g2 = ProdList [ Prod (N S ) [N A, N B ], Prod (N S ) [N B, N B ], Prod (N A ) [N C, N C ], Prod (N A ) [N A, N B ], Prod (N A ) [T a ], Prod (N B ) [N B, N B ], Prod (N B ) [N C, N A ], Prod (N B ) [T b ], Prod (N C ) [N B, N A ], Prod (N C ) [N A, N A ], Prod (N C ) [T b ] ] Funktionale Programmierung mit Haskell 10.18
Kern des CKY-Algorithmus Effektive Konstruktion der m ij : m i,i+1 := {A A ::= t i+1 } m ij := {A ( i < k < j)( B m ik )( C m kj )(A ::= BC)} Implementierung der effektiven Konstruktion: cky(i,j) = if j == i+1 then [a (Prod a [T b]) <- listofprods, b == word!! i ] else concat [ [a (Prod a [b,c]) <- listofprods, elem b (cky(i,k)) && elem c (cky(k,j)) ] k <- [i+1.. j-1] ] where (ProdList listofprods) = grammar Ein Feld kann ein Element mehrfach enthalten. Funktionale Programmierung mit Haskell 10.19
CKY-Algorithmus in Haskell Top-Level-Funktion im Modul CKYParsing: ckyisword grammar word = elem (startsymbol grammar) (cky(0, length word)) where cky(i,j) =... Beispiel: CKYParsing> ckyisword g2 "abb" True Kann man den CKY-Kern so ändern, dass alle Bäume ausgegeben werden? Funktionale Programmierung mit Haskell 10.20
CKY mit Bäumen Wir speichern in jedem Feld der Matrix alle Teilbäume mit der Eigenschaft ( 0 i < j n)(m ij = {A A N A t i+1... t j }) cky(i,j) = if j == i+1 then [ArbNode(a, [termnode (T b)]) (Prod a [T b]) <- listofprods, b == word!! i ] else concat [ [reduce (Prod a [b,c], [t1, t2]) t1 <- cky(i,k), t2 <- cky(k,j), (Prod a [b,c]) <- listofprods, b == root t1 && c == root t2 ] k <- [i+1.. j-1] ] where (ProdList listofprods) = grammar Funktionale Programmierung mit Haskell 10.21
CKY mit Bäumen (II) Die Analysefunktion muss die Bäume aus m 0n heraussuchen, die mit dem Startsymbol beginnen. ckytrees grammar word = if length(result) == 0 then error "Word not derivable" else result where result = validtrees (cky(0, length(word))) Bemerkungen: validtrees [] = [] validtrees (h:t) = if (root h) == (startsymbol grammar) then h : (validtrees t) else validtrees t cky(i,j) =... cky ist lokal definiert und kann daher in beiden Algorithmen verwandt werden. Benutze bei validtrees die filter-funktion! Die Lösung ist sehr ineffizient. Funktionale Programmierung mit Haskell 10.22
Ausdrucken aller Ableitungsbäume Erzeugung von Leerzeilen zwischen den Bäumen: printckytrees grammar word = putstr(printdertrees (ckytrees grammar word)) where -- printdertrees :: [StandArbTree Char] -> String printdertrees [] = "\n" printdertrees [t] = show t ++ "\n" printdertrees (t1:rest) = (show t1) ++ "\n\n" ++ (printdertrees rest) Beispiel einer (mehrdeutigen) Analyse: CKYParsing> printckytrees g2 "abb" ( S ( A ( a )) ( B ( B ( b )) ( B ( b )))) ( S ( A ( A ( a )) ( B ( b ))) ( B ( b ))) Funktionale Programmierung mit Haskell 10.23