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

Podobne dokumenty