Arantium Maestum

プログラミング、囲碁、読書の話題

HaskellとParsecでLisp REPL その4(四則演算)

今回の変更点

github.com

ついに四則演算の実装。入力をそのまま返すのではなく、入力を評価・変換して出力するようになり、「ちゃんとしたREPL」にちょっと近づく。

データ型とパーサ

今回は変更なし。四則演算ならAtomList そしてNumber型だけで十分。演算子Atom、数値がNumber、それを式として繋げる容器がListだ。

評価器

Evaluatorに一つ変換ルールを追加:

eval :: LispVal -> LispVal
eval (List (Atom funcName : args)) = apply funcName $ map eval args
eval lispVal                       = lispVal

ロジックとしては以下のとおり:

  1. あるLispValList型で、その先頭の要素が任意のAtom型の場合、そのListを関数適用の式とみなす。
  2. まず先頭以外の要素すべてを評価する
  3. 評価された先頭以外の要素に対し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を使ってfuncNameprimitivesテーブルの中から探す
  • lookupMaybe型を返すので、それをmaybe関数で処理
    • funcNameprimitivesに含まれず、MaybeNothingだった場合(Number 0)を返す
    • funcNameが見つかりMaybeJust 該当する関数だった場合、その関数にargsを適用した結果を返す

($ args) ff 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らしくなってきた。

四則演算ができるところまで実装されたリリース

github.com

srcapp/Main.hsにコードが記述されている。

次回

if構文をサポートしたいので、次回はとりあえずブール値を表すデータ型の追加。