注:この回答は、Gistで読み書きのできるHaskellファイルとして入手できます。
私はこの運動をとても楽しんだ。私は答えを見ずにそれをやろうとしました、そしてそれはそれだけの価値がありました。かなりの時間がかかりましたが、結果は驚くべきことに、他の2つの回答と、モナドコルーチンライブラリに近いものになっています。ですから、これはこの問題に対するやや自然な解決策だと思います。この演習がなければ、モナドコルーチンが実際にどのように機能するのか理解できません。
いくつかの付加価値を追加するために、私は最終的に解決策にたどり着いたステップを説明します。
州のモナドを認識する
状態を扱っているので、状態モナドによって効果的に記述できるパターンを探します。特に、s - s
はと同型であるためs -> (s, ())
、に置き換えることができますState s ()
。そして、型の関数は、実際にはであるにs -> x -> (s, y)
反転することができます。これにより、署名が更新されますx -> (s -> (s, y))
x -> State s y
mutate :: State s () - Pause s ()
step :: Pause s () - State s (Maybe (Pause s ()))
一般化
私たちのPause
モナドは現在、州によってパラメータ化されています。ただし、現在、州は実際には何も必要ないことがわかります。また、州のモナドの詳細も使用していません。したがって、任意のモナドによってパラメーター化される、より一般的なソリューションを作成することを試みることができます。
mutate :: (Monad m) = m () -> Pause m ()
yield :: (Monad m) = Pause m ()
step :: (Monad m) = Pause m () -> m (Maybe (Pause m ()))
また、だけでなく、あらゆる種類の値を許可することmutate
で、step
より一般的なものにすることもできます()
。Maybe a
そして、それが同型であることを認識することによって、Either a ()
最終的に署名を次のように一般化することができます。
mutate :: (Monad m) = m a -> Pause m a
yield :: (Monad m) = Pause m ()
step :: (Monad m) = Pause m a -> m (Either (Pause m a) a)
そのためstep
、計算の中間値を返します。
モナド変換子
これで、実際にモナドからモナドを作成しようとしていることがわかります。いくつかの機能を追加します。これは通常モナド変換子と呼ばれるものです。さらに、の署名はからのリフトmutate
とまったく同じです。おそらく、私たちは正しい方向に進んでいます。MonadTrans
最後のモナド
step
関数は私たちのモナドの最も重要な部分のようです、それは私たちが必要とするものだけを定義します。おそらく、これは新しいデータ構造である可能性がありますか?やってみよう:
import Control.Monad
import Control.Monad.Cont
import Control.Monad.State
import Control.Monad.Trans
data Pause m a
= Pause { step :: m (Either (Pause m a) a) }
Either
パーツがの場合Right
、それは一時的な値であり、中断はありません。これにより、最も簡単なものを実装する方法がわかりlift
ますMonadTrans
。
instance MonadTrans Pause where
lift k = Pause (liftM Right k)
そしてmutate
単に専門です:
mutate :: (Monad m) => m () -> Pause m ()
mutate = lift
Either
パーツがである場合Left
、それは一時停止後の継続的な計算を表します。それでは、そのための関数を作成しましょう。
suspend :: (Monad m) => Pause m a -> Pause m a
suspend = Pause . return . Left
yield
計算を行うのは簡単です。空の計算で一時停止します。
yield :: (Monad m) => Pause m ()
yield = suspend (return ())
それでも、最も重要な部分が欠けています。Monad
インスタンス。修正しましょう。実装return
は簡単で、内側のモナドを持ち上げるだけです。実装>>=
は少し注意が必要です。元のPause
値が単純な値()のみであった場合は、結果としてRight y
ラップするだけです。f y
継続できる一時停止された計算である場合(Left p
)、再帰的にその中に下降します。
instance (Monad m) => Monad (Pause m) where
return x = lift (return x) -- Pause (return (Right x))
(Pause s) >>= f
= Pause $ s >>= \x -> case x of
Right y -> step (f y)
Left p -> return (Left (p >>= f))
テスト
状態を使用および更新するモデル関数を作成して、計算中に生成してみましょう。
test1 :: Int -> Pause (State Int) Int
test1 y = do
x <- lift get
lift $ put (x * 2)
yield
return (y + x)
そして、モナドをデバッグするヘルパー関数-その中間ステップをコンソールに出力します:
debug :: Show s => s -> Pause (State s) a -> IO (s, a)
debug s p = case runState (step p) s of
(Left next, s') -> print s' >> debug s' next
(Right r, s') -> return (s', r)
main :: IO ()
main = do
debug 1000 (test1 1 >>= test1 >>= test1) >>= print
結果は
2000
4000
8000
(8000,7001)
予想通り。
コルーチンとモナドコルーチン
私たちが実装したのは、コルーチンを実装する非常に一般的なモナディックソリューションです。おそらく驚くことではありませんが、誰かが以前にアイデアを持っていて、モナドコルーチンパッケージを作成しました:-)。それほど驚くことではありませんが、これは私たちが作成したものと非常によく似ています。
パッケージは、アイデアをさらに一般化します。継続的な計算は、任意のファンクター内に格納されます。これにより、中断された計算を処理する方法の多くのバリエーションを中断できます。たとえば、resumeの呼び出し元(これを呼び出しました)に値を渡したり、値が提供されるのを待って続行したりします。step