Parserkombinatoren

Werbung
Parserkombinatoren
Ein Programm, das für ein gegebenes Wort w entscheidet, ob es von einer
gegebenen kontextfreien Grammatik G beschrieben wird, heißt Parser für G.
Dabei ist es wünschenswert, dass der Parser zusätzlich zur Entscheidung, ob das
Eingabewort erkannt wird, Zusatzinformationen über die Eingabe als Ausgabe
liefern kann. Solche Zusatzinformationen könnten zum Beispiel eine Linksableitung
oder ein abstrakter Syntaxbaum sein.
Es gibt unterschiedliche Ansätze, Parser zu implementieren:
• Parsergeneratoren wie YACC (für C) oder Happy (für Haskell) erzeugen
aus einer textuellen Beschreibung einer kontextfreien Grammatik einen
Parser für diese Grammatik.
• Rekursive Abstiegsparser sind durch gegenseitig rekursive Funktionen
- eine für jedes Nichtterminalsymbol der Grammatik - definiert.
• Parserkombinatoren erlauben die Definition von rekursiven Abstiegsparsern
in einer Grammatik-ähnlichen Notation. Mit Parserkombinatoren definierte
Grammatiken sind also direkt als Parser ausführbar.
Im Folgenden behandeln wir die Verwendung und Implementierung solcher Parserkombinatoren.
Verwendung
Ein Kombinator-Parser ist ein Wert vom Typ Parser a und kann mit der parseFunktion auf ein Wort angewendet werden.
parse :: Parser a -> String -> Maybe a
Der Typ a beschreibt Zusatzinformation, die bei einem erfolgreichen ParserLauf zurückgegeben wird. Parser, die nur entscheiden, ob das gegebene Wort
erkannt wird, liefern in der Regel () als Ergebnis. Zum Beispiel konstruiert der
Kombinator
char :: Char -> Parser ()
einen Parser, der genau das gegebene Zeichen erkennt:
ghci> parse (char ’a’) "a"
Just ()
1
Bei anderen Eingaben liefert dieser Aufruf Nothing.
ghci> parse (char ’a’) ""
Nothing
ghci> parse (char ’a’) "b"
Nothing
ghci> parse (char ’a’) "ab"
Nothing
Einfache Parser können zu komplexeren kombiniert werden. Dazu gibt es zum
Beispiel den Kombinator
(*>) :: Parser a -> Parser b -> Parser b
der zwei Parser hintereinander ausführt und das Ergebnis des zweiten Parsers
liefert. Damit kann man zum Beispiel einen Parser definieren, der eine öffnende
Klammer gefolgt von einer schließenden erkennt:
parens = char ’(’ *> char ’)’
Die zu diesem Parser gehörige Grammatik sieht in Backus-Naur Form (BNF)
so aus:
Parens ::= ’(’ ’)’
Es gibt auch eine Variante des obigen Kombinators, die es erlaubt, das Ergebnis
des ersten Arguments zu liefern:
(<*) :: Parser a -> Parser b -> Parser a
Bei beiden Kombinatoren wird das linke Argument vor dem rechten angewendet.
Es gilt also nicht (<*) = flip (*>).
Als Ergebnis eines erfolgreichen Parser-Laufs des Klammern-Parsers erhalten
wir den Wert ().
ghci> parse parens "()"
Just ()
Wir erweitern nun diesen Parser so, dass er korrekt geschachtelte KlammerAusdrücke erkennt. Die zugehörige BNF sieht so aus:
Nested ::= ’(’ Nested ’)’ Nested
|
2
Hierbei ist die zweite Alternative das leere Wort und das definierte Nichtterminalsymbol Nested wird auf der rechten Seite der Definition rekursiv verwendet.
In Haskell können wir diesen Parser ebenso rekursiv definieren:
nested
= char ’(’ *> nested *> char ’)’ *> nested
<|> empty
Hierbei verwenden wir den Kombinator
(<|>) :: Parser a -> Parser a -> Parser a
zur Deklaration der Alternativen und den Parser
empty :: Parser ()
zur Erkennung des leeren Wortes. Wir testen auch diesen Parser wieder mit der
parse-Funktion:
ghci> parse nested "(()(()()))"
Just ()
ghci> parse nested "(()()"
Nothing
Die bisher vorgestellten Kombinatoren erfüllen die folgenden Gleichungen. empty
erkennt nur das leere Wort und ist deshalb neutral bezüglich (*>) und (<*):
empty *> p
p <* empty
=
=
p
p
Außerdem gelten Distributivgesetze für die Sequenz-Kombinatoren und den
<|>-Kombinator, wie zum Beispiel:
(p <|> q) *> r
p *> (q <|> r)
=
=
(p *> r) <|> (q *> r)
(p *> q) <|> (p *> r)
Alle binären Kombinatoren sind assoziativ, zum Beispiel gilt
(p <|> q) <|> r
=
p <|> (q <|> r)
und es gibt auch ein neutrales Element
3
failure :: Parser a
für den <|>-Kombinator. failure ist ein Parser, der auf kein Wort passt, also
die leere Sprache repräsentiert.
Auf die gezeigte Weise kann man jede kontextfreie Grammatik mit Parserkombinatoren ausdrücken. Ein Problem stellen dabei aber linksrekursive Grammatiken dar. Übersetzt man die folgende Grammatik für die Sprache a*
AStar ::= AStar ’a’
|
in Parserkombinatoren
aStar = aStar *> char ’a’
<|> empty
dann terminiert der entsprechende Aufruf der parse-Funktion nicht (bzw. nur
mit einem Laufzeitfehler):
ghci> parse aStar "aaa"
*** Exception: stack overflow
Man muss Linksrekursion also eliminieren. Das Beispiel kann man so transformieren:
aStar = char ’a’ *> aStar
<|> empty
Dann terminiert die parse-Funktion.
ghci> parse aStar "aaa"
Just ()
Im Allgemeinen kann die Elimination von Linksrekursion komplizierter sein
(siehe Vorlesung: Übersetzerbau). Die Klasse der kontextfreien Grammatiken,
die man mit Parserkombinatoren ausdrücken kann, ist genau die Vereinigung
der LL(k)-Grammatiken für alle natürlichen Zahlen k. Parserkombinatoren erlauben also eine beliebig große Vorausschau. Wir werden später sehen, dass man
mit Parserkombinatoren sogar Sprachen erkennen kann, die nicht kontextfrei
sind.
Wir lernen nun weitere Kombinatoren kennen, die es erlauben, Parser mit
Zusatzinformation als Ausgabe zu definieren. Der einfachste dieser Parser wird
durch
4
yield :: a -> Parser a
konstruiert. yield x ist ein Parser, der das leere Wort erkennt und in dem Fall
x liefert.
Als komplizierteres Beispiel erweitern wir den Parser für korrekt geschachtelte
Klammer-Ausdrücke um ein Ergebnis:
nesting :: Parser Int
nesting
= (\m n -> max (m+1) n)
<$> (char ’(’ *> nesting <* char ’)’)
<*> nesting
<|> yield 0
Der Parser nesting erkennt die selbe Sprache wie nested gibt aber zusätzlich
die maximale Schachtelungstiefe aus:
ghci> parse nesting "(()(()()))"
Just 3
ghci> parse nesting ""
Just 0
ghci> parse nesting "(()()"
Nothing
Wir haben zur Definition von nesting zwei neue Kombinatoren verwendet. Der
erste
(<$>) :: (a -> b) -> Parser a -> Parser b
wendet eine Funktion auf das Ergebnis eines Parsers an. Das Ergebnis von <$>
ist ein Parser, der die selbe Sprache erkennt wie das zweite Argument aber ein
durch das erste Argument verändertes Ergebnis liefert. Der Typ von <$> erinnert an die map-Funktion. Tatsächlich können wir den Typkonstruktor Parser
zu einer Instanz der Klasse Functor machen,
instance Functor Parser where
fmap = (<$>)
denn es gilt
id <$> p
f <$> (g <$> p)
=
=
p
(f . g) <$> p
5
Sowohl <$> als auch <*> sind linksassoziativ. Im Beispiel wird also eine Funktion
vom Typ
Int -> Int -> Int
auf den Parser
(char ’(’ *> nesting <* char ’)’)
vom Typ Parser Int angewendet. Das Ergebnis ist ein Parser vom Typ
Parser (Int -> Int)
der ein Funktion liefert! Dieser wird dann mit dem Kombinator <*> mit dem
Parser nesting vom Typ Parser Int zu einem Parser vom Typ Parser Int
kombiniert. Der Kombinator <*> hat den folgenden Typ:
(<*>) :: Parser (a->b) -> Parser a -> Parser b
Dieser Typ ähnelt dem des <$>-Kombinators nur dass die Funktion nicht direkt
übergeben sondern von einem Parser geliefert wird. Tatsächlich können wir jede
Verwendung von <$> auch mit <*> ausdrücken, denn es gilt:
f <$> p
=
yield f <*> p
<*> konstruiert also einen Parser, der die gegebenen Parser hintereinander
ausführt und die Funktion, die der erste Parser liefert, auf das Ergebnis des
zweiten Parsers anwendet. Die beiden anderen Sequenz-Kombinatoren könnten
wir mit Hilfe von <$> und <*> definieren, denn es gilt:
p <* q
p *> q
=
=
(\x _ -> x) <$> p <*> q
(\_ y -> y) <$> p <*> q
Neben den gezeigten Gleichungen gelten auch die folgenden:
f <$> yield x
p <*> yield y
p <*> (q <*> r)
=
=
=
yield (f x)
($y) <$> p
(.) <$> p <*> q <*> r
6
In der ersten Gleichung wird eine Funktion auf das Ergebnis eines Parsers
angewendet, der das leere Wort erkennt und x liefert. Die zweite Gleichung
zeigt, auf welche Weise man die Funktion, die ein Parser liefert, auf einen
Wert anwenden kann und die dritte behandelt die Hintereinanderausführung
von Funktionen, die von Parsern geliefert werden.
Wir betrachten ein weiteres Beispiel für Parser mit Ergebnis und verwenden
dabei zusätzlich die folgenden Kombinatoren.
anyChar :: Parser Char
check
:: (a -> Bool) -> Parser a -> Parser a
Der Parser anyChar c liest ein einzelnes Zeichen und gibt es zurück und check
verändert einen Parser so, dass er fehlschlägt, wenn sein Ergebnis das gegebene
Prädikat nicht erfüllt. Wir könnten zum Beispiel den Kombinator char mit
Hilfe vom anyChar und check definieren:
char :: Char -> Parser ()
char c = check (c==) anyChar *> empty
Das abschließende *> empty ist hier nur dazu da, den Ergebnistyp des Parsers
von a nach () zu ändern.
Ein Vorteil von Parserkombinatoren ist, dass man sich aufbauend auf existierenden Kombinatoren neue Kombinatoren definieren kann, um sie später zur Definition von Parsern zu verwenden. Als Beispiel definieren wir den Kombinator
many, der einen Parser beliebig oft hintereinander ausführt.
many :: Parser a -> Parser [a]
many p = (:) <$> p <*> many p
<|> yield []
Die Ergebnisse des gegebenen Parsers p werden gesammelt und als Liste von
many p zurückgeliefert. Wir können die neuen Kombinatoren verwenden, um
einen Parser für Palindrome zu definieren.
palindrom
= check (\ (u,v) -> u == reverse v)
$ (,)
<$> many anyChar
<*> many anyChar
Dieser Parser erkennt zunächst zwei beliebige Worte u und v und testet dann,
ob u die Umkehrung von v ist.
7
ghci> parse palindrom "anna"
Just ("an","na")
ghci> parse palindrom "otto"
Just ("ot","to")
Dieses Beispiel zeigt, dass die Sprachklasse, die man mit Parserkombinatoren
erkennen kann, auch nicht-kontextfreie Sprachen enthält.
Implementierung
Wir werden nun sehen, wie man den Parser-Typ in Haskell implmentieren kann.
Da die parse-Funktion die einzige ist, die auf Parsern aufgerufen wird und
dabei keinen neuen Parser erzeugt, könnten wir versuchen, den Parser-Typ als
ebendiese Funktion zu definieren:
type Parser a = String -> Maybe a
parse :: Parser a -> String -> Maybe a
parse p = p
Diese Darstellung stößt jedoch schnell an ihre Grenzen. Zum Beispiel bei der
Definition des *>-Kombinators:
(*>) :: Parser a -> Parser b -> Parser b
p *> q = \s -> p s ??? q s
Wir können keine sinnvolle Definition für *> angeben, weil der zweite Parser q
nicht auf der kompletten Eingabe aufgerufen werden soll sondern auf dem Rest
der Eingabe, die nach dem Parsen von p noch übrig ist.
Wir könnten den Parser-Typ daher wie folgt ändern:
type Parser a = String -> Maybe (a,String)
Zusätzlich zum Ergebnis, liefert jeder Parser nun den Teil der Eingabe zurück,
den er nicht verbraucht hat. Die parse-Funktion müssten wir dann wie folgt
umschreiben:
parse :: Parser a -> String -> Maybe a
parse p s = case p s of
Just (x,"") -> Just x
_
-> Nothing
8
parse liefert genau dann ein Ergebnis, wenn der Parser eines liefert und die
restliche Eingabe leer ist.
Mit dieser Definition können wir *> sinnvoll implementieren:
(*>) :: Parser a -> Parser b -> Parser b
p *> q = \s -> case p s of
Just (_,s’) -> q s’
Nothing
-> Nothing
Wir ignorieren das Ergebnis des ersten Parsers p und geben nur die verbleibende
Eingabe an den Parser q weiter.
Wir versuchen nun weitere Kombinatoren zu definieren. empty liefert das Ergebnis () und die Eingabe unverändert zurück.
empty :: Parser ()
empty = \s -> ((),s)
Die char-Funktion liefert einen Parser, der () liefert, wenn das erste Zeichen
der Eingabe das gegebene Zeichen ist:
char :: Char -> Parser ()
char x (c:cs) | x == c
= Just (c,cs)
| otherwise = Nothing
Bei der Definition von <|> zur Deklaration von Alternativen, parsen wir erst
mit dem ersten Parser und, wenn dieser fehlschlägt, mit dem zweiten:
(<|>) :: Parser a -> Parser a -> Parser a
p <|> q = \s -> case p s of
Just xs -> Just xs
Nothing -> q s
Diese Implementierung erfüllt jedoch nicht das Distributivgesetz, wie das folgende Beispiel zeigt:
test1
test2
=
(empty <|> char ’a’) *> char ’b’
= (empty *> char ’b’)
<|> (char ’a’ *> char ’b’)
Der erste Parser erkennt das Wort "ab" nicht, der zweite hingegen schon.
9
ghci> parser test1 "ab"
Nothing
ghci> parser test2 "ab"
Just ()
Wir definieren schließlich den Parser-Typ unter Verwendung von Listen statt
Maybe-Werten, um die Distributivgesetze zu erfüllen:
type Parser a = String -> [(a,String)]
Ein Parser kann also mehrere Ergebnisse liefern und zu jedem Ergebnis kann
auch ein unterschiedlicher Anteil der Eingabe übrig bleiben.
Die parse-Funktion testet, ob es unter den Ergebnissen eines mit leerer Resteingabe
gibt und gibt dieses dann zurück.
parse :: Parser a -> String -> Maybe a
parse p s = case filter (null.snd) $ p s of
(x,_):_ -> Just x
_
-> Nothing
Falls es mehrere Ergebnisse mit leerer Resteingabe gibt, wird einfach das erste
zurück gegeben.
Wir passen nun die bisher definierten Kombinatoren an Listen an. Das leere
Wort kann man auf genau eine Art parsen. Der Parser für empty liefert also
eine einelementige Liste:
empty :: Parser ()
empty = \s -> [((),s)]
Der Parser für ein Zeichen liefert entweder eine einelementige oder eine leere
Liste:
char :: Char -> Parser ()
char x (c:cs) | x == c = [((),cs)]
char x _
= []
Der folgende Aufuf zeigt, wie sich ein mit char erzeugter Parser verhält:
ghci> char ’a’ "abc"
[((),"bc")]
10
Das erste Zeichen ist weggelesen, neben dem einzigen Ergebnis () bleibt als
"bc" als Resteingabe. Wenn das erste Zeichen nicht das gesuchte ist, wird gar
kein Ergebnis geliefert:
ghci> char ’a’ "bc"
[]
Alternativ zur eben gezeigten Definition können wir char auch mit check und
anyChar definieren:
anyChar :: Parser Char
anyChar []
= []
anyChar (c:cs) = [(c,cs)]
check :: (a->Bool) -> Parser a -> Parser a
check ok p = filter (ok . fst) . p
char :: Char -> Parser ()
char c = check (c==) anyChar *> empty
anyChar liefert, auf die leere Eingabe angewendet, kein Ergebnis und ansonsten das erste Zeichen der Eingabe als Ergebnis und die restlichen Zeichen als
verbleibende Eingabe. check ok p filtert aus den Ergebnissen von p die Ergebnisse heraus, die das Prädikat ok erfüllen.
Den Sequenz-Operator *> definieren wir auf Listen wie folgt:
(*>) :: Parser a -> Parser b -> Parser b
p *> q =
\s -> [ xs | (_,s’) <- p s, xs <- q s’ ]
Wie bei der Maybe-Variante ignorieren wir das Ergebnis des ersten Parsers p,
reichen aber die verbleibende Eingabe s’ an den zweiten Parser q weiter und
liefern dessen Ergebnisse zurück.
Wir können zwei beliebige Zeichen hintereinander lesen, indem wir zwei anyCharParser mit *> kombinieren:
ghci> (anyChar *> anyChar) "abc"
[(’b’,"c")]
Da das Gesamtergebnis, das Ergebnis des zweiten Parsers ist, erhalten wir als
Ergebnis das Zeichen ’b’. Als Resteingabe verbleibt "c".
Der Kombinator <*> verallgemeinert *>, da er das Ergebnis des ersten Parsers
nicht ignoriert sondern auf das Ergebnis des zweiten anwendet:
11
(<*>) :: Parser (a->b) -> Parser a -> Parser b
p <*> q =
\s -> [ (f x,s2) | (f,s1) <- p s,
(x,s2) <- q s1 ]
Mit Hilfe von <*> können wir die anderen Sequenz-Kombinatoren definieren.
Da wir <* noch nicht definiert haben, holen wir das hiermit nach:
(<*) :: Parser a -> Parser b -> Parser a
p <* q = const <$> p <*> q
Auch <$> können wir mit <*> definieren:
(<$>) :: (a -> b) -> Parser a -> Parser b
f <$> p = yield f <*> p
yield ist hierbei eine Verallgemeinerung des Parsers empty, die den gegebenen
Wert als Ergebnis liefert, aber keine Eingabe verbraucht.
yield :: a -> Parser a
yield x = \s -> [(x,s)]
Zur Illustration dieser Kombinatoren betrachten wir das Ergebnis eines Aufrufs
des Parsers anyChar <* anyChar:
ghci> let c = yield const
ghci> :t c
Parser (a -> b -> a)
ghci> let a = c <*> anyChar
ghci> :t a
Parser (b -> Char)
ghci> let ab = a <*> anyChar
ghci> :t ab
Parser Char
ghci> ab "abc"
[(’a’,"c")]
Der <*> Kombinator wendet schrittweise die const-Funktion auf die Ergebnisse
der beiden anyChar-Parser an. Im Ergebnis wird also das Zeichen ’a’ mit
verbleibender Eingabe "c" geliefert.
Es bleibt noch die Definition des <|>-Kombinators zur Deklaration von Alternativen in einer Grammatik. <|> wendet beide Parser auf die Eingabe an und
konkateniert deren Ergebnisse.
12
(<|>) :: Parser a -> Parser a -> Parser a
p <|> q = \s -> p s ++ q s
Das folgende Beispiel zeigt, wie ein mit <|> definierter Parser unterschiedliche
Ergebnisse liefert:
ghci> (empty <|> char ’a’) "abc"
[((),"abc"),((),"bc")]
Dieser Parser gibt entweder () zurück, ohne ein Zeichen von der Eingabe zu
lesen, oder liest ein ’a’ und liefert als verbleibende Eingabe "bc".
Der Parser failure ist neutral bezüglich <|>, da er kein Ergebnis liefert:
failure :: Parser a
failure _ = []
Die Definition von <|> für Listen erfüllt im Gegensatz zur Maybe-Variante das
Distributivgesetz. Dadurch probiert ein Parser mit Backtracking alle möglichen
Parser-Läufe. Dies ist zum Beispiel für den Palindrom-Parser notwendig, der in
der Maybe-Variante Palindrome nicht erkennt.
Backtracking kann zu Effizienzproblemen führen. Daher gibt es einen alternativen Alternativ-Kombinator <!>, der kein Backtracking verursacht sondern die
zweite Alternative nur ausführt, wenn die erste fehlschlägt:
(<!>) :: Parser a ->
p <!> q = \s -> case
[]
xs
Parser a -> Parser a
p s of
-> q s
-> xs
Wie <|> ist auch <!> assoziativ mit neutralem Element failure. Statt des
Distributivgesetzes gilt:
yield x <!> p
=
yield x
Man verwendet <!> in der Regel dann, wenn man weiß, dass die zweite Alternative nicht zum Erfolg führen kann, falls die erste schon erfolgreich war.
Der folgende Parser für Binärzahlen in LSB-Darstellung (least significant bit
first) demonstriert noch einmal die Verwendung der definierten Kombinatoren:
binary
bit
= (\b n -> 2*n + b) <$> bit <*> binary
<!> yield 0
= char ’0’ *> yield 0
<!> char ’1’ *> yield 1
13
Der binary-Parser liest folgen von Nullen und Einsen und berechnet als Ergebnis den Wert der zugehörigen Binärzahl. Dazu wird die Funktion (\b n -> 2*n + b)
mit den Kombinatoren <$> und <*> auf die Ergebnisse der Parser bit und
binary angewendet. Wenn kein Zeichen mehr vorhanden ist, gibt binary die
Zahl Null zurück. Da die Alternativen in diesem Beispiel sich gegenseitig ausschließen, verwenden wir <!> statt <|>.
Hier einige Beispielaufrufe:
ghci> parse
Just 0
ghci> parse
Just 0
ghci> parse
Just 1
ghci> parse
Just 1
ghci> parse
Just 2
ghci> parse
Just 3
ghci> parse
Just 11
ghci> parse
Just 42
binary ""
binary "0"
binary "1"
binary "10"
binary "01"
binary "110"
binary "1101"
binary "010101"
Ein einfache Verallgemeinerung des Parser-Typs ist es, den Typ der Eingabe
zu parametrisieren. Mit einem Typ
type Parser tok a = [tok] -> [(a,[tok])]
kann man ohne wesentliche Änderungen an der Definition der Kombinatoren
beliebige Tokenfolgen parsen. Dies ist besonders dann nötig, wenn dem Parser
ein Scanner vorgeschaltet ist, der die Eingabe in Symbolklassen zerlegt.
Die hier gezeigte Implementierung der Parserkombinatoren definiert den ParserTyp durch ein Typsynonym. Stattdessen sollte man einen newtype verwenden,
damit man Typklasseninstanzen für Parser angeben kann. Darauf haben wir
hier nur aus Gründen der Übersichtlichkeit verzichtet.
Monadische Parser
Wir haben am Beispiel der Palindrome gesehen, dass man mit Parserkombinatoren nicht nur kontextfreie Sprachen erkennen kann. Das liegt auch daran,
dass man zur Laufzeit Parser generieren kann, die von Ergebnissen anderer
14
Parser abhängen. Der check-Kombinator ist ein Bespiel für solche Parser,
da er das Ergebnis eines Parsers verwendet um die Sprache, die er erkennt,
einzuschränken. Wir können diese Idee auch in einem anderen Kombinator
wieder finden.
Der Kombinator *>= übergibt das Ergebnis eines Parsers an eine Funktion, die
daraus einen neuen Parser berechnet:
(*>=) :: Parser a -> (a->Parser b) -> Parser b
Mit Hilfe dieses Kombinators können wir den Palindrom-Parser so umschreiben,
dass er nur noch ein beliebiges Wort, gefolgt von dessen Umkehrung liest, also
nicht mehr beliebige Kombinationen aus Worten, die erst anschließend getestet
werden.
palindrom =
many anyChar *>= \u ->
word (reverse u)
Der Parser word (reverse u) kennt das zuvor gelesene Wort u und liest dann
das Wort reverse u. word ist eine Funktion, die aus einer bereits gelesenen
Eingabe, zur Laufzeit einen Parser generiert:
word :: String -> Parser ()
word []
= empty
word (c:cs) = char c *> word cs
Da der word-Kombinator () als Ergebnis liefert, gilt dies auch für den neuen
Palindrom-Parser:
ghci> parse palindrom "anna"
Just ()
ghci> parse palindrom "otto"
Just ()
ghci> parse palindrom "hans"
Nothing
Der *>=-Kombinator hat genau den Typ des monadischen >>=-Operators. Außerdem entspricht yield der return-Funktion. Da die Monadengesetze für diese
Parser-Kombinatoren erfüllt sind, können wir eine Instanz der Klasse Monad für
Parser angeben:
instance Monad Parser where
return = yield
(>>=) = (*>=)
15
Diese Instanz erlaubt es, Parser mit Hilfe von do-Notation zu definieren. Zum
Beispiel könnten wir den Parser für korrekt geschachtelte Klammer-Ausdrücke
so umschreiben:
nested
= do char ’(’
nested
char ’)’
nested
<|> empty
Parser, die im do-Block hintereinander stehen, werden hintereinander ausgeführt,
und wir können auf die Ergebnisse von einzelnen Parsern mit einem Linkspfeil
zugreifen:
nesting
= do char ’(’
m <- nesting
char ’)’
n <- nesting
return (max (m+1) n)
<|> return 0
Durch die do-Notation können wir die Ergebnisse der Parser beachten, die wir
benötigen, und brauchen nicht die Kombinatoren *> und <* zu verwenden, um
einzelne Ergebnisse zu ignorieren.
Schließlich können wir auch den Palindrom-Parser in do-Notation schreiben:
palindrom =
= do u <- many anyChar
word u
<|> empty
Wir kommen nun zur Implementierung des *>=-Kombinators:
(*>=) :: Parser a -> (a->Parser b) -> Parser b
p *>= f =
\s -> [ (y,s2) | (x,s1) <- p s,
(y,s2) <- f x s1 ]
Das Ergebnis x des Parsers p wird der Funktion f übergeben, die daraus einen
neuen Parser berechnet, der mit der verbleibenden Eingabe aufgerufen wird.
Der *>=-Kombinator ist der mächtigste der Sequenz-Kombinatoren. Wir haben
bereits gesehen, dass man <* und *> mit Hilfe von <*> ausdrücken kann. Das
folgende Gesetz zeigt, dass man <*> mit Hilfe von *>= ausdrücken kann:
16
p <*> q
=
p *>= \f -> q *>= \x -> yield (f x)
Man könnte also auch <$> durch *>= und yield definieren.
Applikative Funktoren
Wir haben bereits gesehen, dass <$> der Funktion fmap aus der Functor-Klasse
entspricht. Ferner entsprechen yield und *>= den Monad-Operationen return
und >>=. Auch <*> ist in Haskell mit einer Typkonstruktorklasse abstrahiert:
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
(*>) :: f a -> f b -> f b
(<*) :: f a -> f b -> f a
Die Funktion pure entspricht yield bzw der return-Funktion der Klasse Monad.
Obwohl *> und <* durch <*> definiert werden können, wurden sie mit in die
Applicative-Klasse aufgenommen, um es Programmierern zu ermöglichen, effizientere Implementierungen anzugeben.
Instanzen der Klasse Applicative müssen die folgenden Gesetze erfüllen, die
wir schon für Parser kennen gelernt haben.
Die Functor-Instanz muss im folgenden Sinn verträglich mit der Implementierung von pure und <*> sein:
fmap f u
=
pure f <*> u
Die Functor-Gesetze übertragen sich also auf applikative Funktoren. Außerdem
muss gelten:
u<*>(v<*>w)
pure (f x)
u<*>pure x
u *> v
u <* v
=
=
=
=
=
pure
pure
pure
pure
pure
(.)<*>u<*>v<*>w
f <*> pure x
($x) <*> u
(const id)<*>u<*>v
const <*> u <*> v
Für jeden Typkonstruktor M, der Instanz der Klasse Monad ist, kann man eine
Applicative-Instanz wie folgt definieren:
instance Functor M where
fmap = liftM
instance Applicative M where
pure = return
(<*>) = ap
17
Die Funktionen liftM und ap sind im Modul Control.Monad definiert. Für *>
und <* gibt es Default-Implementierungen in der Applicative-Klasse gemäß
der obigen Gesetze, man braucht dafür also keine Implementierungen anzugeben.
Für viele vordefinierte Monaden ist auch eine Applicative-Instanz definiert, so
dass man monadische Programme auch in applikativem Stil schreiben kann.
Zum Beispiel ist bei der Definition von Arbitrary-Instanzen für QuickCheckTesteingaben der applikative Stil oft übersichtlicher. Statt:
arbitrary = return Leaf
‘mplus‘ do l <- arbitrary
x <- arbitrary
r <- arbitrary
return (Branch l x r)
kann man unter Verwendung der Kombinatoren <*> und <$> (letzterer ist im
Modul Data.Functor als Synonym für fmap definiert) die folgende Definition
angeben:
arbitrary = return Leaf
‘mplus‘ Branch <$> arbitrary
<*> arbitrary
<*> arbitrary
Wie man sieht, ist es auch möglich monadische Kombinatoren (wie return und
mplus) mit den applikativen zu mischen.
18
Herunterladen