Parsecの練習でよくある電卓を「演算子を拡張可能」と「内部では有理数として扱う」ことを念頭にこちらを参考にして作ってみました。コード全文はgithubにおいてあります。作成時に使用しただけの関数もインポートしている可能性があるのでコード全文を読むときは注意してください。
どんな電卓か
まず最初に電卓のASTですがこちらになります。
data CalcAST =
Number Rational
| BinaryOp String CalcAST CalcAST
| UnaryOp String CalcAST
| Paren CalcAST
木構造なので説明しなくてもわかると思います。ここで数値型としてRational
を使っています。
次に、使える二項演算子は次となります。
bop0Map = [
("*", (*))
, ("/", (/))
]
bop1Map = [
("+", (+))
, ("-", (-))
]
一つ目は掛け算と同じ優先順位、二つ目は足し算と同じ優先順位の演算子との対応が用意されています。この辞書に追加するとパースしてもらえるようになります。
次に、単項演算子は次となります。考え方は二項演算子と同じ辞書形式になるので説明は省略します。
uopMap = [
("-", ((-) 0))
, ("abs", abs)
, ("sign", signum)
]
それぞれのパーサー
数字用
ratio :: Parser Rational
ratio = do
xs <- many1 digit
ys <- optionMaybe (char '.' *> many1 digit)
return $ convert xs ys
where
convert :: String -> Maybe String -> Rational
convert xs Nothing = toRational $ (read :: String -> Integer) xs
convert xs (Just ys) = (convert xs Nothing) + (toRational $ (read :: String -> Double) $ "0." ++ ys)
num :: Parser CalcAST
num = Number <$> ratio <* spaces
数字はratio
で読み取り、num
はcalcAST
型に包むだけです。
ratio
は"123"や"123.4"のどちらも読み取るように制作しています。
記号関係用
symbol :: String -> Parser String
symbol xs = do
res <- string xs
spaces
return res
paren :: Parser CalcAST
paren = do
symbol "("
res <- expr
symbol ")"
return $ Paren res
unary :: String -> Parser (CalcAST -> CalcAST)
unary s = do
symbol s
return $ UnaryOp s
binary :: String -> Parser (CalcAST -> CalcAST -> CalcAST)
binary s = do
symbol s
return $ BinaryOp s
役目は関数名を見るとわかると思います。paren
で出てくるexpr
は次で出てきます。
式用
term :: Parser CalcAST
term = try paren <|> num <|> withUop
where
uops = map unary (map fst uopMap)
withUop = do
uop <- choice uops
n <- term
return $ uop n
expr :: Parser CalcAST
expr = term `chainl1` (choice bop0) `chainl1` (choice bop1)
where
bop0 = map binary (map fst bop0Map)
bop1 = map binary (map fst bop1Map)
それぞれのパーサの説明を簡単にしますとterm
は単項用、expr
は複数項用となります。
まずterm
は括弧、数字、単項演算子から始まる単項のどれかをパースします。これは"--6"もパースできることを意味します。
次にexpr
はbop1Map
の二項演算子でつながった「bop0Map
でつながった単項」をパースします。
利用用
mainParser = expr <* eof
式は余分なものがなくて最後にパースするのは終端(つまりEOF)で終わるということとしてパースします。
こうして出来上がったパーサーをMain関数で引数を読み取って計算させます。それが次となります。
main :: IO ()
main = do
arg <- head <$> getArgs
let parseRes = parseCalcAST mainParser arg
case parseRes of
Right r -> do
putStrLn $ "calculateAST from\n" ++ show r
let (Right calcRes) = fromRational <$> (calc r)
putStrLn $ "to " ++ show calcRes
Left e -> print e
試しに実行してみると
*Main Lib> :main "abs -3 * 4 + 3"
calculateAST from
BinaryOp "+" (BinaryOp "*" (UnaryOp "abs" (UnaryOp "-" (Number (3 % 1)))) (Number (4 % 1))) (Number (3 % 1))
to 15.0
*Main Lib> :main "abs -3 * -4 + 3"
calculateAST from
BinaryOp "+" (BinaryOp "*" (UnaryOp "abs" (UnaryOp "-" (Number (3 % 1)))) (UnaryOp "-" (Number (4 % 1)))) (Number (3 % 1))
to -9.0
といった感じになります。
まとめ
Parsecを学ぶ人にとって電卓は最初の壁のようなものなのでParsecの練習がてら作成してみました。最初はただ書き写そうと思っていたのですが、それでは少しつまらないなと思ったので改造してみました。Haskell、Parsecともに初心者なので、良くない書き方があるかと思います。もし間違いがあったら指摘してくれるとありがたいです。そしてlexeme
を使ってパーサーを自動生成する方法があるよと言う方もいると思います。そういう方はqiitaにlexeme
版を投稿してください。お願いします。(情報が探しにくいおかげで勉強したいのになかなか自分の中で情報がまとまらない為。)
簡潔すぎる記事ですがこれで終わりにさせていただきます。最後まで読んでいただきありがとうございました。