末尾呼出し その3

末尾呼出しというよりはHaskellでの実装の話。
関数呼出しが末尾呼出しかどうかを知るために継続を比較する必要があるのだが,わたしの実装では継続は関数であり,そしてHaskellでは関数そのものの比較はできない(はず。たぶん)。

type Cont = [Val] -> Env -> Store -> IO Ans

継続の型は上記のようになっている。

  • [Val]には評価結果を格納する。実行時スタックみたいなもん。たぶん。
  • Envは環境。変数と記憶場所の対応。
  • Storeは記憶域。記憶場所と値の対応。
  • Ansは評価結果。値と評価により変更された環境と記憶域の対。

環境まで渡すのはdefineをほかの式と同等に扱うため。Schemeではdefineが現れる場所は限定されているため,継続に環境を渡さなくても平気なように実装できると思う。というのもdefineがなければ環境は式をまたがって存在しないので*1
で,継続の比較だが,ようは関数そのものではなく,関数へのポインタを比較すればよい。HaskellではCのようにポインタは扱えないが,IORefを使えば似たようなことはできる。

IORefを使ううえでの注意点は,newIORefはIOアクションを返すということ。型を見れば明らかなのだが次のようなミスはおかしやすい。わたしだけかもしれないが。

Prelude Data.IORef> let a = newIORef 1; b = a:[] in do {x <- a; y <- head b; print (x == y)}
False

aとリストbの先頭は同じものだからTrueが返ってくると思いきや,Falseだった。aとリストbの先頭は同じアクションに束縛されているが,"x <- a"と"y <- head b"のそれぞれで実行されたアクションの結果が異なるということなのだと思う。これに気づくのに結構時間かかったり。
期待通りに動作させるためには次のように書けばよい。

Prelude Data.IORef> let a = newIORef 1 in do {a' <- a; let b = a':[] in print (a' == head b)}
True

アクションが実行されるのは "a' <- a" だけであり,リストbに格納されるのは IO (IORef t) ではなく IORef t であり,a'と同じIORefであるので当然比較結果はTrueになる。
これを使えば継続の比較ができる。が,コードが汚くなるし,Haskellっぽくなくなるし,なにより導入する利点がないので,今後の開発はIORefを使わない版で行う予定。

参考。もともとのコードは↓(set!式を評価する部分)

evalExpr :: Expr -> Cont -> Cont
evalExpr (SetExpr ide e) p v r s
    = evalExpr e q v r s
      where
      q vs@(v:_) r s = p vs r (store s (lookup r ide) v)

この部分を,継続をIORefで比較できるようにしたら下記のようになった。

evalExpr :: Expr -> (IORef Cont) -> Cont
evalExpr (SetExpr ide e) p v r s
    = do {k <- newIORef q; evalExpr e k v r s}
      where
      q vs@(v:_) r s = do {k <- readIORef p; k vs r (store s (lookup r ide) v)}

*1:ちなみに今はdefineを特別扱いした実装になっており,トップレベルでしかdefineは正しく動かない・・・