拡張可能作用 ― モナド変換子に取って代わるもの 5 - shiatsumat/fp-papers GitHub Wiki
5. MTLを超越する
これまで私たちのフレームワークは MTL の便利な持ち上げのある変種にすぎないように見えたかもしれない。しかし私たちのフレームワークは、これからこのセクションで見るように、MTL を超越する。まずは、MTL が不十分である場合を見せよう。この限界はめったに話されることがないが、実に現実的なものである。作用の交替を示す一般的なプログラミングのパターンは、モナド変換子を使うと、時には複雑かつ非効率で([§5.1](拡張可能作用 ― モナド変換子に取って代わるもの 5#section5-1))、時には表現不可能ですらある([§5.2](拡張可能作用 ― モナド変換子に取って代わるもの 5#section5-2))。私たちのフレームワークはこれらの状況を、効率的、かつ素直に扱う。
5.1 モナド変換子の意味論は柔軟でない
このセクションでは、単独の例外を発生させたりハンドルしたりする単純な例が、驚くべきことに、2つの ErorT
モナド変換子の層を使い、計算全体に余分な実行時オーバーヘッドを課さなければ実装できないということを示す。
他の作用がなければ、例外は動的に最も近いハンドラまでの間にあるすべての計算を中断する。しかし、例外が他の作用とともに使われているとき、状況はもっと微妙になる。[§2](拡張可能作用 ― モナド変換子に取って代わるもの 2#section2) で説明したように、例外と状態の組み合わせに対しては2つの合理的な意味論がある――エラーがハンドルされる間、割り当てが保持されるかどうかである。非決定論的計算があるとき、もっと微妙な状況が生まれる。例外は非決定論的計算のそれぞれの枝 (branch) に限られるのか、例外は非決定論的選択全体のコレクションをも中断しうるのか?私たちは後者の状況を詳しく調べる。
具体的には、私たちは型 m Int
の計算を任意のモナド m
について考え、例外作用を次のように加える――もし既定の計算が5より大きい値を返したら、例外が投げられる。意図された意味論は、例外が m
の計算の残りを中断するというものである。これは非自明であるが可能である。次に、例外を補足し、解析し、通常の制御の流れを再開するか、再び例外を投げるかして復帰するようなハンドラを付け足そう。できれば、一般性を保って、モナド m
についてできるだけ少ない仮定の下で、例外の発生と捕捉の両方を実装したい。
例の1番目のパートは MTL によって素直に実装されている。
newtype TooBig = TooBig Int deriving (Show)
ex2 :: MonadError TooBig m => m Int -> m Int
ex2 m = do
v <- m
if v > 5 then throwError (TooBig v)
else return v
これは、モナド m
について TooBig
例外をサポートするということ以外に何の構造も課していない。私たちは、計算を保護 (guard) している ex2
をリストから整数を選び出す非決定論的計算に適用することで ex2
をテストする。ただし、
choose :: MonadPlus m => [a] -> m a
choose = msum . map return
である。ex2 (choose [5,7,1])
を実行するには、MonadError
と MonadPlus
の制約を満たすモナド m
を選ばねばならない。 MTL は独立した作用に応答できるモナド変換子を合成することで、そうしたモナドを作らせてくれる。私たちの例において、層 ErrorT Too Big
と ListT
を基本のモナド Identity
に適用することでそれはできる。(註:ListT m
は m
が(>=>
の演算について)可換なモナドであるときのみモナドであるので、ListT
は厳密にいえばモナド変換子ではないけれども、非決定論のために私たちは ListT
を使う。ListT
は単純で MTL 正典の一部であり、私たちの例の目的において、ListT
はモナド変換子の十分よい近似であるからである。)
層 ErrorT
と ListT
は2種類の順番で合成でき、それぞれ異なる振る舞いをする。一方の合成の順番は計算の型 (ErrorT TooBig (ListT Identity)) a
をもたらす。型の略称を展開して Identity
を除けば、[Either TooBig a]
が得られる。それぞれの選択が値か TooBig
例外を生み出しうる非決定論的計算の型である。ゆえに例外は非決定論的選択の内部に収まっている。私たちの例では、例外が計算を完全に中断しなければならない。ゆえに、私たちはモナドの層の逆順の合成、ListT (ErrorT TooBig Identity) a
を使うべきである。これは脱糖衣化すると Either TooBig [a]
となり、TooBig
の例外か非決定論的計算のリストを生みうる計算の型となる。モナド変換子のこの順番は、Identity
、ErrorT
、ListT
の関数がこの順番で合成されることにより生じる。
ex2_1 = runIdentity . runErrorT . runListT $ ex2 (choose [5,7,1])
-- Left (TooBig 7)
コメントに書いてある結果は、望ましいものである。
私たちの例の2番目のパートは例外を補足し、ある場合に復帰するものである。
exRec :: MonadError TooBig m => m Int -> m Int
exRec m = catchError m handler
where handler (TooBig n) | n <= 7 = return n
handler e = throwError e
このラッパーは、引数の計算が TooBig
例外で終わったが、値があまりに大きいわけではなかったかどうか確かめる。そうであれば、私たちは復帰する。そうでなければ、例外は再び投げられる。このラッパーに先ほどの例 ex2_1
を付け足す。
ex2r_1 = runIdentity . runErrorT . runListT $
exRec (ex2 (choose [5,7,1]))
-- Right [7]
すると、驚くべき、望まれない結果が生まれる。私たちは例外から復帰しようとしていたのだ!計算 choose [5,7,1]
は3つの非決定論的選択肢を生む。1番目の選択肢 ex2 (return 5)
は例外を投げないので、全体の結果は Right [5]
である。二番目の選択肢 exRec (ex2 (return 7))
において、例外は投げられるが捕捉されず、私たちは Right [7]
を期待する。3番目の選択肢は Right [1]
を与える。これらの選択肢を集めると、私たちは結果が Right [5,7,1]
になることを期待する。つまり、私たちは選択の演算子が持ち「上げ」られることを期待しているのだ。明らかに、そんなことは起こらない。例外は復帰されるが、すべてのほかの選択肢は失われるのである。
私たちが失敗した理由は、例のパート1において、例外が非決定論的選択を捨てるような ErrorT
と ListT
の順序が必要だったということである。例外から復帰すると、選択はすでに復帰不可能なだけ失われている。例外が起こらなかったかのように、選択肢を保持して復帰するには、ErrorT
と ListT
の逆の順番が必要なのである。つまり単一のプログラムは2つの異なる順番のモナド変換子の層を必要とするのである。
この例は MTL を使う場合 ErrorT
の層が2つある型
ErrorT TooBig (ListT (ErrorT TooBig Identity))
を使うという、かなり直感に反する方法で実装される(詳しくは付属するコードの中の [transf.hs
](拡張可能作用 ― モナド変換子に取って代わるもの 付録#appendixh) を参照せよ)。外側の ErrorT TooBig
は、非決定論的選択肢の内部に限られた例外に対応している。ここにおいて、この例外は他の選択肢に影響しないし、復帰可能である。例外は、最後まで復帰されないならば内部の ErrorT
の層に再び投げられ、選択肢がすべて捨てられる。次の関数は例外を再び投げる。
runErrorRelay :: MonadError e m => ErrorT e m a -> m a
runErrorRelay m = runErrorT m >>= check
where check (Right x) = return x
check (Left e) = throwError e
これは私たちの問題の終わりではない。以下のコード
ex22_1 = runIdentity . runErrorT . runListT . runErrorRelay $
ex2 (choose [5,7,1])
-- Right [5]
はとても説明し難い結果を生む。この驚くべき挙動は、MTL の変換子の層のやりとりの結果である (ErrorT
は MonadPlus
としても定義されている)。このやりとりは、MTL に組み込まれていて、変えることができない。例外と非決定論の、MTL に組み込まれた、望まないやりとりを避けるためには、ex2
を書き換えて持ち上げを明示し、
ex1 :: Monad m => m Int -> ErrorT TooBig m Int
ex1 m = do
v <- lift m
if v > 5 then throwError (TooBig v)
else return v
内部の ERrorT TooBig
の層を迂回するようにする必要がある。ついに私たちは、(非決定論的)計算を終了する例外を投げ、そこから完全に復帰する、という目標を達成した。
ex4_1 = runIdentity . runErrorT . runListT . runErrorRelay $
ex1 (choose [5,7,1])
-- Left (TooBig 7)
ex4_21 = runIdentity . runErrorT . runListT . runErrorRelay $
exRec (ex1 (choose [5,7,1]))
-- Right [5,7,1]
ErrorT
の層の避けられない重複は、美しくないだけでなく非効率でもある。保護 (guard) する対象である計算 choose [5,7,1]
の型は ListT (ErrorT TooBig) Int
か Either TooBig [Int]
である。この計算は例外を使わないが、return x
(これは Right [x]
である)を使うたびに、Right
構成子を追加しなければならず、それぞれの bind
はその構成子についてパターンマッチしなければならない。計算全体は、最後にだけ使われるものに、対価を払っているのだ。
例外を発生して復帰するという単純な例の実装が、驚くほどに複雑で非効率であり、その理由がモナド変換子の順序が重要であり、同じ計算の異なる部分が層の異なる順序を要求するということにある、ということをこれまで見てきた。幸運にも、私たちはこの例において層を複製することでこの難問を解決したが、効率性は犠牲になった。次は、モナド変換子を使うのでは、望んでいる作用の計算をどうしても実装できない例である。
5.2 作用の交替
次の例は、モナド変換子の限界を力強く説明する。この例はモナド変換子を使うのでは全く表現できないのだ。動的束縛とコルーチンの2つの作用が交替し、層の順序が静的に固定されていると十分でないのだ。この例は、著者らの一部 [[16](拡張可能作用 ― モナド変換子に取って代わるもの 9#reference16)] による以前の論文において詳しく研究されている実用的な例を抽象化している。(註:http://lambda-the-ultimate.org/node/1396#comment-16128, http://keepworkingworkerbee.blogspot.jp/2005/08/i-learned-today-that-plt-scheme.html を参照。)この例の要点は、最初コルーチンとその親に共有されている動的環境である――コルーチンがこの環境を変更すると、この動的環境はスレッド局所記憶 (thread local) になる。
動的束縛、というのは環境モナド、あるいは Reader
作用(MTL がこれに対して ReaderT
モナド変換子を提供している)に対する別名である。MTL はコルーチンに対する変換子は提供していないが、継続モナド変換子 ContT
を使うと実装は自明である。
type CoT a m = ConT (Y m a) m
data Y m a = Done | Y a (() -> m (Y m a))
yield :: Monad m => a -> CoT a m ()
runC :: Monad m => CoT a m b -> m (Y m a)
CoT a
は型 a
の値を yield
する(譲る)コルーチンに対するモナド変換子である。簡潔さのため、yield
からの再開(そしてそれゆえに yield
の結果の型)は、()
である。コルーチン全体を、値を譲るだけでなく、再開する際に値を受け取るように一般化するのは簡単である。演算 runC
は、終了する(Done
を返す)か yield
にて保留 (suspend) する(そして再開するのに使える継続を返す)コルーチンを実行する。
美文化 (pretty-printing) を行い、現在の紙の幅を動的環境に尋ねるかもしれないコルーチンについてのシナリオを考えてみよう。このクエリは、コルーチンによって設定された最新の値か、コルーチンが引数を束縛していない場合は親によって設定された最新の値を、与える。ゆえにこの動的環境は、コルーチンとその親の間だけで部分的に共有されている――最初は共有されているが、コルーチンが変更した場合はコルーチンのプライベートな値 (coroutine-private) になる。以下の特徴的な例は、こうした、動的環境に対する継承された、プライベートなアクセスを説明する。
th3 :: MonadReader Int m => CoT Int m ()
th3 = ay >> ay >> local (+10) (ay >> ay)
where ay = ask >>= yield
計算 ay
は動的環境を問い合わせ (query) て結果を譲る (yield)。th3
は、親から継承した現在の動的環境を使って、この演算を2回行う。さらに環境を変更し、あと2回問い合わせて譲る。th3
の型が示すように、CoT
の層は MonadReader
の層の上にある。
親のスレッド
c31 = runReaderT (loop =<< runC th3) 10 where
loop (Y x k) = liftIO (print x) >> local (+1) (k ()) >>= loop
loop Done = liftIO (print "Done")
は、10 という最新の値の環境の中でコルーチンを開始し、コルーチンが譲るものを全て表示 (print) し、値が 11 に変化した最新の環境の中でコルーチンを再開する。表示される結果はギョッとするものである―― 10 11 21 11 "Done"
。滑り出しは好調である――コルーチンは、最初 10 である最新の環境を読み取り譲る。コルーチンを再開する動的なスコープの間に親が値を 11
に変え、コルーチンはそれに気づく。コルーチンは環境を 21
に変え、結果はそれを示す。最後に表示された数が間違っている――ローカルな変更は保留のあと失われてしまったのだ。私たちはコルーチンのローカルな (coroutine-local) 動的環境を保持できなかった。
この失敗は型からも予期される。th3
はモナド CoT Int (ReaderT Int IO) a
の中で実行される。この型は
(a -> (Int -> IO w)) -> (Int -> IO w)
へと展開される。ただし w
は (ReaderT Int IO)
、つまり返事の型である。したがって、親が再開したら、親は自らの動的環境を渡すことができる。型は、保留が動的環境を覆い囲んでおらず、保留の呼び主 (caller) から動的環境を必ず受け入れる、ということも示している。ゆえにプライベートな動的環境はありえないのだ。
層の順番を逆にすると、
th4 :: Monad m => ReaderT Int (CoT Int m) ()
th4 = ay >> ay >> local (+10) (ay >> ay)
where ay = ask >>= lift . yield
c4 = loop =<< runC (runReaderT th4 10)
where loop (Y x k) = liftIO (print x) >> (k ()) >>= loop
loop Done = liftIO (print "Done")
表示される結果 10 10 20 20 "Done"
は、動的環境がローカルな束縛 20
についてプライベートになっているということを示している。しかし、親はもはや、自らの動的環境を変えることでコルーチンに影響を与えることは出来ないのだ。実際、別の環境で再開しようとしたとき、local (+1) (k ())
は型検査が通らない。またしても、型は何が起こっているのかを表す。今、th4
はモナドの型 ReaderT Int (CoT Int IO)
を持ち、これは w
を (Y Int IO)
とすると Int -> (a -> IO w) -> IO w
となる。保留は、コルーチンの動的環境を覆い囲んでいて、型 () -> IO w
を持つ。保留はもはや親からいかなる環境も受け入れないのである。
モナド変換子を使う場合、同じ作用が同じ計算の異なる部分において異なる方法でやりとりをすることはできない。モナディックな作用の静的な層は、共有された動的環境かプライベートな動的環境を実装させてくれる。しかし、両方の種類のやりとりをする(実用的に意味のある)プログラムを表現することは出来ないのだ。
5.3 モナド変換子の限界を克服する
私たちは拡張可能作用の実演として、上述の問題となっている例を再実装し、振る舞いが期待通りで、もはや厄介なものではないということを見る。再実装と言うのは強い言葉である――モナド変換子から拡張可能作用へと切り替えても、コードはほとんど変わらないのだから。ほとんどの変更は型注釈に限られている。
例外 [§5.1](拡張可能作用 ― モナド変換子に取って代わるもの 5#section5-1) の1番目の問題は計算 m Int
を保護することであった。生まれた値を確かめて、値が何らかの閾 (threshold) を超えていれば例外を投げるのだ。以下のコードは [§5.1](拡張可能作用 ― モナド変換子に取って代わるもの 5#section5-1) のコード ex2
と等価である。
ex2 :: Member (Exc TooBig) r => Eff r Int -> Eff r Int
ex2 m = do
v <- m
if v > 5 then throwError (TooBig v)
else return v
型注釈だけが変わっていて、制約 Member (Exc TooBig)
が MonadError TooBig m
の代わりに使われている。この例の1番目のパートは、ex2 (choose [5,7,1])
が非決定論的選択を切り捨てる TooBig
例外を生んでいることを確かめると、[§5.1](拡張可能作用 ― モナド変換子に取って代わるもの 5#section5-1) のように振る舞っている。
runErrBig :: Eff (Exc TooBig :> r) a -> Eff r (Either TooBig a)
runErrBig m = runError m
ex2_1 = run . runErrBig . makeChoice $ ex2 (choose [5,7,1])
-- Left (TooBig 7)
[§5.1](拡張可能作用 ― モナド変換子に取って代わるもの 5#section5-1) とは違い、同じ ex2
は例の2番目のパートをサポートする――もし例外の値が本当に大き過ぎるのでなければ、TooBig
例外から復帰するのである。もはや失敗や驚きはない。復帰するコード exRec
は、その名前の由来である [§5.1](拡張可能作用 ― モナド変換子に取って代わるもの 5#section5-1) のモナド変換子のコードと同じであり、型注釈だけが違う。
exRec :: Member (Exc TooBig) r => Eff r Int -> Eff r Int
exRec m = catchError m handler
where handler (TooBig n) | n <= 7 = return n
handler e = throwError e
この復帰は、まさしく期待通りに振る舞う。
ex2r_1 = run . runErrBig . makeChoice $
exRec (ex2 (choose [5,7,1]))
-- Right [5,7,1]
ex2r_2 = run . runErrBig . makeChoice $
exRec (ex2 (choose [5,7,11,1]))
-- Left (TooBig 11)
もしこの例外が本当に復帰されたのならば、(ex2r_1
を参照してほしいが)計算は何の例外もなかったかのように進行し、非決定論的選択には何の影響もない――これは [§5.1](拡張可能作用 ― モナド変換子に取って代わるもの 5#section5-1) のモナド変換子について見たこととは違う。
限定動的スコープ [§5.2](拡張可能作用 ― モナド変換子に取って代わるもの 5#section5-2) の2番目の例は、コルーチンの動的束縛に関するものであった。まず私たちは新しい拡張可能作用、コルーチンを私たちのフレームワークで定義する必要がある。これは少しの努力で可能である。
data Yield a v = Yield a (() -> v)
deriving (Typeable, Functor)
yield :: (Typeable a, Typeable b, Member (Yield a) r) =>
a -> Eff r ()
yield x = send (inj . Yield x)
data Y r a = Done | Y a (() -> Eff r (Y r a))
runC :: Typeable a => Eff (Yield a :> r) w -> Eff r (Y r a)
runC m = loop (admin m) where
loop (Val x) = return Done
loop (E u) = handle_relay u loop $
\(Yield x k) -> return (Y x (loop . k))
Yield
作用は型 a
の値を譲り型 ()
の値とともに再開する計算をモデル化する。(Reader
リクエストの)標準的なパターンに従って、新たなリクエスト Yield
を定義する。このリクエストは譲る値と差出人住所(ここでは ()
の返事しか期待していないので、引数の型が ()
である関数である)を持つ。アクション yield
はリクエストを送る。推論された型注釈はモナド Eff r
が Yield a
作用を含めなければならないということを述べている。Yield
リクエストの解釈機はコルーチンのステータスを素直に生成するものであり、Y r a
と定義されている。
新たな作用を定義するのには、コードは数行で済む。コルーチンのライブラリは本質的に私たちが先ほど MTL で実装したものと同じであるので、動的環境を問い合わせて結果を譲るという先ほどからの例は同じ形を保っている。
th3 :: (Member (Yield Int) r, Member (Reader Int) r)
=> Eff f ()
th3 = ay >> ay >> local (+ (10 :: Int)) (ay >> ay)
where ay = ask >>= yield'
yield' x = yield (x :: Int)
MTL のコードとの違いは、th3
の型注釈と、動的環境の値を Int
に固定するいくつかの注釈だけである。([§4](拡張可能作用 ― モナド変換子に取って代わるもの 4#section4) で述べたように、これらの注釈は省くことができる。)th3
コルーチンを実行し、譲られた値を表示し、修正された動的環境の中で再開するコードも、いくつかの注釈を除いて同じである。
c31 = runTrace $ runReader (loop =<< runC th3) (10 :: Int)
where loop (Y x k) = trace (show (x :: Int)) >>
local (+ (1 :: Int)) (k ()) >>= loop
loop Done = trace "Done"
得られる結果は 10 11 21 21 Done
であり、MTL を使って得られるいかなるものとも全く違う。この結果は、コルーチンが親と動的環境を共有しているということを示している。しかし環境は、ローカルに再束縛されるとコルーチンにとってプライベートなものになり、親にはもう影響されないのである(そしてそれゆえにトレースされた値のうち最後の2つは同じなのである)。拡張可能作用のライブラリは重要なプログラミングパターン――MTL が失敗した仕事――を表現することに成功した。
こうして、拡張可能作用のフレームワークが MTL を包含するということを見てきた。私たちは任意の MTL の計算を(本質帝に同じ構文で)私たちのフレームワークを使って書くことができ、モナド変換子を使うのでは扱いにくかったり実装不可能だったりしたコードを書くこともできるのである。
5.4 制御を用いた非決定論
このセクションの締めくくりとして、私たちのライブラリにおける作用について推論する (reason) のが MTL の対応する作用について推論するよりもずっと素直であるということを示す例を最後の例としよう。定評のある論文において、ヒンツェ [[9](拡張可能作用 ― モナド変換子に取って代わるもの 9#reference9)] は単純な推論原理を使ってどのようにモナド変換子を導き出すかを示している。ヒンツェが Prolog に似た「カット」を使うバックトラッキングについて考えているとき、2つのモナド変換子の実装の導出は、提示された推論原理に従っていない。特に、項ベースのモナド変換子は自由代数に基づいていない――文脈 (context) 渡しの実装は文脈についてパターンマッチしなければならず、それゆえ継続渡しスタイルにはならないのだ。たいてい、導出は全く単純でも機械的でもなくて、「気が遠くなるような」型に頼っていて、証明されていない性質を要求している(論文は帰納法による証明を示唆している。しかし、Haskell の再帰的項に帰納法は当てはまらないので、この視差は十分な根拠に基づいていない)。ヒンツェの導出を、同じ例の以下で述べている並はずれて素直な実装と比べると、いろいろなことに気付くだろう。
目標は、(演算 mzero
と mplus
があり)「カット」と「コール」があるバックトラッキングのモナド(仕様は [[9](拡張可能作用 ― モナド変換子に取って代わるもの 9#reference9), Sec. 5] にあるが、参照するために以下においてこれを繰り返す)を拡張することである。ヒンツェはカットを、それ以上の選択を全て切り捨てる mzero
に似ている原始的な演算 cutfalse :: m a
を使って実装することを推奨している。この演算は Prolog の一般的なイディオムを表現していて、以下の等式の規則を伴うより明確な意味論を持っている。
cutfalse >>= k == cutfalse
cutfalse `mplus` m == cutfalse
つまり、cutfalse
は (>>=)
と mplus
の両方について左零元であるのだ。演算 call :: m a -> m a
は cutfalse
の作用を限定する。cutfalse
に出会ったら、m
の実行後になされた選択だけが切り捨てられる。ヒンツェは以下のコールの公理を要求している。
call mzero == mzero
call (return a `mplus` m) == mzero
call (m `mplus` cutfalse) == call m
call (lift m >>= k) == lift m >>= (call . k)
モナド変換子の導出をこれだけ複雑にしている根本的な問題は、call
と (>>=)
のやりとりを明確に記述し、call
の入れ子になった呼び出しを単純化する方法が無いということである。
拡張可能作用のフレームワークはそのような困難には遭遇しない。初めに私たちは cutfalse
が (>>=)
の左零元であるという性質が、cutfalse
が例外であるということを示していると気付いている。
data CutFalse = CutFalse deriving Typeable
cutfalse = throwError CutFalse
call
は cutfalse
の作用を限定するので、call
は CutFalse
の例外を完全に扱えると考えられる。Choose
作用については、call
は(切り捨てなければならない選択点を蓄積するために)リクエストを傍受 (intercept) し、その後再発行するだろう。call
の型注釈はこれらの性質を要約している。完全なコードは以下の通りである。
call :: Member Choose r => Eff (Exc CutFalse :> r) a -> Eff r a
call m = loop [] (admin m) where
loop jq (Val x) = return x `mplus'` next jq
loop jq (E u) = case decomp u of
Right (Exc CutFalse) -> mzero
Left u -> check jq u
check jq u | Just (Choose [] _) <- prj u = next jq
check jq u | Just (Choose [x] k) <- prj u = loop jq (k x)
check jq u | Just (Choose lst k) <- prj u = next $ map k lst ++ jq
check jq u = send (\k -> fmap k u) >>= loop jq
next [] = mzero
next (h:t) = loop t h
それぞれの節 (clause) は call
か cutfalse
の公理に対応していて、全体ですべての公理をカバーしている。このコードは明らかに、call
が引数の計算の選択点を見守っている、という直感を表現している。cutfalse
リクエストに出会ったら、残りの選択点を切り捨てる。付属の [Eff.hs
](拡張可能作用 ― モナド変換子に取って代わるもの 付録#appendixa) には cutfalse
と call
を使ういくつかの例があり、それには call
の入れ子になった、問題のない現れを含んでいる。
私たちは、2つの既存の作用、Choose
と Exc
のやりとりを定義し、推論する方法を実演してきた。私たちは単に新しい、継ぎ目となるハンドラを定義しただけであるが、このハンドラは Choose
作用と Choose
の他のハンドラ(makeChoice
など)を使っている以前に書かれたコードとともに使うことができる。