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 Parser-Lauf 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 () Bei anderen Eingaben liefert dieser Aufruf Nothing. 1 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 Klammer-Ausdrücke erkennt. Die zugehörige BNF sieht so aus: Nested ::= ’(’ Nested ’)’ Nested | 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: 2 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 parseFunktion: 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 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* 3 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 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 KlammerAusdrücke um ein Ergebnis: nesting :: Parser Int nesting = (\m n -> max (m+1) n) <$> (char ’(’ *> nesting <* char ’)’) <*> nesting <|> yield 0 4 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 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 eine 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: 5 (<*>) :: 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 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. 6 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. 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 implementieren 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: 7 (*>) :: 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 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 -> Just ((),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 8 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 äb" nicht, der zweite hingegen schon. ghci> parser test1 "ab" Nothing ghci> parser test2 "ab" Just () Wir definieren schließlich den Parser-Typ unter Verwendung von Listen statt MaybeWerten, 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 _ = [] 9 Der folgende Aufuf zeigt, wie sich ein mit char erzeugter Parser verhält: ghci> char ’a’ "abc" [((),"bc")] 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 anyChar-Parser mit *> kombinieren: ghci> (anyChar *> anyChar) "abc" [(’b’,"c")] 10 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: (<*>) :: 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. 11 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. (<|>) :: 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: 12 binary bit = (\b n -> 2*n + b) <$> bit <*> binary <!> yield 0 = char ’0’ *> yield 0 <!> char ’1’ *> yield 1 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 Parser-Typ 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. 13 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 Parser abhängen. Der checkKombinator 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 PalindromParser: 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 ParserKombinatoren erfüllt sind, können wir eine Instanz der Klasse Monad für Parser angeben: instance Monad Parser where return = yield (>>=) = (*>=) 14 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: p <*> q = p *>= \f -> q *>= \x -> yield (f x) Man könnte also auch <$> durch *>= und yield definieren. 15 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 ApplicativeKlasse 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 16 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. 17