LoginSignup
1
0

More than 5 years have passed since last update.

Parsecで少しだけ使えそうな電卓を作ってみた

Posted at

Parsecの練習でよくある電卓を「演算子を拡張可能」と「内部では有理数として扱う」ことを念頭にこちらを参考にして作ってみました。コード全文はgithubにおいてあります。作成時に使用しただけの関数もインポートしている可能性があるのでコード全文を読むときは注意してください。

どんな電卓か

まず最初に電卓のASTですがこちらになります。

src/Lib.hs
data CalcAST =
          Number Rational
        | BinaryOp String CalcAST CalcAST
        | UnaryOp String CalcAST
        | Paren CalcAST

木構造なので説明しなくてもわかると思います。ここで数値型としてRationalを使っています。
次に、使える二項演算子は次となります。

src/Lib.hs
bop0Map = [
      ("*", (*))
    , ("/", (/))
    ]
bop1Map = [
      ("+", (+))
    , ("-", (-))
    ]

一つ目は掛け算と同じ優先順位、二つ目は足し算と同じ優先順位の演算子との対応が用意されています。この辞書に追加するとパースしてもらえるようになります。
次に、単項演算子は次となります。考え方は二項演算子と同じ辞書形式になるので説明は省略します。

src/Lib.hs
uopMap = [
      ("-", ((-) 0))
    , ("abs", abs)
    , ("sign", signum)
    ]

それぞれのパーサー

数字用

src/Lib.hs
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で読み取り、numcalcAST型に包むだけです。
ratioは"123"や"123.4"のどちらも読み取るように制作しています。

記号関係用

src/Lib.hs
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は次で出てきます。

式用

src/Lib.hs
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"もパースできることを意味します。
次にexprbop1Mapの二項演算子でつながった「bop0Mapでつながった単項」をパースします。

利用用

src/Lib.hs
mainParser = expr <* eof

式は余分なものがなくて最後にパースするのは終端(つまりEOF)で終わるということとしてパースします。
こうして出来上がったパーサーをMain関数で引数を読み取って計算させます。それが次となります。

app/Main.hs
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版を投稿してください。お願いします。(情報が探しにくいおかげで勉強したいのになかなか自分の中で情報がまとまらない為。)
簡潔すぎる記事ですがこれで終わりにさせていただきます。最後まで読んでいただきありがとうございました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0