Programmieren in Haskell Stefan Janssen Programmieren in Haskell Kombinator-Sprachen Stefan Janssen Universität Bielefeld AG Praktische Informatik 20. Januar 2015 Grammatiken und Parser DSL Vorschau Programmieren in Haskell Das heutige Thema hat zwei Teile kontextfreie Grammatiken, Sprachen und ihre “Parser” Domain Specific Languages (DSL, anwendungsspezifische Sprachen) am Beispiel des Parsens als Beispiel einer Spracherweiterung in Haskell Stefan Janssen Grammatiken und Parser DSL Vorschau Programmieren in Haskell Das heutige Thema hat zwei Teile kontextfreie Grammatiken, Sprachen und ihre “Parser” Domain Specific Languages (DSL, anwendungsspezifische Sprachen) am Beispiel des Parsens als Beispiel einer Spracherweiterung in Haskell Gemeinsamer Ausgangspunkt: In vielen Anwendungen besteht die Eingabe aus einem Text, der in seiner Struktur erfasst werden muss, ehe die eigentliche Verarbeitung beginnt kann diese Struktur durch ein Grammatik beschrieben werden Stefan Janssen Grammatiken und Parser DSL Beispiel: Übersetzung von Programmen Programmieren in Haskell Wichtigstes Beispiel: Compiler Stefan Janssen Eingabe: Programmtext als ASCII String Grammatiken und Parser Zerlegung 1: Folge von Namen, Zahlen, Operatoren DSL Zerlegung 2: Erkennen von Deklarationen, Definitionen, Ausdrücken, Import-Listen, ... Resultat der “syntaktischen Analyse”: Baumartige Darstellung des Programms als rekursiver Datentyp Weitere Verarbeitung des Programmbaums: Semantische Prüfung, Transformation, Optimierung, Codeerzeugung, ... Beispiele von DSLs (1) Programmieren in Haskell Textbeschreibung in LATEX: Stefan Janssen \section{Grammatiken und Parser} Grammatiken und Parser \begin{frame}{Vorschau} DSL Das n\"achste Thema hat zwei Teile \begin{itemize} \item ‘‘Parser’’ f\"ur kontextfreie Sprachen \item Domain Specific Languages (DSL, anwendungsspezifische S \item \emph{als Beispiel} einer Spracherweiterung in Haskell \end{itemize} \pause ... Beispiele von DSLs (2) Programmieren in Haskell Literatureintrag in BibTeX: Example 1 2 3 4 5 6 7 8 9 Stefan Janssen Grammatiken und Parser @book {UBHD1745069 , DSL a u t h o r ={B i r d , R i c h a r d } , t i t l e ={ I n t r o d u c t i o n t o f u n c t i o n a l programming u s i n g H a s k e l l } , p a g e s ={433} , p u b l i s h e r ={ P r e n t i c e H a l l } , y e a r ={1998} , i s b n ={978−0−13−484346−9}, e d i t i o n ={2nd } } Beispiele von DSLs (2) Programmieren in Haskell Stefan Janssen Literatureintrag in BibTeX: Example 1 2 3 4 5 6 7 8 9 Grammatiken und Parser @book {UBHD1745069 , DSL a u t h o r ={B i r d , R i c h a r d } , t i t l e ={ I n t r o d u c t i o n t o f u n c t i o n a l programming u s i n g H a s k e l l } , p a g e s ={433} , p u b l i s h e r ={ P r e n t i c e H a l l } , y e a r ={1998} , i s b n ={978−0−13−484346−9}, e d i t i o n ={2nd } } \ cite { UBHD 1745069} -- Referenz im Text Textzerlegung mit groupBy Programmieren in Haskell Eine ausgesprochen nützliche Funktion: groupBy aus Data.List groupBy :: (a -> a -> Bool) -> [a] -> [[a]] zerlegt einen String in “Gruppen”, so dass concat . groupBy f == id für jedes f gruppiert wird, solange die Bedingung f über dem ersten und dem (bisher) letzten Buchstaben der Gruppe erfüllt ist der erste Buchstabe, der die Bedingung f verletzt, startet eine neue Gruppe Stefan Janssen Grammatiken und Parser DSL Beispiele zu GroupBy 1 2 Parse > groupBy (==) " aaggccccggt " [ " aa " ," gg " ," cccc " ," gg " ," t " ] Programmieren in Haskell Stefan Janssen 3 4 5 Parse > groupBy (/=) " aaccgaccgaaa " [ " a " ," accg " ," accg " ," a " ," a " ," a " ] Grammatiken und Parser DSL 6 7 8 Parse > groupBy ( <=) " abraham " [ " abraham " ] 9 10 11 Parse > groupBy ( <) " abraham " [ " abr " ," ah " ," am " ] 12 13 14 15 Parse > groupBy (\ a b -> a /= ’ ’ && b /= ’ ’) " wenn er aber kommt ? " [ " wenn " ," " ," er " ," " ," aber " ," " ," kommt ? " ] Textstruktur allgemein groupBy ist nützlich, aber beschränkt. Programmieren in Haskell Stefan Janssen Nur ein Kriterium der Zerlegung, nur eine Form des Zusammenhangs der Teile (:) Texte habe Zeichen, Wörter, Sätze, Abschnitte, Kapitel, relativ flache Strukturen Programme haben Operatoren, Variablen, Konstanten, Zahlen, Ausdrücke, Deklarationen, Definitionen, und beliebig tiefe Strukturen Komplexe formale Sprachen beschreibt man durch Grammatiken, die Sprachen generieren, oder Automaten, die Sprachen akzeptieren Beide Methoden sind gleich mächtig Grammatiken und Parser DSL Klassen formaler Sprachen Programmieren in Haskell Stefan Janssen Endliche Automaten sind aus A&D bekannt. Sie können Sprachen wie {(ab)n } beschreiben, aber nicht {an b n }. Grammatiken und Parser DSL Klassen formaler Sprachen Programmieren in Haskell Stefan Janssen Endliche Automaten sind aus A&D bekannt. Sie können Sprachen wie {(ab)n } beschreiben, aber nicht {an b n }. Kontextfreie Grammatiken sind etwas mächtiger. Sie können Sprachen wie {an b n } und {an b m c n } beschreiben, aber nicht {an b n c n } oder {an b m c n d m }. Grammatiken und Parser DSL Kontextfreie Grammatiken Hier eine kleine Grammatik für Ausdrücke: Terminalsymbole: 0 1 2 a b c d ) ( + * Nichtterminalsymbole: A T F Z B C Axiom A Regeln A -> T T -> F F -> Z Z -> 0 B -> C C -> a | | | | | | T F B 1 C b + A * T | ( A ) | 2 B | c | d Ausdrücke sind also "a+1*(abba+2)", "2*a+2*b", "2*(a+b)" Programmieren in Haskell Stefan Janssen Grammatiken und Parser DSL Programmbäume Programmieren in Haskell 1 2 3 Darstellung der Ausdrücke "a+1*(abba+2)", "2*a+2*b", "2*(a+b)" nach ihrer Zerlegung Plus ( Var " a " ) ( Mul ( Numb " 1 " ) ( Plus ( var " abba " ) ( Numb " 2 " ))) 4 5 6 Plus ( Mul ( Numb " 2 " ) ( Var " a " )) ( Mul ( Numb " 2 " ) ( Var " b " )) 7 8 9 10 Mul ( Numb " 2 " ) ( Plus ( Var " a " ) ( Var " b " )) Stefan Janssen Grammatiken und Parser DSL Vorüberlegungen zum Parser Programmieren in Haskell Anforderungen: Der Parser soll Stefan Janssen wohlgeformte Ausdrücke in Programmbäume übersetzen Grammatiken und Parser wenn es mehrere Parses gibt, dann für alle den Programmbaum liefern ... DSL ... und ggf. gar keinen Wir schreiben einen rekursiven Top-Down Parser: Für jedes Nichtterminalsymbol X gibt es eine Parserfunktion pX die Ableitungen aus X konstruiert und die entsprechenden Programmbäume zusammensetzt Arbeitsweise der Parser Programmieren in Haskell Eine Parserfunktion pX arbeitet wie folgt Eingabe ist ein (restlicher) String w pX leitet Präfix u von w = uv aus X ab dazu ruft sie sich selbst und andere Parser für die NTs der rechten Seite auf gibt Programmbaum P(X -> u) und Resteingabe v zurück ... ... und das für alle Paare P(X -> u) und v Wird kein Parse gefunden, wird eine leere Liste zurückgegeben Stefan Janssen Grammatiken und Parser DSL Parser-Code Programmieren in Haskell 1 > type Parser a b = [ a ] -> [( b , [ a ])] 2 3 4 5 6 > data Ptree = Plus > Mul > Var > Numb Ptree Ptree | Ptree Ptree | String | String deriving Show 7 8 > parse xs = [ t | (t ,[]) <- pA xs ] pA wird als erster Parser aufgerufen, weil A das Axiom der Grammatik ist Ein Parse ist nur erfolgreich, wenn er die ganze Eingabe “verbraucht”. Stefan Janssen Grammatiken und Parser DSL Parser-Code Programmieren in Haskell Stefan Janssen 1 2 3 4 5 6 7 8 9 10 > pA xs = pT xs ++ > [( Plus l r , v ) | (l , o : u ) <- pT xs ,Grammatiken und Parser > o == ’+ ’ , DSL > (r , v ) <- pA u ] > pT xs = pF xs ++ > [( Mul l r , v ) | (l , ’* ’: u ) <- pF xs , > (r , v ) <- pT u ] > pF xs = pZ xs ++ pB xs ++ > [( t , v ) | ( ’( ’: u ) <- [ xs ] , > (t , ’) ’: v ) <- pA u ] Parser-Code Programmieren in Haskell 1 2 3 4 5 6 7 8 9 10 11 12 > pZ xs = > > > > pB xs = > > > pC xs = > > > > Stefan case xs of ’0 ’: v -> [( Numb " 0 " , v )] Janssen ’1 ’: v -> [( Numb " 1 " , v )] Grammatiken ’2 ’: v -> [( Numb " 2 " , v )]und Parser DSL otherwise -> [] [( Var ( c : " " ) , v ) | (c , v ) <- pC xs ] ++ [( Var ( c : cs ) , v ) | (c , u ) <- pC xs , ( Var cs , v ) <- pB u ] case xs of ’a ’: v -> [( ’a ’ , v )] ’b ’: v -> [( ’b ’ , v )] ’c ’: v -> [( ’c ’ , v )] ’d ’: v -> [( ’d ’ , v )] otherwise -> [] Systematik Bei der Parser-Konstruktion wiederholen sich Lesen bestimmter Zeichen oder Zeichenkombinationen Aufruf anderer Parser in der Reihenfolge der Nonterminals auf den rechten Seiten (++) für die Zusammenführung von Resultaten aus Alternativen Weiterreichen von Teilergebnissen und Resteingabe an den nächsten Parser Anwendung eines Programmbaum-Konstruktors auf Teilergebnisse Wenn wir dafür spezielle Operationen kreieren, erhalten wir eine DSL für die schnelle und fehlersichere Implementierung von Parsern Programmieren in Haskell Stefan Janssen Grammatiken und Parser DSL Vorfreude Der Parser für die Grammatik A T F Z B C -> -> -> -> -> -> T F Z 0 C a | | | | | | T F B 1 C b + * | | B | A T ( A ) 2 c | d Programmieren in Haskell Stefan Janssen Grammatiken und Parser DSL Vorfreude Der Parser für die Grammatik A T F Z B C 1 2 3 4 5 6 -> -> -> -> -> -> T F Z 0 C a | | | | | | T F B 1 C b + * | | B | A T ( A ) 2 c | d wird dann etwa so aussehen: > pA = pT ||| Plus <<< pT ~~~ ’+ ’ ~~~ pA > pT = pF ||| Mul <<< pF ~~~ ’* ’ ~~~ pT > pF = pZ ||| pB ||| ’( ’ ~~~ pA ~~~ ’) ’ > pZ = Numb <<< ’0 ’ ||| ’1 ’ ||| ’2 ’ > pB = (:[]) <<< pC ||| (:) <<< pC ~~~ pB > pC = ’a ’||| ’ b ||| ’ c ’||| ’ d ’ Achtung: Das ist noch nicht der endgültige Code Programmieren in Haskell Stefan Janssen Grammatiken und Parser DSL Vorbereitung Programmieren in Haskell Stefan Janssen Was wird gebraucht? Definition der Parser-Kombinatoren Definition der terminalen Parser Definition der Funktionen zum Baumaufbau, soweit nicht direkt die Konstruktoren verwendbar sind Grammatiken und Parser DSL Zur Erinnerung Programmieren in Haskell Stefan Janssen 1 Typ eines Parsers war > type Parser a b = [ a ] -> [( b , [ a ])] Ein Parser verarbeitet Liste vom Elementtyp a berechnet Liste von Ergebnissen vom Typ [(Programmbaum, Resteingabe)] NB: Die Eingabe muss also kein String sein .... Grammatiken und Parser DSL Parser für einzelne Zeichen Programmieren in Haskell 1 2 3 4 5 > cchar :: ( Eq a ) = > a -> Parser a a Stefan Janssen -- vergleiche mit Definition von char weiter unten > cchar c [] = [] Grammatiken und Parser > cchar c ( x : xs ) DSL > | x == c = [ (c , xs ) ] > | otherwise = [] Parser cchar ’x’ erkennt nur den Buchstaben ’x’ am Anfang der Eingabe. Später lernen wir einen allgemeineren Parser für beliebige Eingabesymbole kennen. Parser-Kombinatoren Programmieren in Haskell Kombinatoren nennt man Funktionen, deren Argumente und Ergebnisse Funktionen sind. Stefan Janssen Grammatiken und Parser DSL 1 2 3 Unsere Kombinatoren kombinieren Parser mit Parsern zu neuen Parsern > infix 8 <<< -- Programmbaum - Aufbau > infixl 7 ~~~ -- Verkettung von Parsern > infixr 6 ||| -- Zusammenfassen von Alternativen Die Wahl der Prioritäten und der Assoziierungsrichtung wird später wichtig ... Parser-Kombinatoren Programmieren in Haskell Stefan Janssen Kombinatoren für Funktionsanwendung, Verkettung und Alternative 1 2 3 Grammatiken und Parser DSL > ( < < <) :: ( b -> c ) -> Parser a b -> Parser a c > ( < < <) f p inp = [( f x , rest )| (x , rest ) <- p inp ] 4 5 6 7 8 9 > > > > > (~~~) :: Parser a (b - > c ) - > Parser a b - > Parser a c (~~~) p q inp = [( x y , r2 ) | (x , rest ) <- p inp , (y , r2 ) <- q rest ] (|||) :: Parser a b -> Parser a b -> Parser a b (|||) p q inp = p inp ++ q inp Parser-Kombinatoren Programmieren in Haskell Stefan Janssen 1 2 Jeder Parser kann zum Axiom der Grammatik erklärt werden durch den Kombinator axiom > axiom :: [ a ] -> Parser a b -> [ b ] > axiom inp p = [ x | (x , []) <- p inp ] Das Axiom muss die ganze Eingabe ableiten, nicht nur einen Präfix davon (wie alle anderen Parser) Grammatiken und Parser DSL Grammatik in Kombinator-Schreibweise Programmieren in Haskell Jetzt können wir den Parser wie eine Kopie der Regeln in der Grammatik schreiben: 1 1 2 Stefan Janssen Grammatiken und Parser Aus > DSL A -> T | T + A wird pA = pT ||| plus <<< pT ~~~ cchar ’+ ’ ~~~ pA aus > > F -> Z | B | ( A ) wird pF = Numb <<< pZ ||| Var <<< pB ||| mid <<< cchar ’( ’ ~~~ pA ~~~ cchar ’) ’ Wir brauchen aber noch ein paar Bausteine Programmbaumaufbau Programmieren in Haskell Stefan Janssen Grammatiken und Parser 1 2 3 4 Die Konstruktoren Plus und Mul erwarten nur zwei DSL Argumente, der Parser liefert drei. Daher: > pC = cchar ’a ’ ||| cchar ’b ’ ||| cchar ’c ’ > ||| cchar ’d ’ > plus x _ z = Plus x z > mul x _ z = Mul x z Der komplette Parser Programmieren in Haskell 1 2 3 4 5 6 7 8 9 10 11 12 13 14 > parse_C :: String -> [ Ptree ] Stefan Janssen > parse_C inp = axiom inp pA where > pA = pT ||| plus <<< pT ~~~ cchar ’+ ’ ~~~Grammatiken pA und Parser > pT = pF ||| mul <<< pF ~~~ cchar ’* ’ ~~~ pT DSL > pF = Numb <<< pZ ||| Var <<< pB ||| > mid <<< cchar ’( ’ ~~~ pA ~~~ cchar ’) ’ > pZ = (:[]) <<< ( cchar ’0 ’ ||| cchar ’1 ’ > ||| cchar ’2 ’) > pB = (:[]) <<< pC ||| (:) <<< pC ~~~ pB > pC = cchar ’a ’ ||| cchar ’b ’ ||| cchar ’c ’ > ||| cchar ’d ’ > plus x _ z = Plus x z > mul x _ z = Mul x z > mid _ y _ = y Vereinfachungen Programmieren in Haskell Stefan Janssen Reale Parser werden nach der gleichen Methode gebaut, sind aber komplizierter Zahlen mit beliebig vielen Ziffern Operatoren aus mehreren Zeichen “Whitespace” in der Eingabe Es muss eine Ebene der lexikalischen Analyse vorgschaltet werden. Dazu gibt es ein ausgefeilteres Beispiel im nächsten Abschnitt Grammatiken und Parser DSL DSLs – Anwendungssprachen Programmieren in Haskell Domain Specific Language (DSL) vs. General-Purpose-Language, Libraries, Frameworks Ziele: Unterstützung komplexer Anwendungen mit wiederkehrenden Grund-Konstruktionen Beispiele: TEX, LATEX, Lex, Yacc, awk . . . Stefan Janssen Grammatiken und Parser DSL DSLs – Anwendungssprachen Programmieren in Haskell Domain Specific Language (DSL) vs. General-Purpose-Language, Libraries, Frameworks Ziele: Unterstützung komplexer Anwendungen mit wiederkehrenden Grund-Konstruktionen Beispiele: TEX, LATEX, Lex, Yacc, awk . . . Fortgeschrittenes Paul Hudak. Modular Domain Specific Languages and Tools. Proceedings of the Fifth International Conference on Software Reuse, IEEE Computer Society, 1998. http://haskell.cs. yale.edu/wp-content/uploads/2011/01/DSEL-Reuse.pdf Stefan Janssen Grammatiken und Parser DSL Autonome versus eingebettete DSL Autonome DSL: Eigenständige Sprache Beispiel aus Bielefeld: Bellman’s GAP Gebiet: biologische Sequenzanalyse eigener Compiler mit umfangreicher Optimierung Programmieren in Haskell Stefan Janssen Grammatiken und Parser DSL Autonome versus eingebettete DSL Autonome DSL: Eigenständige Sprache Beispiel aus Bielefeld: Bellman’s GAP Gebiet: biologische Sequenzanalyse eigener Compiler mit umfangreicher Optimierung Embedded DSL: Sprach-Subset, Erweiterung oder Bibliothek in Host Sprache Beispiel aus Bielefeld: Haskell ADP, ADPfusion Gebiet: biologische Sequenzanalyse kein eigener Compiler, keine spezifische Optimierung, aber einfache Implementierung Programmieren in Haskell Stefan Janssen Grammatiken und Parser DSL Autonome versus eingebettete DSL Autonome DSL: Eigenständige Sprache Beispiel aus Bielefeld: Bellman’s GAP Gebiet: biologische Sequenzanalyse eigener Compiler mit umfangreicher Optimierung Embedded DSL: Sprach-Subset, Erweiterung oder Bibliothek in Host Sprache Beispiel aus Bielefeld: Haskell ADP, ADPfusion Gebiet: biologische Sequenzanalyse kein eigener Compiler, keine spezifische Optimierung, aber einfache Implementierung Strategie für neue DSL: Erst “embedded” entwickeln und erproben, dann “ex-bedding” Programmieren in Haskell Stefan Janssen Grammatiken und Parser DSL Unser eDSL-Beispiel Eine eDSL zur Parser-Konstruktion, eingebettet in Haskell allgemein: Parser-Combinators (sind unabhängig von gegebener Grammatik) Haskell ist flexibel genug und erlaubt durch die Definition der Kombinatoren als inf-x-Operatoren eine “eingebettete” Spracherweiterung Dazu noch ein nicht ganz so einfaches Beispiel: BibTeX-Format Programmieren in Haskell Stefan Janssen Grammatiken und Parser DSL Unser eDSL-Beispiel Eine eDSL zur Parser-Konstruktion, eingebettet in Haskell allgemein: Parser-Combinators (sind unabhängig von gegebener Grammatik) Haskell ist flexibel genug und erlaubt durch die Definition der Kombinatoren als inf-x-Operatoren eine “eingebettete” Spracherweiterung Dazu noch ein nicht ganz so einfaches Beispiel: BibTeX-Format Programmieren in Haskell Stefan Janssen Grammatiken und Parser DSL Example 1 2 3 4 5 6 7 8 9 @book {UBHD1745069 , a u t h o r ={B i r d , R i c h a r d } , t i t l e ={ I n t r o d u c t i o n t o f u n c t i o n a l programming u s i n g H a s k e l l } , p a g e s ={433} , p u b l i s h e r ={ P r e n t i c e H a l l } , y e a r ={1998} , i s b n ={978−0−13−484346−9}, e d i t i o n ={2nd } } Grammatik für BibTex-Eintraege Programmieren in Haskell record recHead recBody recEnd entype entries entry rhs -> -> -> -> -> -> -> -> recHead recBody recEnd ’@’ entype ’{’ aword ’,’ entries ’}’ "book" | "article" entry | entry ’,’ entries aword ’=’ rhs ’{’ content ’}’ | ’"’ content ’"’ | aword aword: beliebige Zeichenreihe ohne die Trennzeichen { } @ = , Stefan Janssen Grammatiken und Parser DSL Weitere Festlegungen Programmieren in Haskell Stefan Janssen Die “lexikalische Ebene”: “Wort” ist beliebige Zeichenfolge ohne Trennzeichen ... ... und ohne die “whitespace” Zeichen <neue Zeile> und <Blank> vor dem eigenlichen Parsen wird der Text in “tokens” (Worte oder Trennzeichen) zerlegt ... ... und der Whitespace herausgefiltert Diese Vorverarbeitung nennt man lexikalische Analyse Grammatiken und Parser DSL Zur Erinnerung Programmieren in Haskell Stefan Janssen 1 Typ eines Parsers war > type Parser a b = [ a ] -> [( b , [ a ])] Ein Parser verarbeitet Liste vom Elementtyp a berechnet Liste von Ergebnissen vom Typ [(Programmbaum, Resteingabe)] NB: Die Eingabe muss also kein String sein .... Grammatiken und Parser DSL Parser-Kombinatoren Wie zuvor ... Kombinatoren nennt man Funktionen, deren Argumente und Ergebnisse Funktionen sind. Programmieren in Haskell Stefan Janssen Grammatiken und Parser DSL 1 2 3 Unsere Kombinatoren kombinieren Parser mit Parsern zu neuen Parsern > infix 8 <<< -- Programmbaum - Aufbau > infixl 7 ~~~ -- Verkettung von Parsern > infixr 6 ||| -- Zusammenfassen von Alternativen Die Wahl der Prioritäten und der Assoziierungsrichtung wird später wichtig ... Parser-Kombinatoren Programmieren in Haskell Wie zuvor ... 1 2 3 Stefan Janssen Grammatiken und Parser > ( < < <) :: ( b -> c ) -> Parser a b -> Parser a c DSL > ( < < <) f p inp = [( f x , rest )| (x , rest ) <p inp ] 4 5 6 7 8 9 > > > > > (~~~) :: Parser a (b - > c ) - > Parser a b - > Parser a c (~~~) p q inp = [( x y , r2 ) | (x , rest ) <- p inp , (y , r2 ) <- q rest ] (|||) :: Parser a b -> Parser a b -> Parser a b (|||) p q inp = p inp ++ q inp Parser-Kombinatoren Programmieren in Haskell Stefan Janssen Wie zuvor... Grammatiken und Parser DSL 1 2 Jeder Parser kann zum Axiom der Grammatik erklärt werden durch den Kombinator axiom > axiom :: [ a ] -> Parser a b -> [ b ] > axiom inp p = [ x | (x , []) <- p inp ] Das Axiom muss die ganze Eingabe ableiten, nicht nur einen Präfix davon (wie alle anderen Parser) Grammatik in Kombinator-Schreibweise Programmieren in Haskell Stefan Janssen Jetzt können wir den Parser wie eine Kopie der Regeln in der Grammatik schreiben: Grammatiken und Parser DSL Aus entries -> entry | entry ’,’ entries wird 1 2 > > entries = s1 <<< entry ||| s2 <<< entry ~~~ char ’,’ ~~~ entries aus entry -> aword ’=’ rhs wird 1 > entry = e1 <<< aword ~~~ char ’= ’ ~~~ Wir brauchen aber noch ein paar Bausteine rhs Parser für die lexikalischen tokens Programmieren in Haskell Stefan Janssen 1 2 3 4 5 > char :: ( Eq a ) = > a -> Parser [ a ] a > char c [] = [] > char c ( x : xs ) > | x == [ c ] = [ (c , xs ) ] > | otherwise = [] Parser char ’c’ erkennt nur den Buchstaben ’c’ als "c" am Anfang der Eingabe Grammatiken und Parser DSL Parser für die lexikalischen tokens Programmieren in Haskell 1 2 3 4 5 6 > word :: Eq a = > a -> Parser a a > word w [] = [] > word w ( x : xs ) > | x == w = [ (w , xs ) ] > | otherwise = [] 7 8 9 10 11 12 > aword :: Parser String String > aword [] = [] > aword ( x : xs ) > | x /= " { " && x /= " } " = [ (x , xs ) ] > | otherwise = [] Parser aword akzeptiert ein beliebiges Wort (außer den Klammern), Parser word w nur das Wort w Stefan Janssen Grammatiken und Parser DSL Aufbau der Bäume für die Bib-Entries Programmieren in Haskell Stefan Janssen 1 2 3 4 5 > data BibRecord = BR { > bibType :: String , Grammatiken und Parser > bibId :: String , DSL > bibEntries :: [ BibEntry ] > } deriving ( Show ) 6 7 8 9 10 > data BibEntry = BE { > entryKey :: String , > entryVals :: [ String ] > } deriving ( Show ) Lexikalische Analyse Programmieren in Haskell Stefan Janssen 1 2 3 4 5 6 > lexer :: String -> [ String ] Grammatiken > lexer inp = filter isWS $ groupBy g inp where und Parser > g a b = and $ map (\ c -> a /= c && b /= c ) delims DSL > delims = " {} ,@ ,= " > isWS w = not $ and $ > map (\ c -> c == ’ ’ || c == ’\ n ’) w Vorgruppieren mittels groupBy, Herausfiltern des Whitespace mit isWS Parser-Code Programmieren in Haskell Die Grammatik in Kombinator-Schreibweise > parser inp > where > record > recHead > > recBody > recEnd > entype > > entries = axiom inp record Stefan Janssen Grammatiken und Parser = a1 <<< recHead ~~~ recBody ~~~ recEnd DSL = h1 <<< char ’@’ ~~~ entype ~~~ char ’{’ ~~~ aword ~~~ char ’,’ = entries = char ’}’ = word "book" ||| word "article" = s1 <<< entry ||| s2 <<< entry ~~~ char ’,’ ~~~ entries Parser-Code Programmieren in Haskell Die Grammatik in Kombinator-Schreibweise > > entry rhs > content = e1 <<< = r1 <<< r1 <<< r2 <<< = c1 <<< c2 <<< c3 <<< Stefan Janssen aword ~~~ char ’=’ ~~~ rhs Grammatiken und Parser char ’{’ ~~~ content ~~~ char ’}’ ||| DSL char ’"’ ~~~ content ~~~ char ’"’ ||| aword char ’{’ ~~~ content ~~~ char ’}’ ||| aword ~~~ content ||| aword Die Funktionen e1, r1, ... sind für den Baumaufbau zuständig Baumaufbau Programmieren in Haskell 1 2 3 4 5 6 7 8 9 10 Der Baumaufbau benutzt die Konstruktoren lässt irrelvante tokens weg: > a1 ( typ , iden ) ents _ = > h1 _ typ _ iden _ = > s1 e = > s2 e _ es = > e1 k _ xs = > r1 _ x _ = > r2 x = > c1 _ x _ = > c2 a b = > c3 a = BR und BE, aber Stefan Janssen Grammatiken und Parser BR typ iden ents DSL ( typ , iden ) [e] e : es BE k xs x [x] ( " { " : x ) ++ [ " } " ] a:b [a] Aufruf des Parsers Lexer und Parser werden hintereinandergeschaltet im Aufruf parser $ lexer xs Programmieren in Haskell Stefan Janssen Grammatiken und Parser Wir sind fertig!! Die Grammatik in Kombinator-Schreibweise IST der Parser ...! Siehe Anwendungsbeispiel im Code von parse.lhs DSL Aufruf des Parsers Lexer und Parser werden hintereinandergeschaltet im Aufruf parser $ lexer xs Programmieren in Haskell Stefan Janssen Grammatiken und Parser Wir sind fertig!! Die Grammatik in Kombinator-Schreibweise IST der Parser ...! Siehe Anwendungsbeispiel im Code von parse.lhs Fortgeschrittenes Graham Hutton. Higher-order functions for parsing. Journal of Functional Programming, Volume 2 Issue 3, 323–343, 1992. http://www.cs.nott.ac.uk/~gmh/parsing.pdf DSL Traum und Wahrheit Programmieren in Haskell Wir lesen den Parser für Stefan Janssen A → B C D | F, Grammatiken und Parser also pA = f <<< pA ~~~ pB ~~~ pC DSL ||| g <<< pF als pA = (f <<< (pA ~~~ pB ~~~ pC)) ||| (g <<< pF) die Warhheit ist dank der Prioritäten aber pA = ((((f <<< pA) ~~~ pB) ~~~ pC)) ||| (g <<< pF) Nutzung der Parser-eDSL Programmieren in Haskell Kombinatorparser sind einfach zu entwickeln fehlersicher: die Grammatik IST der Parser einfach zu anzupassen, wenn die Sprache sich ändert Grenzen: nicht so effizient wie generierte Parser (z.B. durch Yacc, Bison) bei mehrdeutigen Sprachen müssen sie mit dynamischer Programmierung verbunden werden zur Bewertung und Auswahl der Lösungen Stefan Janssen Grammatiken und Parser DSL Nutzung der Parser-eDSL Programmieren in Haskell Kombinatorparser sind einfach zu entwickeln fehlersicher: die Grammatik IST der Parser einfach zu anzupassen, wenn die Sprache sich ändert Grenzen: nicht so effizient wie generierte Parser (z.B. durch Yacc, Bison) bei mehrdeutigen Sprachen müssen sie mit dynamischer Programmierung verbunden werden zur Bewertung und Auswahl der Lösungen Ende der Vorlesung “Programmieren in Haskell” Stefan Janssen Grammatiken und Parser DSL Wie geht es weiter Programmieren in Haskell Programmiersprachen und Algorithmik ⇒ Objektorientierte Programmierung in Java (2. Semester, vll. OOP mit Hashing, Heaps, RB-Trees) ⇒ Sequenzanalyse (3. Semester) ⇒ Algorithmen der Informatik (4. Semester, Graph-Algorithmen, . . . ) Endliche Automaten, Registermaschine, Komplexität ⇒ Grundlagen Theoretischer Informatik (3. Semester, Formale Sprachen, Berechenbarkeit, Komplexität . . . ) von Neumann Architektur Rechnerarchitektur (3. Semester) Dynamic Programming diverse DP-Algorithmen in verschiedenen Veranstaltungen ⇒ Algebraic Dynamic Programming (im Master) Stefan Janssen Grammatiken und Parser DSL Programmieren in Haskell Stefan Janssen The End Grammatiken und Parser DSL Viel Erfolg im Studium ... ... und auch etwas Spaß! Letzte Sitzung: Fragestunde zur Prüfungsvorbereitung