参考URL
#ソース
https://github.com/0zawa/scheme_in_48_hours/blob/master/evaluation_1.hs
プリミティブ
evalで加減乗除などの計算を行えるようにします。
まずListの先頭がAtomのときにマッチするパターンを
追加します。
eval (List (Atom func : args)) = apply func $ map eval args
apply関数は次のようになります。
apply :: String -> [LispVal] -> LispVal
apply func args = maybe (Bool False) ($ args) $ lookup func primitives
いろいろと初めてみる関数が出てきますが、
まずはprimitivesから見ていきます。
primitives :: [(String, [LispVal] -> LispVal)]
primitives = [("+", numericBinop (+)),
("-", numericBinop (-)),
("*", numericBinop (*)),
("/", numericBinop div),
("mod", numericBinop mod),
("quotient", numericBinop quot),
("remainder", numericBinop rem)]
ここで使われているnumericBinopは以下です。
numericBinop :: (Integer -> Integer -> Integer) -> [LispVal] -> LispVal
numericBinop op params = Number $ foldl1 op $ map unpackNum params
unpackNumは以下のようになります。
unpackNum :: LispVal -> Integer
unpackNum (Number n) = n
unpackNum (String n) = let parsed = reads n in
if null parsed
then 0
else fst $ parsed !! 0
unpackNum (List [n]) = unpackNum n
unpackNum _ = 0
これでこの項目で定義した関数はすべて出揃いました。
ひとつずつゆっくりと調べていきます。
まずunpackNumはLispValをIntegerに変換します。
Numberならその中身をStringならその中身の文字列を
reads関数に適用します。
ここでreads関数は文字列のうち数字に変換できる部分と
変換できない部分に分けて返します。
> reads "123abc" :: [(Int,String)]
[(123,"abc")]
ここではIntを指定していますがDouble型なども指定可能です。
> reads "123.456abc" :: [(Double,String)]
[(123.456,"abc")]
unpackNum関数はIntegerを返すので、reads関数は
Integer型に変換できるものとできないものにわけます。
その結果は要素がひとつのリストになっているので
最初の要素を取り出します。
また要素はタプルになっているので
タプルの最初のものを取り出します。
fst $ parsed !! 0
もし、Integerに変換できなかったり、Number,String以外の
LispValが渡された場合は0を返します。
unpackNum関数の役割がわかったので、
次にnumericBinop関数をみていきます。
numericBinop :: (Integer -> Integer -> Integer) -> [LispVal] -> LispVal
numericBinop op params = Number $ foldl1 op $ map unpackNum params
opはIntegerを2つとりIntegerを返す関数です。
これは+や-、*や/などにあたります。
paramsはLispValのリストです。
foldl1は第一引数の関数を第二引数のリストに順々に適用していきます。
> foldl1 (+) [1,2,3,4]
10
> foldl1 (*) [1,2,3,4]
24
LispValのリストparamsはunpackNumで
すべてIntegerに変換されて渡されます。
つまり、numericBinOp関数は演算子と値のリストが渡され、
その結果を返す関数になります。
結果はNumberコンストラクタでLispVal型にされます。
numericBinopを使っているprimitivesをみてみます。
primitives :: [(String, [LispVal] -> LispVal)]
primitives = [("+", numericBinop (+)),
("-", numericBinop (-)),
("*", numericBinop (*)),
("/", numericBinop div),
("mod", numericBinop mod),
("quotient", numericBinop quot),
("remainder", numericBinop rem)]
numericBinopには演算子しか渡されていないため、
後ほどLispValのリストを渡すようです。
また、各要素はStringとnumericBinOpのタプルになっています。
primitivesを使うapply関数をみていきます。
apply :: String -> [LispVal] -> LispVal
apply func args = maybe (Bool False) ($ args) $ lookup func primitives
まずlookup関数は最初の引数の文字列を持つタプルを、
第二引数のリストから探しだして、そのタプルの後ろの要素を返します.
言葉にするとややこしいので例をみてみます。
> lookup "+" [("+",1),("-",2)]
Just 1
> lookup "-" [("+",1),("-",2)]
Just 2
> lookup "/" [("+",1),("-",2)]
Nothing
結果はMaybe型になります。
apply関数で使われているmaybeは3つの引数をとり、
3番めの評価結果がNothingなら一番目の引数を返し、
評価結果がJustならその中身を2番めの引数に適用します。
これも説明するとわかりにくいので例を見てみます。
> maybe (0) (+100) $ lookup "+" [("+",1),("-",2)]
101
> maybe (0) (+100) $ lookup "-" [("+",1),("-",2)]
102
> maybe (0) (+100) $ lookup "/" [("+",1),("-",2)]
0
Justがの場合はその中身に(+100)が適用された結果が、
Nothingの場合は0が戻っています。
結果としてapply関数は渡された文字列に対応する関数があれば
その関数にLispValのリストを適用した結果を返し、
なければBool Falseを返します。
これで、やっとevalに戻ってこれました。
eval (List (Atom func : args)) = apply func $ map eval args
今まで見てきたものからこの関数は、
funcの文字列に対応する関数があれば
それにargsを評価した後のリストを、
無ければBool Falseを返す処理をしているということがわかります。
つまり(+ 1 1)のようなリストを評価すると2を(実際はNumber 2)、
(* 1 2 3 4)のようなリストを評価すると24(Number 24)
を返す関数になります。
これで四則演算などの計算が行えるようになりました。