web-dev-qa-db-ja.com

一時停止モナド

モナドは多くの驚くべき、クレイジーなことをすることができます。それらは、値の重ね合わせを保持する変数を作成できます。計算する前に、将来のデータにアクセスできるようになります。彼らはあなたが破壊的な更新を書くことを可能にすることができますが、実際にはそうではありません。そして、継続モナドを使用すると、人々の心を壊す!通常は自分のものにすることができます。 ;-)

しかし、ここに課題があります。一時停止のモナドを作成できますか?

[。 )
 step :: s-> Pause s()->(s、Maybe(Pause s()))

Pauseモナドは一種の状態モナドです(したがって、mutate、明白なセマンティクスを備えています)。通常、このようなモナドには、計算を実行して最終状態を返す、ある種の「実行」関数があります。ただし、Pauseは異なります。step関数を提供します。この関数は、魔法のyield関数を呼び出すまで計算を実行します。ここで計算は一時停止され、後で計算を再開するのに十分な情報が呼び出し元に返されます。

さらに素晴らしい場合:呼び出し元がstep呼び出し間の状態を変更できるようにします。 (たとえば、上記の型シグネチャはこれを可能にするはずです。)


ユースケース:複雑なことを行うコードを書くのは簡単なことがよくありますが、それを変換するためのトータルPITA output操作の中間状態。ユーザーが実行の途中でchange何かを実行できるようにしたい場合、物事は非常に速く複雑になります。

実装のアイデア:

  • 明らかにスレッド、ロック、およびIOを使用して実行できます。しかし、もっとうまくやれるでしょうか? ;-)

  • 継続モナドで何か気が狂った?

  • おそらく、yieldが現在の状態をログに記録するだけの、ある種のライターモナドであり、ログ内の状態を反復処理することで、stepのふりをすることができます。 (明らかに、現在は実際には何も「一時停止」していないため、ステップ間で状態を変更することはできません。)

64

承知しました;計算を結果で終了させるか、それ自体を一時停止して、再開時に使用するアクションとその時点の状態を指定するだけです。

data Pause s a = Pause { runPause :: s -> (PauseResult s a, s) }

data PauseResult s a
    = Done a
    | Suspend (Pause s a)

instance Monad (Pause s) where
    return a = Pause (\s -> (Done a, s))
    m >>= k = Pause $ \s ->
        case runPause m s of
            (Done a, s') -> runPause (k a) s'
            (Suspend m', s') -> (Suspend (m' >>= k), s')

get :: Pause s s
get = Pause (\s -> (Done s, s))

put :: s -> Pause s ()
put s = Pause (\_ -> (Done (), s))

yield :: Pause s ()
yield = Pause (\s -> (Suspend (return ()), s))

step :: Pause s () -> s -> (Maybe (Pause s ()), s)
step m s =
    case runPause m s of
        (Done _, s') -> (Nothing, s')
        (Suspend m', s') -> (Just m', s')

Monadインスタンスは、通常の方法でシーケンスを実行し、最終結果をk継続に渡すか、中断時に実行される残りの計算を追加します。

56
ehird

注:このモナドの現在の状態sに直接アクセスすることはできませんでした。

Pause sは、mutateおよびyield操作の単なる無料のモナドです。直接実装すると、次のようになります。

data Pause s a
  = Return a
  | Mutate (s -> s) (Pause s a)
  | Yield (Pause s a)

instance Monad (Pause s) where
  return = Return
  Return a   >>= k = k a
  Mutate f p >>= k = Mutate f (p >>= k)
  Yield p    >>= k = Yield (p >>= k)

目的のAPIを提供するためのいくつかのスマートコンストラクターを使用します。

mutate :: (s -> s) -> Pause s ()
mutate f = Mutate f (return ())

yield :: Pause s ()
yield = Yield (return ())

そしてそれを駆動するためのステップ関数

step :: s -> Pause s () -> (s, Maybe (Pause s ()))
step s (Mutate f k) = step (f s) k
step s (Return ()) = (s, Nothing)
step s (Yield k) = (s, Just k)

これを直接使用して定義することもできます

data Free f a = Pure a | Free (f (Free f a))

(私の「無料」パッケージから)

data Op s a = Mutate (s -> s) a | Yield a

一時停止のモナドがすでにあります

type Pause s = Free (Op s)

スマートコンストラクターとステッパーを定義する必要があります。

高速化

現在、これらの実装は簡単に推論できますが、最速の運用モデルはありません。特に、(>> =)の左関連の使用は、漸近的に遅いコードを生成します。

これを回避するには、 Codensity モナドを既存の無料モナドに適用するか、 'Church free' モナドを直接使用します。どちらも詳細に説明します。ブログ。

http://comonad.com/reader/2011/free-monads-for-less/

http://comonad.com/reader/2011/free-monads-for-less-2/

http://comonad.com/reader/2011/free-monads-for-less-3/

Freeモナドのチャーチエンコードバージョンを適用した結果、データ型のモデルについて簡単に推論でき、それでも高速な評価モデルが得られます。

63
Edward KMETT

freeモナドを使用して、これを実行する方法を説明します。えーと、ええと、彼らは何ですか?それらは、ノードにアクションがあり、リーフに値があるツリーであり、>>=木の接ぎ木のように振る舞います。

data f :^* x
  = Ret x
  | Do (f (f :^* x))

Fを書くことは珍しいことではありません*数学のそのようなことのためのX、それ故に私の気難しい中置タイプ名。インスタンスを作成するには、fをマップできるものにする必要があります。どのFunctorでもかまいません。

instance Functor f => Monad ((:^*) f) where
  return = Ret
  Ret x  >>= k  = k x
  Do ffx >>= k  = Do (fmap (>>= k) ffx)

これは、「すべての葉にkを適用し、結果のツリーに移植する」だけです。これらの缶ツリーは、インタラクティブな計算のためにstrategiesを表します。ツリー全体が環境とのすべての可能な相互作用をカバーし、環境はツリー内のどのパスをたどるかを選択します。なぜ彼らは無料?それらは単なる木であり、どの戦略が他のどの戦略と同等であるかを示す興味深い方程式理論はありません。

それでは、実行したい一連のコマンドに対応するファンクターを作成するためのキットを用意しましょう。この事

data (:>>:) s t x = s :? (t -> x)

instance Functor (s :>>: t) where
  fmap k (s :? f) = s :? (k . f)

入力タイプxおよび出力タイプsの-​​oneコマンドの後にtで値を取得するというアイデアをキャプチャします。これを行うには、sで入力を選択し、xでコマンドの出力を指定してtで値を続行する方法を説明する必要があります。そのようなもの全体に関数をマッピングするには、それを継続に追加します。これまでのところ、標準装備。私たちの問題では、2つのファンクターを定義することができます。

type Modify s  = (s -> s) :>>: ()
type Yield     = () :>>: ()

実行したいコマンドの値型を書き留めたようなものです。

次に、これらのコマンドの間にchoiceを提供できることを確認しましょう。ファンクター間の選択がファンクターを生み出すことを示すことができます。より標準的な装備。

data (:+:) f g x = L (f x) | R (g x)

instance (Functor f, Functor g) => Functor (f :+: g) where
  fmap k (L fx) = L (fmap k fx)
  fmap k (R gx) = R (fmap k gx)

そう、 Modify s :+: Yieldは、変更と降伏の間の選択を表します。 signatureの単純なコマンド(計算を操作するのではなく、値の観点から世界と通信する)は、この方法でファンクターに変換できます。手でやらなきゃいけないのが面倒!

それは私にあなたのモナドを与えます:modifyとyieldの署名上の無料のモナド。

type Pause s = (:^*) (Modify s :+: Yield)

Modifyコマンドとyieldコマンドをone-do-then-returnとして定義できます。 yieldのダミー入力をネゴシエートすることを除けば、それは単なる機械的なものです。

mutate :: (s -> s) -> Pause s ()
mutate f = Do (L (f :? Ret))

yield :: Pause s ()
yield = Do (R (() :? Ret))

step関数は、戦略ツリーに意味を与えます。それは制御演算子であり、ある計算を(多分)別の計算から構築します。

step :: s -> Pause s () -> (s, Maybe (Pause s ()))
step s (Ret ())            = (s, Nothing)
step s (Do (L (f  :? k)))  = step (f s) (k ())
step s (Do (R (() :? k)))  = (s, Just (k ()))

step関数は、Retで終了するか、生成されるまで戦略を実行し、状態を変化させます。

一般的な方法は次のようになります。コマンド(値の観点から相互作用する)を制御演算子(計算を操作する)から分離します。コマンドの署名(ハンドルのクランク)の上に「戦略ツリー」の無料モナドを構築します。戦略ツリーを再帰的に実行して、制御演算子を実装します。

32
pigworker

タイプの署名と正確には一致しませんが、確かに単純です。

{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, UndecidableInstances #-}
import Control.Monad.State

newtype ContinuableT m a = Continuable { runContinuable :: m (Either a (ContinuableT m a)) }
instance Monad m => Monad (ContinuableT m) where
    return = Continuable . return . Left
    Continuable m >>= f = Continuable $ do
        v <- m
        case v of
            Left  a -> runContinuable (f a)
            Right b -> return (Right (b >>= f))

instance MonadTrans ContinuableT where
    lift m = Continuable (liftM Left m)

instance MonadState s m => MonadState s (ContinuableT m) where
    get = lift get
    put = lift . put

yield :: Monad m => ContinuableT m a -> ContinuableT m a
yield = Continuable . return . Right

step :: ContinuableT (State s) a -> s -> (Either a (ContinuableT (State s) a), s)
step = runState . runContinuable

-- mutate unnecessary, just use modify
12
Daniel Wagner
_{-# LANGUAGE TupleSections #-}
newtype Pause s x = Pause (s -> (s, Either x (Pause s x)))

instance Monad (Pause s) where
  return x = Pause (, Left x)

  Pause k >>= f = Pause $ \s -> let (s', v) = k s in
                                case v of
                                  Left x -> step (f x) s'
                                  Right x -> (s', Right (x >>= f))

mutate :: (s -> s) -> Pause s ()
mutate f = Pause (\s -> (f s, Left ()))

yield :: Pause s ()
yield = Pause (, Right (return ()))

step :: Pause s x -> s -> (s, Either x (Pause s x))
step (Pause x) = x
_

それが私がこれを書いた方法です。 stepにもう少し一般的な定義を付けましたが、runPauseという名前にすることもできます。実際、stepのタイプについて考えると、Pauseの定義につながります。

monad-coroutine パッケージには、一般的なモナド変換子が含まれています。 _Pause s_モナドはCoroutine (State s) Idと同じです。コルーチンを他のモナドと組み合わせることができます。

関連: http://themonadreader.files.wordpress.com/2010/01/issue15.pdf のプロンプトモナド

9
sdcvvc

注:この答えは 利用可能 Gistの読み書きのできるHaskellファイルとしてです。

私はこの運動をとても楽しんだ。私は答えを見ずにそれをやろうとしました、そしてそれはそれだけの価値がありました。かなりの時間がかかりましたが、結果は驚くほど他の2つの答えと、 monad-coroutine ライブラリに近いものになっています。したがって、これはこの問題に対するやや自然な解決策だと思います。この演習がなければ、monad-coroutineが実際にどのように機能するのか理解できません。

いくつかの付加価値を追加するために、最終的にソリューションにたどり着いた手順を説明します。

状態モナドの認識

州を扱っているので、州のモナドで効果的に記述できるパターンを探します。特に、_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 ()))
_

また、_()_だけでなく、あらゆる種類の値を許可することで、mutatestepをより一般的にしようとすることもできます。そして、_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の-​​ lift とまったく同じです。おそらく、私たちは正しい方向に進んでいます。

最後のモナド

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の場合、これは一時停止のない単なるモナド値です。これにより、最も簡単なもの、つまりliftMonadTrans関数を実装する方法がわかります。

_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)
_

予想通り。

コルーチンとmonad-コルーチン

私たちが実装したのは、 コルーチン を実装する非常に一般的なモナドソリューションです。おそらく驚くことではないが、誰かが以前にアイデアを持っていて、 monad-coroutine パッケージを作成した。それほど驚くことではありませんが、これは私たちが作成したものと非常によく似ています。

パッケージはアイデアをさらに一般化します。継続的な計算は、任意のファンクター内に保存されます。これにより、 suspend 中断された計算を処理する方法の多くのバリエーションが可能になります。たとえば、to 値を渡す の呼び出し元に resumestepと呼びます)、またはto 値を待つ =継続するために提供されるなど。

9
Petr Pudlák