HaskellとParsecでLisp REPL その4(四則演算)
今回の変更点
ついに四則演算の実装。入力をそのまま返すのではなく、入力を評価・変換して出力するようになり、「ちゃんとしたREPL」にちょっと近づく。
データ型とパーサ
今回は変更なし。四則演算ならAtom、List そしてNumber型だけで十分。演算子がAtom、数値がNumber、それを式として繋げる容器がListだ。
評価器
Evaluatorに一つ変換ルールを追加:
eval :: LispVal -> LispVal eval (List (Atom funcName : args)) = apply funcName $ map eval args eval lispVal = lispVal
ロジックとしては以下のとおり:
- ある
LispValがList型で、その先頭の要素が任意のAtom型の場合、そのListを関数適用の式とみなす。 - まず先頭以外の要素すべてを評価する
- 評価された先頭以外の要素に対し
apply funcNameで関数適用する(funcNameは先頭要素のAtom型が保持している文字列)
applyがキモで、これは新しいModuleのFunctionsで定義されている。
関数適用と演算の定義
Functions内をみていく。このModuleからはapplyしか公開しない。あとは内部実装。
まずはそのapplyの定義:
apply :: String -> [LispVal] -> LispVal apply funcName args = maybe (Number 0) ($ args) $ lookup funcName primitives
すこしややこしく見えるが、やっていることはおおまかに二つ
- Haskellの標準関数である
lookupを使ってfuncNameをprimitivesテーブルの中から探す lookupはMaybe型を返すので、それをmaybe関数で処理funcNameがprimitivesに含まれず、MaybeがNothingだった場合(Number 0)を返すfuncNameが見つかりMaybeがJust 該当する関数だった場合、その関数にargsを適用した結果を返す
($ args) fがf argsになるのは面白い。慣れるまですこし面食らったが・・・
primitivesテーブルは、「文字列と[LispVal] -> LispVal型の関数のタプル」が入っているHaskellのリスト:
primitives :: [(String, [LispVal] -> LispVal)] primitives = [ ("+", numBinOp (+)), ("-", numBinOp (-)), ("*", numBinOp (*)), ("/", numBinOp div) ]
今のところ四則演算のみ。Haskell自身の四則演算関数を自前のnumBinOpの引数にすることで[LispVal] -> LispVal型にしている。
numBinOpの定義:
numBinOp :: (Integer -> Integer -> Integer) -> [LispVal] -> LispVal numBinOp op args = Number $ foldl1 op $ map numerify args
Haskellの整数をとる二項演算関数を[LispVal] -> LispValに変換する。
LispValのリストであるargsを(後述の)numerify関数でHaskellのIntegerに変換- Integerのリストに対し、二項演算を
foldl1する(op a b c d) = (op (op (op a b) c) d) = (((aopb)opc)opd)
- その結果のIntegerを
Number型に格納して(LispVar化して)返す
という流れになっている。
numerifyのコード:
numerify :: LispVal -> Integer numerify (Number n) = n numerify _ = 0
パターンマッチでNumber型からは保持する整数を取り出し、それ以外のLispValはDon't Careパターンで0と解釈する。
これで四則演算の関数が定義・登録された。
現在気になっている点
- numBinOpが二項演算じゃない
- 任意の数に対して
foldl1している - パターンマッチで二項演算を強制することもできるが・・・
- 名前をnumBinOpからnumOpなどに変えたほうがいいかもしれない
- 任意の数に対して
- 動的なだけじゃなくて弱い型になっている
- 登録されていない関数適用は数値0を返す
- 数値以外の
LispValへの四則演算適用は、そのLispValを数値0と解釈して続行 - 後々エラー処理を追加するのでその時に解決
REPL・Main
変更なし
実行
>> (+ 1 2) 3 // 足せる >> (+ 1 2 3) 6 // 3以上の要素に対しても適用可能 >> (* (+ 5 (- 7 3)) (/ 9 2)) 36 // 四則すべて・ネストも可 >> (+ (1 2 3)) 0 // リストは関数適用時には0として認識される・要素1でも適用可能 >> (1 2 3) (1 2 3) // 関数適用外の場合リストはそのままの値として評価される >> (+ 1 x) 1 // `Atom`も0として評価される >> (f 5) 0 // 登録されていない`Atom`を関数として使う場合の結果も0 >> quit
やはりREPLらしくなってきた。
四則演算ができるところまで実装されたリリース
srcとapp/Main.hsにコードが記述されている。
次回
if構文をサポートしたいので、次回はとりあえずブール値を表すデータ型の追加。