Programmieren in Haskell - Kombinator-Sprachen

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