multi-promptな限定継続 in Haskell (失敗編&成功編)
multi-promptな限定継続の方が型がわかりやすい件について。
なんだかshift/resetの型付けはむつかしいなーと思っていた。
限定継続のOCaml実装を見てみたくて、Olegさんのdelimccライブラリとその論文を眺めていた。
こちらはmulti-promptな限定継続といい、複数の限定継続を同時に扱える体系の実装なのだけど、こちらの方が型が分かりやすかった。
理論的基礎はDybvig, Peyton Jones, Sabryの論文で、cc-delcontというHaskell版の実装がHackageにある。この実装も indexed monad ではなく普通のモナドなので型は理解しやすいはず。
これらのライブラリでは、 newPrompt, pushPrompt, withSubCont, pushSubCont の 4つのプリミティブを使う。また扱うデータ型も、「プロンプト(Prompt)」「部分継続(SubCont)」の2種類がある。
mutil-prompt 限定継続に関する私の理解
ざっくりとした説明
- Prompt
- 複数の限定継続を区別するための識別子
- SubCont
- 限定継続そのもの。呼び出すには pushSubCont を使う
- newPrompt
- 新しいpromptを作る
- pushPrompt p
- resetのようなもの。限定継続の開始地点をpでマークする。pushSubCont するとこのマークは消えちゃう。
- withSubCont p f
- shiftのようなもの(ちょっと違うけど)。 pushPrompt pでマークした箇所まで大域脱出しつつ、そこまでの限定継続(SubCont)に f を適用する。
- 大域脱出したら pushPrompt pのマークは消えちゃう。マークがない状態で withSubCont すると実行時エラー。
- pushSubCont k
- 限定継続kを表す関数を返す。
shiftと同じような挙動を得たい場合はpushSubCont k e のすぐ外で もういちど pushPrompt する。すると、呼び出した限定継続の中で次のwithSubContが使われて大域脱出した場合にもそこに戻ってきてくれる。
使ってみた
先の例(mapをひっくり返す)を CC-delim を使って焼き直してみた。
Promptを持って回るためにちょっとややこしくなってしまった。
{-# LANGUAGE NoMonomorphismRestriction #-} import Control.Monad.CC import Control.Monad.Identity (Identity) -- 再帰的な限定継続を保持する型 data Iter a = Iter (SubCont a Identity Int (Iter a), Int) -- 限定継続と,何か整数を保持 | End [Int] -- おわり pam :: Prompt ans (Iter ans) -> [Int] -> CC ans [Int] pam _ [] = return [] pam p0 (x:xs) = do y <- withSubCont p0 (\k -> return $ Iter (k, x)) ys <- pam p0 xs return (y:ys) expr :: CC ans (Prompt ans (Iter ans), Iter ans) expr = do p0 <- newPrompt it <- pushPrompt p0 $ do xs <- pam p0 [1,2,3] return (End xs) return (p0, it) iter :: (Int -> Int) -> (Prompt ans (Iter ans), Iter ans) -> CC ans [Int] iter _ (_,End xs) = return xs iter f (p0,Iter (k,x)) = do it <- pushPrompt p0 (pushSubCont k (return $ f x)) iter f (p0,it) main = mapM_ print [runCC $ iter id =<< expr, runCC $ iter (*10) =<< expr]
Haskellなので promptを渡して回らないといけないのが面倒(implicit parameterを使えばいいけど…)。 また pushPrompt を忘れると実行時エラーになるので、少し扱いに気を遣う。
うまくいかなかった版
こちらはNG. expr と iter を別の runCC に入れてしまった.
{-# LANGUAGE NoMonomorphismRestriction #-} import Control.Monad.CC import Control.Monad.Identity (Identity) -- 再帰的な限定継続を保持する型 data Iter a = Iter (SubCont a Identity Int (Iter a), Int) -- 限定継続と,何か整数を保持 | End [Int] -- おわり pam :: Prompt ans (Iter ans) -> [Int] -> CC ans [Int] pam _ [] = return [] pam p0 (x:xs) = do y <- withSubCont p0 (\k -> return $ Iter (k, x)) ys <- pam p0 xs return (y:ys) expr :: CC ans (Iter ans) expr = do p0 <- newPrompt pushPrompt p0 $ do xs <- pam p0 [1,2,3] return (End xs) iter :: (Int -> Int) -> Iter a -> [Int] iter _ (End xs) = xs iter f (Iter (k,x)) = runCC $ do it <- pushSubCont k (return $ f x); -- (*) ここで 外からもってきた subcont を使ってるのが駄目 return (iter f it)
CC-delcont の継続モナドは CC ans a という型をもち、 runCC :: (forall ans. CC ans a) -> a によって実行できるが、だがちょっと待って欲しい。 いわゆるContモナドと違うのは、 answer type (Cont r a の r) がforall束縛されている点だ。 この束縛おかげで CC ans a から aが取り出せている…ともいえるのだけど、今回のように runCC から帰ってきた 部分継続(限定継続)を 別の runCC の中から呼び出せないのだ! (型変数の動く範囲が runCC で限定されているため, (*) の k の SubCont ans ...) の ans と unify できない)
shift/reset の感覚で 使ったのがまずかった。ちゃんと multi-prompt っぽく複数の promptを使えば 似たようなことはできるはずだ。。。