プログラミング Haskell 第8章 関数型パーサー その4
ずいぶん長く更新を休みました。この1週間生活リズムに大きな変化があったこともあって朝の読書時間はなしでしたが、合間にちょこちょこと読んで第8章の練習問題をやっていました。
後半には BNF による文法規則の表記と構文木を作って評価するパーサーの話がありましたが、その内容はパーサーの一般的な論点で、Haskell のコードにどう落とすかという点は練習問題に自然に出てくるので割愛します。
練習問題を解くにあたって、本文に出てくる関数を実装して動作確認もしたいと考えたのですが、do 記法に対応するためには、実際には本文中のサンプルコードのままではだめで、Parser を Monad のインスタンスとして定義しないといけませんでした。この記法は本書中ではまだ出てきていないですが、「ふつうの Haskell プログラミング」を見たり、公開されているコードから記述方法を調べてまず以下のソースを書きました。
module Parse where import Char import Monad newtype Parser a = P (String -> [(a, String)]) instance Monad Parser where return v = P (\inp -> [(v, inp)]) p >>= f = P (\inp -> case parse p inp of [] -> [] [(v, out)] -> parse (f v) out) failure :: Parser a failure = P (\inp -> []) item :: Parser Char item = P (\inp -> case inp of [] -> [] (x:xs) -> [(x, xs)]) parse :: Parser a -> String -> [(a, String)] parse (P p) inp = p inp (+++) :: Parser a -> Parser a -> Parser a p +++ q = P (\inp -> case parse p inp of [] -> parse q inp [(v, out)] -> [(v, out)]) sat :: (Char -> Bool) -> Parser Char sat p = do x <- item if p x then return x else failure digit :: Parser Char digit = sat isDigit lower :: Parser Char lower = sat isLower upper :: Parser Char upper = sat isUpper letter :: Parser Char letter = sat isAlpha alphanum :: Parser Char alphanum = sat isAlphaNum char :: Char -> Parser Char char x = sat (== x) string :: String -> Parser String string [] = return [] string (x:xs) = do char x string xs return (x:xs) many :: Parser a -> Parser [a] many p = many1 p +++ return [] many1 :: Parser a -> Parser [a] many1 p = do v <- p vs <- many p return (v:vs) ident :: Parser String ident = do x <- lower xs <- many alphanum return (x:xs) nat :: Parser Int nat = do xs <- many1 digit return (read xs) space :: Parser () space = do many (sat isSpace) return () token :: Parser a -> Parser a token p = do space v <- p space return v identifier :: Parser String identifier = token ident natural :: Parser Int natural = token nat symbol :: String -> Parser String symbol xs = token (string xs) expr :: Parser Int expr = do t <- term do symbol "+" e <- expr return (t + e) +++ return t term :: Parser Int term = do f <- factor do symbol "*" t <- term return (f * t) +++ return f factor :: Parser Int factor = do symbol "(" e <- expr symbol ")" return e +++ natural eval :: String -> Int eval xs = case parse expr xs of [(n, [])] -> n [(_,out)] -> error ("unused input " ++ out) [] -> error "invalid input"
これを元にして第8章の練習問題を解きました。
- 1.
int :: Parser Int int = do sign <- (char '-') +++ return ' ' xs <- many1 digit return (read (sign:xs))
- 2.
comment :: Parser () comment = do string "--" many (sat (/= '\n')) char '\n' return ()
- 3. と 4. は構文木を書けという問題なので省略します
- 6.
expr :: Parser Int expr = do t <- term do symbol "+" e <- expr return (t + e) +++ do symbol "-" e <- expr return (t - e) +++ return t term :: Parser Int term = do f <- factor do symbol "*" t <- term return (f * t) +++ do symbol "/" t <- term return (f `div` t) +++ return f
- 7.
expr :: Parser Int expr = do t <- term do symbol "+" e <- expr return (t + e) +++ do symbol "-" e <- expr return (t - e) +++ return t
- 8. 問題文からははっきりしませんが、左結合の減算演算子「のみ」を扱うように制限した回答にしています
- a. expr := (expr '-' factor) | factor
- b. 表記をみやすくするため関数を分けています
expr_minus :: Parser Int expr_minus = do e <- expr_minus symbol "-" f <- factor return (e - f) expr :: Parser Int expr = expr_minus +++ factor
expr_minus :: Parser Int expr_minus = do symbol "-" f <- factor return f expr :: Parser Int expr = do f <- factor xs <- many expr_minus return (foldl (-) f xs)
第8章はパーサーを書きつつ自然とモナドに触れることで「モナド怖くないよ」と思わせるための構成なのかなと感じました。しかしオフサイドルールのため空白の数が重要なのに等幅でなかったり(+++ 演算子の前のインデントの間違いでしばらく悩みました)、サンプルコードが特殊な記号のままなのはそろそろ不便のほうが勝ってきたように思います。
またモナドへの理解がぐっと腑に落ちたものの、まだコードを書いていて混乱する時もあるので、慣れるにはもう少しコード書きを繰り返す必要がありそうです。
次は第9章「対話プログラム」です。ついに IO が登場します。まだ生活が完全に落ち着いたわけでもないので、次の更新がいつになるかはわかりません。しかし合間をみて読み進めてまた更新します。