M.Hiroi's Home Page

Functional Programming

お気楽 Haskell プログラミング入門

[ PrevPage | Haskell | NextPage ]

Haskell で作る micro Scheme (2)

micro Scheme の続きです。今回は S 式を評価する処理を作りましょう。

●関数の定義

最初に、関数の定義をデータ型 SExpr に追加します。次のリストを見てください。

リスト : S 式の定義

import qualified Data.Map as M

data SExpr = INT  Integer
           | REAL Double
           | SYM  String
           | STR  String
           | CELL SExpr SExpr
           | NIL
           | PRIM (SExpr -> Scm SExpr)
           | SYNT (GEnv -> LEnv -> SExpr -> Scm (SExpr, GEnv))
           | CLOS SExpr LEnv

-- 評価器の定義
type Scm a = Either String a

-- エラーの送出
scmError :: String -> Scm a
scmError s = Left s

-- 局所変数の環境
type LEnv = [(String, SExpr)]

-- 大域変数の環境
type GEnv = M.Map String SExpr

PRIM はプリミティブ (primitive) の略で、eq?, car, cdr, cons など基本的な関数を表します。引数は評価して、その結果をコンスセル (CELL) に格納して渡します。SYNT はシンタックス形式 (syntax) の略で、quote, if, define など基本的なシンタックス形式を表します。引数は評価せずにそのままシンタックス形式に渡します。このとき、いっしょに環境 (environment) も渡します。CLOS はクロージャ (closure) を表します。Scheme の場合、ラムダ式を評価するとクロージャが生成され、その時点で定義されている局所変数の環境 LEnv がクロージャに保存されます。

LEnv は連想リスト [(String, SExpr)] で表します。今回の micro Scheme は変数の値を書き換えるシンタックス形式 set! を実装しないので、単純な連想リストでも大丈夫です。グローバルな環境 (大域変数) はモジュール Data.Map の Map を使って表します。Scm は S 式を評価する関数の返り値のデータ型を表します。エラーを表すため Either モナドを使います。scmError はエラーを送出する関数で、引数 s を Left に包んで返すだけです。

●S 式の評価

micro Scheme インタプリタの主役は関数 eval と apply です。eval は S 式と環境を受け取り、渡された環境の下で S 式を評価します。eval の仕事は簡単です。S 式が自己評価フォームであれば、それをそのまま返します。シンボル (変数) であれば環境からその値を求めて返します。リストの場合はちょっと複雑です。先頭の要素を評価して、それが関数値であればそれを呼び出します。プリミティブとクロージャの処理は apply で行います。

プログラムは次のようになります。

リスト : S 式の評価

eval :: GEnv -> LEnv -> SExpr -> Scm (SExpr, GEnv)
eval genv lenv NIL        = return (NIL, genv)
eval genv lenv v@(INT _)  = return (v, genv)
eval genv lenv v@(REAL _) = return (v, genv)
eval genv lenv v@(STR _)  = return (v, genv)
eval genv lenv (SYM name) =
  case lookup name lenv of
    Nothing -> case M.lookup name genv of
                 Nothing -> scmError $ "unbound variable: " ++ name
                 Just v  -> return (v, genv)
    Just v -> return (v, genv)
eval genv lenv (CELL func args) = do
  (v, genv1) <- eval genv lenv func
  case v of
    SYNT f -> f genv1 lenv args
    _      -> do (vs, genv2) <- evalArguments genv1 lenv args
                 apply genv2 lenv v vs

eval の引数 genv, lenv が環境で、第 2 引数が評価する S 式です。返り値は評価結果と大域変数の環境です。S 式が NIL, INT, REAL, STR の場合は自己評価フォームなので、S 式をそのまま返します。S 式がシンボルの場合は環境 lenv から局所変数を探します。局所変数が見つからない場合は環境 genv から大域変数を探します。それでも変数が見つからない場合はエラーを返します。

S 式がリストの場合は、その先頭要素 func を eval で評価して値を v にセットします。v が SYNT f の場合は、シンタックス形式 f を呼び出します。このとき、引数として args と genv1, lenv をそのまま渡します。そうでなければ、引数 args を関数 evalArguments で評価して、その結果を apply に渡して関数 v を呼び出します。

引数を評価する関数 evalArguments は簡単です。次のリストを見てください。

リスト : 引数の評価

evalArguments :: GEnv -> LEnv -> SExpr -> Scm (SExpr, GEnv)
evalArguments genv lenv NIL = return (NIL, genv)
evalArguments genv lenv (CELL expr rest) = do
  (v, genv1) <- eval genv lenv expr
  (vs, genv2) <- evalArguments genv1 lenv rest
  return (CELL v vs, genv2)
evalArguments _ _ _ = scmError "invalid function form"

S 式が NIL の場合は NIL を返します。これが再帰呼び出しの停止条件になります。S 式が CELL の場合、CAR の要素 expr を eval で評価します。次に、残りの S 式を evalArguments で評価します。そして、2 つの返り値を CELL に格納して返します。これで引数を評価して、その結果をリストに格納して返すことができます。S 式が NIL, CELL 以外の値はエラーを返します。

●関数適用

次は関数 apply を作りましょう。

リスト : 関数適用

apply :: GEnv -> LEnv -> SExpr -> SExpr -> Scm (SExpr, GEnv)
apply genv lenv func actuals =
  case func of
    PRIM f -> do v <- f actuals
                 return (v, genv)
    CLOS (CELL parms body) lenv0 -> do
      lenv1 <- makeBindings lenv0 parms actuals
      evalBody genv lenv1 body
    _ -> scmError $ "Not Function: " ++ show func

引数 func が PRIM の場合、実引数リスト actuals にプリミティブ f を適用して値 v を求め、モナドに包んで返します。func が CLOS の場合はちょっと複雑です。actuals と仮引数リスト parms と環境 lenv0 を makeBindings に渡して新しい環境 lenv1 を生成します。クロージャが保持している環境 lenv0 も新しい環境 lenv1 に追加されることに注意してください。

ラムダ式の本体は複数の S 式を格納することができます。これらの S 式の評価は関数 evalBody で行います。evalBody は複数の S 式を順番に eval で評価し、最後に評価した S 式の結果を返します。グローバルの環境は genv と同じですが、ローカルな環境を新しく生成された lenv1 に切り替えます。この環境でラムダ式の本体が評価されます。

次は変数束縛を生成する関数 makeBindings を作ります。

リスト : 変数束縛

makeBindings :: LEnv -> SExpr -> SExpr -> Scm LEnv
makeBindings lenv NIL        _    = return lenv
makeBindings lenv (SYM name) rest = return ((name, rest):lenv)
makeBindings lenv (CELL (SYM name) parms) (CELL v args) = do
  lenv' <- makeBindings lenv parms args
  return ((name, v):lenv')
makeBindings lenv _ NIL = scmError errNEA
makeBindings lenv _ _   = scmError "invalid arguments form"

仮引数リストが NIL の場合はクロージャの環境 lenv をそのまま返します。残っている実引数は捨て去ることに注意してください。仮引数リストが SYM name の場合、仮引数リストはドットリストまたはシンボルだけの場合です。残っている実引数リスト rest と name をタプルに格納し、それを lenv に追加します。どちらも CELL の場合は、name と v をタプルに格納し、それを makeBindings の返り値に追加します。それ以外の場合はエラーを返します。

次はラムダ式の本体を評価する関数 evalBody を作ります。

リスト : 本体の評価

evalBody :: GEnv -> LEnv -> SExpr -> Scm (SExpr, GEnv)
evalBody genv lenv (CELL expr NIL) = eval genv lenv expr
evalBody genv lenv (CELL expr rest) = do
  (_, genv1) <- eval genv lenv expr
  evalBody genv1 lenv rest
evalBody _ _ _ = scmError "invalid body form"

evalBody は簡単で、先頭から順番にコンスセルの要素 expr を取り出し、それを eval で評価するだけです。途中の返り値は捨てて、最後の S 式の値だけを返します。

●等値の判定

次は等値を判定する述語 eq? と equal? を作ります。

リスト : 等値の判定

-- 真偽値
true = SYM "true"
false = SYM "false"

-- 等値の定義
instance Eq SExpr where
  INT x  == INT y  = x == y
  REAL x == REAL y = x == y
  SYM x  == SYM y  = x == y
  STR x  == STR y  = x == y
  NIL    == NIL    = True
  _      == _      = False

eq' :: SExpr -> Scm SExpr
eq' (CELL x (CELL y _)) =
  if x == y then return true else return false
eq' _ = scmError $ "eq : " ++ errNEA

equal' :: SExpr -> Scm SExpr
equal' (CELL x (CELL y _)) =
  if iter x y then return true else return false
  where iter (CELL a b) (CELL c d) = iter a c && iter b d
        iter x y = x == y
equal' _ = scmError $ "equal : " ++ errNEA

真偽値はシンボル true と false で表します。true と false の値は自分自身になるよう初期化します。つまり、自己評価フォームと同じ働きになります。次に、SExpr を型クラス Eq のインスタンスに設定します。INT, REAL, SYM の場合は、演算子 == で比較します。NIL と NIL は True を返します。それ以外の場合は False を返します。

Scheme の述語 eq? (Lisp の eq) はデータの同一性を判定します。つまり、データが配置されたメモリのアドレスが等しい場合に真を返します。この仕様を Haskell で実現するのはちょっと難しいので、データの値を使ってチェックすることにします。これは Scheme の述語 eqv? (Lisp の eql) と同じ動作になります。

関数 eq' は実引数リストから二つの要素 x, y を取り出して演算子 == でチェックするだけです。関数 equal' はリストの要素が eq' を満たせば真を返します。実際の判定は局所関数 iter で行います。どちらの引数も CELL の場合は、CAR 部と CDR 部を iter で比較します。そうでなければ、引数を演算子 == で比較します。

●シンタックス形式

次はシンタックス形式を処理する関数を作りましょう。

リスト : シンタックス形式

-- quote
evalQuote :: GEnv -> LEnv -> SExpr -> Scm (SExpr, GEnv)
evalQuote genv lenv (CELL expr _) = return (expr, genv)
evalQuote _ _ _ = scmError "invalid quote form"

-- define
evalDef :: GEnv -> LEnv -> SExpr -> Scm (SExpr, GEnv)
evalDef genv lenv (CELL sym@(SYM name) (CELL expr NIL)) = do
  (v, genv1) <- eval genv lenv expr
  return (sym, M.insert name v genv1)
evalDef _ _ _ = scmError "invalid define form"

-- if
evalIf :: GEnv -> LEnv -> SExpr -> Scm (SExpr, GEnv)
evalIf genv lenv (CELL pred (CELL thenForm rest)) = do
  (v, genv1) <- eval genv lenv pred
  if v /= false
  then eval genv1 lenv thenForm
  else case rest of
         CELL elseForm _ -> eval genv1 lenv elseForm
         _               -> return (false, genv1)
evalIf _ _ _ = scmError $ "if : " ++ errNEA

-- lambda
evalLambda :: GEnv -> LEnv -> SExpr -> Scm (SExpr, GEnv)
evalLambda genv lenv expr = return (CLOS expr lenv, genv)

quote の処理は関数 evalQuote で行います。引数にはリスト (expr) が渡されるので、リストの先頭要素 expr を返すだけです。define の処理は関数 evalDefine で行います。define は変数 name と値 expr を大域変数に追加します。値 expr を eval で評価して、M.insert で Map に追加します。このとき新しい Map が生成されるので、その Map を返すことに注意してください。

if の処理は関数 evalIf で行います。最初に述語 pred を eval で評価し、それが false でなければ then 節を評価して返します。そうでなければ、else 節を有無をチェックして、else 節があればそれを評価します。else 節が無い場合は false を返します。ラムダ式の処理は関数 evalLambda で行います。これはクロージャを生成する処理です。ローカルな環境 lenv と引数の S 式 expr を CLOS に格納して返します。

●リストの操作

次はリストを操作する関数 (primitive) を作りましょう。

リスト : リスト操作の基本関数

car :: SExpr -> Scm SExpr
car NIL = scmError $ "car : " ++ errNEA
car (CELL (CELL a _) _) = return a
car _                   = scmError $ "car : " ++ errCELL

cdr :: SExpr -> Scm SExpr
cdr NIL = scmError $ "cdr : " ++ errNEA
cdr (CELL (CELL _ d) _) = return d
cdr _                   = scmError $ "cdr : " ++ errCELL

cons :: SExpr -> Scm SExpr
cons (CELL a (CELL b _)) = return (CELL a b)
cons _                   = scmError $ "cons : " ++ errNEA

pair :: SExpr -> Scm SExpr
pair NIL                 = scmError $ "pair? : " ++ errNEA
pair (CELL (CELL _ _) _) = return true
pair (CELL _ _)          = return false

PRIM の場合、引数はコンスセルに格納されています。car は先頭要素がコンスセルであれば CAR 部に格納されているデータを返します。cdr は CDR 部に格納されているデータを返します。引数がコンスセルでない場合はエラーを返します。cons は第 1 引数 a と第 2 引数 b を取り出して、CELL に格納して返します。pair は第 1 引数が CELL ならば true を返します。そうでなければ false を返します。どの関数も引数の個数が足りない場合はエラーを返します。引数の数が多い場合は無視することとします。

●REPL の作成

最後に REPL (read - eval - print - loop) を作ります。

リスト : REPL (read - eval - print - loop)

initGEnv :: GEnv
initGEnv = M.fromList [("true",   true),
                       ("false",  false),
                       ("quote",  SYNT evalQuote),
                       ("define", SYNT evalDef),
                       ("lambda", SYNT evalLambda),
                       ("if",     SYNT evalIf),
                       ("eq?",    PRIM eq'),
                       ("equal?", PRIM equal'),
                       ("pair?",  PRIM pair),
                       ("car",    PRIM car),
                       ("cdr",    PRIM cdr),
                       ("cons",   PRIM cons)]

-- read-eval-print-loop
repl :: GEnv -> LEnv -> String -> IO ()
repl genv lenv xs = do
  putStr "Scm> "
  hFlush stdout
  case readSExpr xs of
    Left  (ParseErr xs' mes) -> do putStrLn mes
                                   repl genv lenv $ dropWhile (/= '\n') xs'
    Right (expr, xs') -> do case eval genv lenv expr of
                              Left mes -> do putStrLn mes
                                             repl genv lenv xs'
                              Right (v, genv1) -> do print v
                                                     repl genv1 lenv xs'

main :: IO ()
main = do
  xs <- hGetContents stdin
  repl initGEnv [] xs

repl はプロンプト Scm> を出力してから readSExpr を呼び出して S 式を読み込みます。エラーが返ってきた場合はエラーメッセージを出力し、改行までの入力を読み捨ててから repl を再帰呼び出しします。S 式 expr を読み込んだら、eval を呼び出して expr を評価します。エラーの場合はエラーメッセージを出力して repl を再帰呼び出しします。正常に評価できた場合は、その値 v を表示して repl を再帰呼び出しします。このとき、大域変数の環境は eval が返した genv1 を渡すことに注意してください。

main は hGetContents で標準入力から文字列を読み込み、それを repl に渡します。大域変数の環境は initGEnv で局所関数の環境は空リストになります。

●簡単な実行例

それでは実行してみましょう。

Scm> ()
()
Scm> 1
1
Scm> 1.2345
1.2345
Scm> "hello, world"
"hello, world"
Scm> 'a
a
Scm> (quote a)
a
Scm> '(1 2 3 4 5)
(1 2 3 4 5)
Scm> (cons 1 ())
(1)
Scm> (cons 1 2)
(1 . 2)
Scm> (car '(a b c d))
a
Scm> (cdr '(a b c d))
(b c d)
Scm> (cons 'a '(b c d))
(a b c d)
Scm> (eq? 1 1)
true
Scm> (eq? 1 2)
false
Scm> (eq? 1.234 1.234)
true
Scm> (eq? 1.234 1.2)
false
Scm> (eq? 'a 'a)
true
Scm> (eq? 'a 'b)
false
Scm> (eq? "abc" "abc")
true
Scm> (eq? "abc" "ABC")
false
Scm> (equal? '(1 2 3 4) '(1 2 3 4))
true
Scm> (equal? '(1 2 3 4) '(1 2 3 4.0))
false
Scm> (equal? '((1 2) (3 4)) '((1 2) (3 4)))
true
Scm> (equal? '((1 2) (3 4)) '((1 2) (3 . 4)))
false
Scm> (pair? '(a b c))
true
Scm> (pair? 'a)
false

quote, if, car, cdr, cons, eq?, equal?, pair? は正常に動作していますね。次は lambda と define を試してみます。

Scm> (define a 'b)
a
Scm> a
b
Scm> (lambda (x) x)
<closure>
Scm> ((lambda (x) x) 1)
1
Scm> ((lambda (x) x) 'a)
a
Scm> (define list (lambda x x))
list
Scm> (list 1 2 3 4 5)
(1 2 3 4 5)
Scm> (list 'a 'b 'c 'd 'e)
(a b c d e)

define で変数 a を定義します。シンボル a を入力すると、その値を求めることができます。lambda はクロージャを生成します。リストの先頭要素にラムダ式を指定すると、それを呼び出すことができます。そして、define と lambda を使って関数を定義することができます。

●レキシカルスコープとクロージャの動作

次は、レキシカルスコープとクロージャが正常に動作するか試してみましょう。

Scm> (define x 'a)
x
Scm> x
a
Scm> (define foo (lambda () x))
foo
Scm> (foo)
a
Scm> (define bar (lambda (x) (foo)))
bar
Scm> (bar 'b)
a

まず大域変数 x を a に初期化します。次に、関数 foo を定義します。foo の引数はないので、x は大域変数を参照します。したがって、foo を評価すると返り値は a になります。次に、関数 bar から foo を呼び出します。bar の仮引数は x ですが、(bar 'b) を評価すると a が返ってきます。確かにレキシカルスコープになっています。

今度はクロージャの動作を確かめます。

Scm> (define baz (lambda (x) (lambda (y) (cons x y))))
baz
Scm> (define baz-a (baz 'a))
baz-a
Scm> (baz-a 'b)
(a . b)
Scm> (baz-a 'c)
(a . c)

関数 baz はクロージャを生成して返します。このとき、baz の引数 x の値がクロージャに保存されます。(baz 'a) の返り値を baz-a にセットすると、baz-a は a と baz-a の引数を組にしたものを返す関数となります。したがって、(baz-a 'b) は (a . b) を、(baz-a 'c) は (a . c) を返します。クロージャも正常に動作していますね。

●再帰定義とリスト操作

define で定義する関数は再帰呼び出しが可能です。簡単なリスト操作関数を再帰定義で作ってみました。プログラムリストと実行結果を示します。

リスト : リストの結合

(define append
  (lambda (xs ys)
    (if (pair? xs)
        (cons (car xs) (append (cdr xs) ys))
       ys)))
Scm> (define append (lambda (xs ys) (if (pair? xs) (cons (car xs) (append (cdr xs) ys)) ys)))
append
Scm> (append '(a b c d) '(e f g h))
(a b c d e f g h)
Scm> (append '((a b) (c d)) '((e f) (g h)))
((a b) (c d) (e f) (g h))
リスト : リストの反転

(define reverse
  (lambda (xs)
    (if (pair? xs)
        (append (reverse (cdr xs)) (list (car xs)))
      ())))
Scm> (define reverse (lambda (xs) (if (pair? xs) (append (reverse (cdr xs)) (list (car xs))) ())))
reverse
Scm> (reverse '(a b c d e f))
(f e d c b a)
Scm> (reverse '((a b) (c d) (e f)))
((e f) (c d) (a b))

もちろん、高階関数も定義することができます。

リスト : マッピング

(define map
  (lambda (f xs)
    (if (pair? xs)
        (cons (f (car xs)) (map f (cdr xs)))
      ())))
Scm> (define map (lambda (f xs) (if (pair? xs) (cons (f (car xs)) (map f (cdr xs))) ())))
map
Scm> (map (lambda (x) (cons x x)) '(1 2 3 4 5))
((1 . 1) (2 . 2) (3 . 3) (4 . 4) (5 . 5))
Scm> (map car '((a 1) (b 2) (c 3) (d 4) (e 5)))
(a b c d e)
Scm> (map cdr '((a 1) (b 2) (c 3) (d 4) (e 5)))
((1) (2) (3) (4) (5))
リスト : フィルター

(define filter
  (lambda (f xs)
    (if (pair? xs)
        (if (f (car xs))
            (cons (car xs) (filter f (cdr xs)))
          (filter f (cdr xs)))
      ())))
Scm> (define filter (lambda (f xs) (if (pair? xs) (if (f (car xs)) (cons (car xs) (filter f (cdr xs))) 
(filter f (cdr xs))) ())))
filter
Scm> (filter (lambda (x) (eq? x 'a)) '(a b c a b c a b c))
(a a a)
リスト : 畳み込み

(define fold-left
  (lambda (f a xs)
    (if (pair? xs)
        (fold-left f (f a (car xs)) (cdr xs))
      a)))

(define fold-right
  (lambda (f a xs)
    (if (pair? xs)
        (f (car xs) (fold-rightf a (cdr xs)))
      a)))
Scm> (define fold-left (lambda (f a xs) (if (pair? xs) (fold-left f (f a (car xs)) (cdr xs)) a)))
fold-left
Scm> (fold-left (lambda (a x) (cons x a)) () '(1 2 3 4 5))
(5 4 3 2 1)

Scm> (define fold-right (lambda (f a xs) (if (pair? xs) (f (car xs) (fold-right f a (cdr xs))) a)))
fold-right
Scm> (fold-right cons () '(1 2 3 4 5))
(1 2 3 4 5)

今回はここまでです。次回は変数の値を書き換えるシンタックス形式 set! と数の算術演算、比較演算を追加します。

●参考文献, URL

  1. 黒川利明, 『LISP 入門』, 培風館, 1982
  2. Patrick Henry Winston, Berthold Klaus Paul Horn, 『LISP 原書第 3 版 (1)』, 培風館, 1992
    18. Lisp で書く Lisp
  3. R. Kent Dybvig (著), 村上雅章 (訳), 『プログラミング言語 SCHEME』, 株式会社ピアソン・エデュケーション, 2000
    9.2 Scheme のメタ循環インタプリタ
  4. Ravi Sethi (著), 神林靖 (訳), 『プログラミング言語の概念と構造』, アジソンウェスレイ, 1995
    第 11 章 定義インタプリタ
  5. 小西弘一, 清水剛, 『CプログラムブックⅢ』, アスキー, 1986
  6. Harold Abelson, Gerald Jay Sussman, Julie Sussman, "Structure and Interpretation of Computer Programs",
    4.1 The Metacircular Evaluator
  7. 稲葉雅幸, ソフトウェア特論, Scheme インタプリタ

●プログラムリスト

--
-- mscheme1.hs : microScheme インタプリタ
--
--               Copyright (C) 2013-2021 Makoto Hiroi
--
import Data.Char
import qualified Data.Map as M
import System.IO

-- S 式の定義
data SExpr = INT  Integer
           | REAL Double
           | SYM  String
           | STR  String
           | CELL SExpr SExpr
           | NIL
           | PRIM (SExpr -> Scm SExpr)
           | SYNT (GEnv -> LEnv -> SExpr -> Scm (SExpr, GEnv))
           | CLOS SExpr LEnv

-- 等値の定義
instance Eq SExpr where
  INT x  == INT y  = x == y
  REAL x == REAL y = x == y
  SYM x  == SYM y  = x == y
  STR x  == STR y  = x == y
  NIL    == NIL    = True
  _      == _      = False

-- パーサエラーの定義
data ParseErr = ParseErr String String deriving Show

-- パーサの定義
type Parser a = Either ParseErr a

-- エラーの送出
parseError :: String -> String -> Parser a
parseError x s = Left (ParseErr x s)

-- 評価器の定義
type Scm a = Either String a

-- エラーの送出
scmError :: String -> Scm a
scmError s = Left s

-- ローカル環境の定義
type LEnv = [(String, SExpr)]

-- グローバルな環境
type GEnv = M.Map String SExpr

-- 真偽値
true  = SYM "true"
false = SYM "false"

-- Primitive の定義
errNEA  = "Not enough arguments"
errCELL = "Illegal argument, List required"
errINT  = "Illegal argument, Integer required"
errNUM  = "Illegal argument, Number required"

-- リスト操作
car :: SExpr -> Scm SExpr
car NIL = scmError $ "car : " ++ errNEA
car (CELL (CELL a _) _) = return a
car _                   = scmError $ "car : " ++ errCELL

cdr :: SExpr -> Scm SExpr
cdr NIL = scmError $ "cdr : " ++ errNEA
cdr (CELL (CELL _ d) _) = return d
cdr _                   = scmError $ "cdr : " ++ errCELL

cons :: SExpr -> Scm SExpr
cons (CELL a (CELL b _)) = return (CELL a b)
cons _                   = scmError $ "cons : " ++ errNEA

-- 述語
eq' :: SExpr -> Scm SExpr
eq' (CELL x (CELL y _)) =
  if x == y then return true else return false
eq' _ = scmError $ "eq : " ++ errNEA

equal' :: SExpr -> Scm SExpr
equal' (CELL x (CELL y _)) =
  if iter x y then return true else return false
  where iter (CELL a b) (CELL c d) = iter a c && iter b d
        iter x y = x == y
equal' _ = scmError $ "equal : " ++ errNEA

pair :: SExpr -> Scm SExpr
pair NIL                 = scmError $ "pair? : " ++ errNEA
pair (CELL (CELL _ _) _) = return true
pair (CELL _ _)          = return false

--
-- S 式の表示
--
showCell :: SExpr -> String
showCell (CELL a d) =
  show a ++ case d of
              NIL      -> ""
              PRIM _   -> "<primitive>"
              CLOS _ _ -> "<closure>"
              SYNT _   -> "<syntax>"
              INT x    -> " . " ++ show x
              REAL x   -> " . " ++ show x
              SYM x    -> " . " ++ x
              STR x    -> " . " ++ show x
              _        -> " " ++ showCell d
showCell xs = show xs

instance Show SExpr where
  show (INT x)    = show x
  show (REAL x)   = show x
  show (SYM x)    = x
  show (STR x)    = show x
  show NIL        = "()"
  show (SYNT _)   = "<syntax>"
  show (PRIM _)   = "<primitive>"
  show (CLOS _ _) = "<closure>"
  show xs         = "(" ++ showCell xs ++ ")"

--
-- S 式の読み込み
--

isAlpha' :: Char -> Bool
isAlpha' x = elem x "!$%&*+-/:<=>?@^_~"

isIdent0 :: Char -> Bool
isIdent0 x = isAlpha x || isAlpha' x

isIdent1 :: Char -> Bool
isIdent1 x = isAlphaNum x || isAlpha' x

isREAL :: Char -> Bool
isREAL x = elem x ".eE"

quote = SYM "quote"

isNUM :: String -> Bool
isNUM (x:_) = isDigit x
isNUM _     = False

getNumber :: String -> Parser (SExpr, String)
getNumber xs =
  let (s, ys) = span isDigit xs
  in if not (null ys) && isREAL (head ys)
     then case reads xs of
            [] -> parseError "" ""  -- ありえないエラー
            [(y', ys')] -> return (REAL y', ys')
     else return (INT (read s), ys)

readSExpr :: String -> Parser (SExpr, String)
readSExpr [] = parseError "" "EOF"
readSExpr (x:xs)
  | isSpace x  = readSExpr xs
  | isDigit x  = getNumber (x:xs)
  | isIdent0 x = if x == '+' && isNUM xs
                 then getNumber xs
                 else if x == '-' && isNUM xs
                 then do (y, ys) <- getNumber xs
                         case y of
                           INT x  -> return (INT  (- x), ys)
                           REAL x -> return (REAL (- x), ys)
                 else let (name, ys) = span isIdent1 (x:xs)
                      in return (SYM name, ys)
  | otherwise  =
      case x of
        '('  -> readCell 0 xs
        ';'  -> readSExpr $ dropWhile (/= '\n') xs
        '"'  -> case reads (x:xs) of
                  [] -> parseError "" ""
                  [(y, ys)] -> y `seq` ys `seq` return (STR y, ys)
        '\'' -> readSExpr xs >>= \(e, ys) -> e `seq` ys `seq` return (CELL quote (CELL e NIL), ys)
        _    -> parseError xs ("unexpected token: " ++ show x)

readCell :: Int -> String -> Parser (SExpr, String)
readCell _ [] = parseError "" "EOF"
readCell n (x:xs)
  | isSpace x = readCell n xs
  | otherwise =
      case x of
        ')' -> xs `seq` return (NIL, xs)
        '.' -> if n == 0
               then parseError xs "invalid dotted list"
               else do (e, ys) <- readSExpr xs
                       case dropWhile isSpace ys of
                         ')':zs -> return (e, zs)
                         _      -> parseError xs "invalid dotted list"
        '(' -> do (a, ys) <- readCell 0 xs
                  (d, zs) <- readCell 1 ys
                  return (CELL a d, zs)
        _   -> do (a, ys) <- readSExpr (x:xs)
                  (d, zs) <- readCell 1 ys
                  return (CELL a d, zs)

--
-- S 式の評価
--
eval :: GEnv -> LEnv -> SExpr -> Scm (SExpr, GEnv)
eval genv lenv NIL        = return (NIL, genv)
eval genv lenv v@(INT _)  = return (v, genv)
eval genv lenv v@(REAL _) = return (v, genv)
eval genv lenv v@(STR _)  = return (v, genv)
eval genv lenv (SYM name) =
  case lookup name lenv of
    Nothing -> case M.lookup name genv of
                 Nothing -> scmError $ "unbound variable: " ++ name
                 Just v  -> return (v, genv)
    Just v -> return (v, genv)
eval genv lenv (CELL func args) = do
  (v, genv1) <- eval genv lenv func
  case v of
    SYNT f -> f genv1 lenv args
    _      -> do (vs, genv2) <- evalArguments genv1 lenv args
                 apply genv2 lenv v vs

-- 引数の評価
evalArguments :: GEnv -> LEnv -> SExpr -> Scm (SExpr, GEnv)
evalArguments genv lenv NIL = return (NIL, genv)
evalArguments genv lenv (CELL expr rest) = do
  (v, genv1) <- eval genv lenv expr
  (vs, genv2) <- evalArguments genv1 lenv rest
  return (CELL v vs, genv2)
evalArguments _ _ _ = scmError "invalid function form"

-- 変数束縛
makeBindings :: LEnv -> SExpr -> SExpr -> Scm LEnv
makeBindings lenv NIL        _    = return lenv
makeBindings lenv (SYM name) rest = return ((name, rest):lenv)
makeBindings lenv (CELL (SYM name) parms) (CELL v args) = do
  lenv' <- makeBindings lenv parms args
  return ((name, v):lenv')
makeBindings lenv _ NIL = scmError errNEA
makeBindings lenv _ _   = scmError "invalid arguments form"

-- 関数適用
apply :: GEnv -> LEnv -> SExpr -> SExpr -> Scm (SExpr, GEnv)
apply genv lenv func actuals =
  case func of
    PRIM f -> do v <- f actuals
                 return (v, genv)
    CLOS (CELL parms body) lenv0 -> do
      lenv1 <- makeBindings lenv0 parms actuals
      evalBody genv lenv1 body
    _ -> scmError $ "Not Function: " ++ show func

-- 本体の評価
evalBody :: GEnv -> LEnv -> SExpr -> Scm (SExpr, GEnv)
evalBody genv lenv (CELL expr NIL) = eval genv lenv expr
evalBody genv lenv (CELL expr rest) = do
  (_, genv1) <- eval genv lenv expr
  evalBody genv1 lenv rest
evalBody _ _ _ = scmError "invalid body form"

--
-- シンタックス形式
--

-- quote
evalQuote :: GEnv -> LEnv -> SExpr -> Scm (SExpr, GEnv)
evalQuote genv lenv (CELL expr _) = return (expr, genv)
evalQuote _ _ _ = scmError "invalid quote form"

-- define
evalDef :: GEnv -> LEnv -> SExpr -> Scm (SExpr, GEnv)
evalDef genv lenv (CELL sym@(SYM name) (CELL expr NIL)) = do
  (v, genv1) <- eval genv lenv expr
  return (sym, M.insert name v genv1)
evalDef _ _ _ = scmError "invalid define form"

-- if
evalIf :: GEnv -> LEnv -> SExpr -> Scm (SExpr, GEnv)
evalIf genv lenv (CELL pred (CELL thenForm rest)) = do
  (v, genv1) <- eval genv lenv pred
  if v /= false
  then eval genv1 lenv thenForm
  else case rest of
         CELL elseForm _ -> eval genv1 lenv elseForm
         _               -> return (false, genv1)
evalIf _ _ _ = scmError $ "if : " ++ errNEA

-- lambda
evalLambda :: GEnv -> LEnv -> SExpr -> Scm (SExpr, GEnv)
evalLambda genv lenv expr = return (CLOS expr lenv, genv)

--
-- 大域変数の初期化
--
initGEnv :: GEnv
initGEnv = M.fromList [("true",   true),
                       ("false",  false),
                       ("quote",  SYNT evalQuote),
                       ("define", SYNT evalDef),
                       ("lambda", SYNT evalLambda),
                       ("if",     SYNT evalIf),
                       ("eq?",    PRIM eq'),
                       ("equal?", PRIM equal'),
                       ("pair?",  PRIM pair),
                       ("car",    PRIM car),
                       ("cdr",    PRIM cdr),
                       ("cons",   PRIM cons)]

--
-- read-eval-print-loop
--
repl :: GEnv -> LEnv -> String -> IO ()
repl genv lenv xs = do
  putStr "Scm> "
  hFlush stdout
  case readSExpr xs of
    Left  (ParseErr xs' mes) -> do putStrLn mes
                                   repl genv lenv $ dropWhile (/= '\n') xs'
    Right (expr, xs') -> do case eval genv lenv expr of
                              Left mes -> do putStrLn mes
                                             repl genv lenv xs'
                              Right (v, genv1) -> do print v
                                                     repl genv1 lenv xs'

main :: IO ()
main = do
  xs <- hGetContents stdin
  repl initGEnv [] xs

初版 2013 年 8 月 11 日
改訂 2021 年 8 月 1 日

Copyright (C) 2013-2021 Makoto Hiroi
All rights reserved.

[ PrevPage | Haskell | NextPage ]