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) = (((a
opb)
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
構文をサポートしたいので、次回はとりあえずブール値を表すデータ型の追加。