Functional Programming
Transkrypt
Functional Programming
Programowanie w Haskellu Wykład 7 - Parsowanie 1 Czym jest parser? Parser jest programem dokonującym analizy składniowej tekstu w celu wyznaczenia jego struktury. + ”2∗3+4” znaczy ∗ 2 4 23 2 Określenie typu parsera W językach takich jak Haskell parser jest oczywiście funkcją. type Parser = String −> Drzewo Parser jest funkcją odwzorowującą łańcuch w pewną formę drzewa. 3 Parser może nie przetworzyć wszystkich znaków łańcucha, dlatego zwracamy także nieprzetworzone dane: type Parser = String −> (Drzewo,String) Łańcuch może być zinterpretowany na kilka sposobów lub żaden, dlatego zwracany typ uogólniamy do listy rezultatów: type Parser = String −> [(Drzewo,String)] 4 Ostatecznie, parser nie musi produkować drzewa, dlatego uogólniamy go za pomocą zmiennej typowej: type Parser a = String −> [(a,String)] Uwagi: ❚ W celu uproszczenia prezentacji, będziemy rozważali jedynie takie parsery, które zawodzą (zwracając pustą listę) lub zwracają jeden element. 5 Podstawowe parsery ❚ Parser znak zawodzi, jeśli wejście jest puste, w przeciwnym razie „konsumuje” jeden znak: znak :: Parser Char znak = \wej −> case wej of [] −> [] (x:xs) −> [(x,xs)] 6 ❚ Parser niepoprawny zawsze zawodzi: niepoprawny :: Parser a niepoprawny = \wej −> [] ❚ Parser zwroc v jest niezawodny, zwraca wartość v nie konsumując żadnego znaku: zwroc :: a −> Parser a zwroc v = \wej −> [(v,wej)] 7 ❚ Parser p +++ q działa jak parser p jeśli ten nie zawodzi, w przeciwnym razie działa jak parser q: (+++) :: Parser a −> Parser a −> Parser a p +++ q = \wej −> case p wej of [] −> parsuj q wej [(v,reszta)] −> [(v,reszta)] ❚ Funkcja parsuj uruchamia parser na łańcuchu: parsuj :: Parser a −> String −> [(a,String)] parsuj p wej = p wej 8 Przykłady Działanie czterech podstawowych parserów zostanie zilustrowane kilkoma prostymi przykładami: $ ghci Parsowanie > parsuj znak "" [] > parsuj znak "abc" [('a',"bc")] 9 > parsuj niepoprawny "abc" [] > parsuj (zwroc 1) "abc" [(1,"abc")] > parsuj (znak +++ zwroc 'd') "abc" [('a',"bc")] > parsuj (niepoprawny +++ zwroc 'd') "abc" [('d',"abc")] 10 Uwagi: ❚ Niektóre kompilatory Haskella dla pierwszego z podanych przykładów zwracają błąd związany z typami. ❚ Typ parsera jest tzw. monadą, strukturą matematyczną służącą do modelowania różnego rodzaju obliczeń. 11 Przetwarzanie sekwencyjne Ciąg parserów można złożyć do pojedynczego parsera za pomocą operatora (>>>). (>>>) :: Parser a -> (a->Parser b) -> Parser b p >>> f = \wej -> case parsuj p wej of [] -> [] [(v,r)] -> parsuj (f v) r Na przykład: p :: Parser (Char,Char) p = znak >>> \x -> znak >>> \_ -> znak >>> \y -> zwroc (x,y) 12 ❚ Jeśli dowolny parser w ciągu parserów zawiedzie, to zawiedzie całość. Na przykład: > parsuj p "abcdef" [((’a’,’c’),"def")] > parsuj p "ab" [] 13 Parsery wywiedzione ❚ Parsowanie znaku spełniającego predykat: spelnia :: (Char -> Bool) -> Parser Char spelnia p = znak >>> \x -> if p x then zwroc x else niepoprawny 14 ❚ Parsowanie cyfry oraz specjalnego symbolu: cyfra :: Parser Char cyfra symbol = spełnia isDigit :: Char −> Parser Char symbol x = spelnia (x ==) ❚ Stosowanie parsera zero lub wiele razy: wiele :: Parser a −> Parser [a] wiele p = wiele1 p +++ zwroc [] 15 ❚ Stosowanie parsera jeden lub wiele razy: wiele1 :: Parser a -> Parser [a] wiele1 p = p >>> \v -> wiele p >>> \vs -> zwroc (v:vs) ❚ Parsowanie tylko podanego łańcucha: lancuch :: String −> Parser String lancuch [] = zwroc [] lancuch (x:xs) = symbol x >>> \_ -> lancuch xs >>> \_ -> zwroc (x:xs) 16 Przykłady Łatwo można skonstruować parser do złączania listy jednej lub wielu cyfr w jeden łańcuch: t :: Parser String t = symbol '[' >>> \_ cyfra >>> \d -> wiele (symbol ',' cyfra) >>> symbol ']' >>> \_ zwroc (d:ds) -> >>> \_ -> \ds -> -> 17 Na przykład: > parsuj t "[1,2,3,4]" [("1234","")] > parsuj t "[1,2,3,4" [] Uwaga: ❚ Bardziej wyrafinowane biblioteki do analizy składniowej powinny obsługiwać błędy. 18 Wyrażenia arytmetyczne Rozważmy proste wyrażenia nawiasowe złożone z cyfr, operacji dodawania + oraz mnożenia *. Załóżmy dodatkowo, że: ❚ operatory * oraz + łączą od lewej do prawej; ❚ operator * ma wyższy priorytet niż +. 19 Formalną składnię takich wyrażeń można zdefiniować za pomocą następującej gramatyki bezkontekstowej: wyrażenie → składnik '+' wyrażenie składnik składnik → czynnik '*' składnik czynnik czynnik → cyfra '(' wyrażenie ')' cyfra → '0' '1' … '9' 20 Niektóre reguły produkcji można skrócić przez wykorzystanie łańcucha pustego: wyrażenie → składnik ('+' wyrażenie ε) składnik → czynnik ('*' składnik ε) Uwaga: ❚ Symbol ε oznacza łańcuch pusty. 21 Korzystając z wcześniej zdefiniowanych parserów łatwo jest opracować parser wyznaczający wartość takich wyrażeń arytmetycznych. Zapiszmy gramatykę w Haskellu: wyrazenie :: Parser Int wyrazenie = skladnik >>> \s -> ( (symbol '+' >>> \_ -> wyrazenie >>> \w -> zwroc (s + w)) +++ zwroc s ) 22 skladnik :: Parser Int skladnik = czynnik >>> \c -> ( (symbol '*' >>> \_ -> skladnik >>> \s -> zwroc (c * s)) +++ zwroc c ) czynnik :: Parser czynnik = (cyfra zwroc +++ Int >>> \c -> (digitToInt c)) (symbol '(' >>> \_ -> wyrazenie >>> \w -> symbol ')' >>> \_ -> zwroc w) 23 W końcu definiujemy funkcję wyliczającą wartość: oblicz :: String −> Int oblicz xs = fst (head (parsuj wyrazenie xs)) Na przykład: > oblicz "2*3+4" 10 > oblicz "2*(3+4)" 14 24 Ćwiczenia (1) Rozważmy parser: zad :: Parser String zad = wiele1 (spelnia (\x -> 'a' < x && x < 'd')) Jaki będzie wynik wykonania poniższego kodu? > parsuj zad "bbccbcabbbb" > parsuj zad "abcd" 25 (2) Rozważmy parser: zad :: Parser (Int, Int) zad = spelnia isAlpha >>> \z1 -> spelnia isDigit >>> \z2 -> zwroc (ord z1 - ord 'a' + 1, digitToInt z2) Jaki będzie wynik wykonania poniższego kodu? > parsuj zad "3d" > parsuj zad "d3" 26