これは、Contモナドが定義される方法です。
newtype Cont r a = Cont { runCont :: (a -> r) -> r }
instance Monad (Cont r) where
return a = Cont ($ a)
m >>= k = Cont $ \c -> runCont m $ \a -> runCont (k a) c
これがどのように、そしてなぜ機能するのか説明していただけますか?何してるの?
継続モナドについて最初に気付くのは、基本的に、それは実際には何でもないということですdoing何もありません。それは本当です!
一般的な継続の基本的な考え方は、それが残りの計算を表すということです。次のような式があるとします:foo (bar x y) z
。ここで、括弧で囲まれた部分_bar x y
_--を抽出します。これは式全体のpartですが、適用できる関数だけではありません。代わりに、関数を適用する必要がありますto。したがって、この場合の「残りの計算」は_\a -> foo a z
_であると言えます。これを_bar x y
_に適用して、完全なフォームを再構築できます。
さて、この「残りの計算」の概念は便利ですが、検討している部分式の外にあるため、操作が面倒です。物事をより良く機能させるために、物事を裏返しにすることができます。関心のある部分式を抽出し、残りの計算を表す引数をとる関数\k -> k (bar x y)
でラップします。
この変更されたバージョンは、多くの柔軟性を提供します-コンテキストから部分式を抽出するだけでなく、部分式自体の中でその外部コンテキストを操作する。これは一種の中断された計算と考えることができ、次に何が起こるかを明示的に制御できます。さて、これをどのように一般化できますか?さて、部分式はほとんど変更されていないので、それを裏返し関数のパラメーターに置き換えて、_\x k -> k x
_--つまり、関数適用、反転を与えます。 =。 flip ($)
を簡単に記述したり、エキゾチックな外国語のフレーバーを少し追加して、演算子_|>
_として定義したりすることもできます。
さて、表現のすべての部分をこの形式に変換することは、退屈で恐ろしく難読化されていますが、簡単です。幸いなことに、より良い方法があります。 Haskellプログラマーとして、私たちが考えるときバックグラウンドコンテキスト内で計算を構築する次に考えるのはたとえば、これはモナドですか?そしてこの場合の答えは- はい、はい、そうです。
これをモナドに変えるために、2つの基本的な構成要素から始めます。
m
の場合、タイプ_m a
_の値は、モナドのコンテキスト内でタイプa
の値にアクセスできることを表します。このコンテキスト内でタイプa
の何かにアクセスできるとはどういう意味ですか?これは、ある値_x :: a
_に対して、flip ($)
をx
に適用し、タイプa
の引数を取る関数を取り、その関数をx
に適用することを意味します。 。タイプBool
の値を保持する中断された計算があるとしましょう。これは私たちにどのようなタイプを与えますか?
_> :t flip ($) True
flip ($) True :: (Bool -> b) -> b
_
したがって、中断された計算の場合、タイプ_m a
_は_(a -> b) -> b
_...になります。これは、Cont
の署名をすでに知っているので、おそらく逆クライマックスですが、今のところはユーモアがあります。
ここで注目すべき興味深い点は、一種の「反転」がモナドの型にも適用されることです。_Cont b a
_は、関数_a -> b
_を取り、b
に評価される関数を表します。継続は計算の「未来」を表すため、署名のタイプa
は、ある意味で「過去」を表します。
では、_(a -> b) -> b
_を_Cont b a
_に置き換えると、逆関数適用の基本的な構成要素のモナド型は何ですか? a -> (a -> b) -> b
は_a -> Cont b a
_...に変換されます。return
と同じ型の署名であり、実際、それはまさにそれです。
これ以降、すべてがタイプから直接外れます。実際の実装以外に、_>>=
_を実装するための賢明な方法は基本的にありません。しかし、実際には何ですかdoing?
この時点で、最初に言ったことに戻ります。継続モナドは実際にはそうではありませんdoingほとんど何もありません。タイプ_Cont r a
_の何かは、中断された計算の引数としてa
を指定するだけで、タイプid
の何かと簡単に同等です。これにより、_Cont r a
_がモナドであるが、変換が非常に簡単である場合、a
だけでまたモナドにすべきではないかどうかを尋ねる人がいるかもしれません。もちろん、Monad
インスタンスとして定義する型コンストラクターがないため、そのままでは機能しませんが、_data Id a = Id a
_のような簡単なラッパーを追加するとします。これは確かにモナド、つまり単位元モナドです。
_>>=
_はアイデンティティモナドに対して何をしますか?型シグネチャはId a -> (a -> Id b) -> Id b
であり、これはa -> (a -> b) -> b
と同等であり、これも単純な関数適用です。 _Cont r a
_が_Id a
_と自明に同等であることを確認したら、この場合も_(>>=)
_は単なる関数適用であると推測できます。
もちろん、_Cont r a
_は、誰もがひげを生やしているクレイジーな逆世界です。したがって、実際に発生するのは、2つの中断された計算を新しい中断された計算にチェーンするために、混乱する方法で物事をシャッフルすることですが、本質的には実際には異常なことは何も起こっていません!関数を引数に適用する、ほんとうに、関数型プログラマーの人生の別の日。
これがフィボナッチです:
_fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)
_
呼び出しスタックのないマシンがあると想像してください-それは末尾再帰のみを許可します。そのマシンでfib
を実行する方法は?指数時間ではなく線形で機能するように関数を簡単に書き直すことができますが、それはほんの少しの洞察を必要とし、機械的ではありません。
末尾再帰にする際の障害は、2つの再帰呼び出しがある3行目です。呼び出しは1回だけで、結果を出す必要があります。ここから継続が入ります。
fib (n-1)
に追加のパラメーターを取得させます。これは、結果を計算した後に何を実行するかを指定する関数であり、x
と呼びます。もちろん、それにfib (n-2)
を追加します。したがって、_fib n
_を計算するには、その後fib (n-1)
を計算し、結果x
を呼び出すと、fib (n-2)
を計算します。その後、結果y
を呼び出すと、次のようになります。 _x+y
_。
言い換えれば、あなたは言わなければなりません:
次の計算を行う方法:「_fib' n c
_ = _fib n
_を計算し、結果にc
を適用する」?
答えは、「fib (n-1)
を計算して結果にd
を適用する」ということです。ここで、_d x
_は「fib (n-2)
を計算してe
を結果に適用する」という意味です。ここで、_e y
_はc (x+y)
を意味します。コード内:
_fib' 0 c = c 0
fib' 1 c = c 1
fib' n c = fib' (n-1) d
where d x = fib' (n-2) e
where e y = c (x+y)
_
同様に、ラムダを使用できます。
_fib' 0 = \c -> c 0
fib' 1 = \c -> c 1
fib' n = \c -> fib' (n-1) $ \x ->
fib' (n-2) $ \y ->
c (x+y)
_
実際のフィボナッチを取得するには、IDを使用します:_fib' n id
_。行fib (n-1) $ ...
がその結果x
を次の行に渡すと考えることができます。
最後の3行はdo
ブロックのようなにおいがし、実際には
_fib' 0 = return 0
fib' 1 = return 1
fib' n = do x <- fib' (n-1)
y <- fib' (n-2)
return (x+y)
_
モナドCont
の定義により、newtypesまでは同じです。違いに注意してください。最初に_\c ->
_の代わりに_x <- ...
_があり、c
の代わりに_... $ \x ->
_とreturn
があります。
CPSを使用して末尾再帰スタイルでfactorial n = n * factorial (n-1)
を記述してみてください。
_>>=
_はどのように機能しますか? _m >>= k
_はと同等です
_do a <- m
t <- k a
return t
_
_fib'
_と同じスタイルで、翻訳を元に戻すと、
_\c -> m $ \a ->
k a $ \t ->
c t
_
_\t -> c t
_をc
に単純化する
_m >>= k = \c -> m $ \a -> k a c
_
取得したニュータイプを追加する
_m >>= k = Cont $ \c -> runCont m $ \a -> runCont (k a) c
_
これはこのページの上部にあります。複雑ですが、do
表記と直接使用の間の変換方法を知っている場合は、_>>=
_の正確な定義を知る必要はありません。 do-blockを見ると、継続モナドがはるかに明確になっています。
モナドと継続
リストモナドのこの使用法を見ると...
_do x <- [10, 20]
y <- [3,5]
return (x+y)
[10,20] >>= \x ->
[3,5] >>= \y ->
return (x+y)
([10,20] >>=) $ \x ->
([3,5] >>=) $ \y ->
return (x+y)
_
それは継続のように見えます!実際、1つの引数を適用する場合の_(>>=)
_のタイプは_(a -> m b) -> m b
_で、これはCont (m b) a
です。説明については、sigfpeの すべてのモナドの母 を参照してください。それはおそらくそれを意味するものではありませんでしたが、私はそれを良い継続モナドチュートリアルと見なします。
継続とモナドは両方向で非常に強く関連しているので、モナドに適用されることは継続にも当てはまると思います。ハードワークだけがそれらを教えてくれ、ブリトーの比喩や類推を読むことはありません。
編集:記事は以下のリンクに移行されました。
このトピックに直接対処するチュートリアルを作成しましたので、お役に立てば幸いです。 (それは確かに私の理解を固めるのに役立ちました!)スタックオーバーフローのトピックに快適に収まるには少し長すぎるので、HaskellWikiに移行しました。
参照してください: ボンネットの下のMonadCont
Cont
モナドを把握する最も簡単な方法は、コンストラクターの使用方法を理解することだと思います。 transformers
パッケージの現実は少し異なりますが、ここでは次の定義を想定します。
newtype Cont r a = Cont { runCont :: (a -> r) -> r }
これは与える:
Cont :: ((a -> r) -> r) -> Cont r a
したがって、タイプCont r a
の値を作成するには、Cont
に関数を与える必要があります。
value = Cont $ \k -> ...
現在、k
自体の型はa -> r
であり、ラムダの本体の型はr
である必要があります。明らかなことは、k
をタイプa
の値に適用し、タイプr
の値を取得することです。そうです、そうすることはできますが、それは私たちができる多くのことの1つにすぎません。 value
はr
でポリモーフィックである必要はなく、タイプCont String Integer
またはその他の具体的なものである可能性があることに注意してください。そう:
k
をタイプa
のいくつかの値に適用し、結果を何らかの方法で組み合わせることができます。k
をタイプa
の値に適用し、結果を観察してから、それに基づいてk
を他の何かに適用することを決定できます。k
を完全に無視して、タイプr
の値を自分で生成することができます。しかし、これはどういう意味ですか? k
は最終的に何になりますか?まあ、do-blockでは、次のようなものがあるかもしれません:
flip runCont id $ do
v <- thing1
thing2 v
x <- Cont $ \k -> ...
thing3 x
thing4
楽しい部分は次のとおりです。Cont
コンストラクターの出現時にdo-blockを2つに分割し、残りの計算全体を考えることができます afterそれ自体の値として。ただし、それが何であるかはx
が何であるかによって異なるため、実際には、タイプx
の値a
から関数になります。いくつかの結果値:
restOfTheComputation x = do
thing3 x
thing4
実際、このrestOfTheComputation
は大まかに言えばk
が最終的に何であるかです。言い換えると、k
計算の結果x
になる値を使用して、Cont
を呼び出し、残りの計算を実行してから、生成されたr
がk
を呼び出した結果、ラムダに戻ります。そう:
k
を複数回呼び出した場合、残りの計算は複数回実行され、結果は必要に応じて組み合わせることができます。k
をまったく呼び出さなかった場合、残りの計算全体はスキップされ、それを囲むrunCont
呼び出しは、管理したタイプr
の値を返すだけです。合成。つまり、計算の他の部分がtheirk
からyouを呼び出して、結果...この時点でまだ私と一緒にいる場合は、これが非常に強力であることが簡単にわかります。少し要点を述べるために、いくつかの標準型クラスを実装しましょう。
instance Functor (Cont r) where
fmap f (Cont c) = Cont $ \k -> ...
タイプCont
のバインド結果x
のa
値と、関数f :: a -> b
が与えられ、バインドでCont
値を作成します。タイプb
の結果f x
。バインド結果を設定するには、k
..を呼び出すだけです。
fmap f (Cont c) = Cont $ \k -> k (f ...
待って、どこからx
を取得しますか?さて、それは私たちがまだ使用していないc
を含むでしょう。 c
がどのように機能するかを覚えておいてください。関数が与えられ、バインド結果を使用してその関数を呼び出します。そのバインド結果にf
を適用してour関数を呼び出したいと思います。そう:
fmap f (Cont c) = Cont $ \k -> c (\x -> k (f x))
多田!次は、Applicative
:
instance Applicative (Cont r) where
pure x = Cont $ \k -> ...
これは簡単です。バインド結果は、取得したx
になります。
pure x = Cont $ \k -> k x
さて、<*>
:
Cont cf <*> Cont cx = Cont $ \k -> ...
これは少しトリッキーですが、基本的にfmapと同じアイデアを使用します。最初に、呼び出すラムダを作成して、最初のCont
から関数を取得します。
Cont cf <*> Cont cx = Cont $ \k -> cf (\fn -> ...
次に、2番目から値x
を取得し、fn x
をバインド結果にします。
Cont cf <*> Cont cx = Cont $ \k -> cf (\fn -> cx (\x -> k (fn x)))
Monad
はほとんど同じですが、runCont
またはケースが必要であるか、新しいタイプを解凍する必要があります。
この答えはすでにかなり長いので、ContT
には立ち入りません(つまり、Cont
とまったく同じです!唯一の違いは、型コンストラクターの種類、すべてが同一)またはcallCC
(k
を無視する便利な方法を提供し、サブブロックからの早期終了を実装する便利なコンビネーター)。
シンプルでもっともらしいアプリケーションについては、Edward Z. Yangのブログ投稿を実装してみてください breakとラベル付けされてforループで続行 。
他の答えを補完しようとしています:
ネストされたラムダは読みやすさのために恐ろしいです。これが、中間変数を使用してネストされたラムダを削除するために、let ... in ...および... where ...が存在する理由です。これらを使用して、バインドの実装を次のようにリファクタリングできます。
newtype Cont r a = Cont { runCont :: (a -> r) -> r }
instance Monad (Cont r) where
return a = Cont ($ a)
m >>= k = k a
where a = runCont m id
うまくいけば、何が起こっているのかがより明確になります。怠惰な適用で実装ボックスの値を返します。 runCont idを使用すると、ボックス化された値にidが適用され、元の値が返されます。
ボックス化された値を単純にボックス化解除できるモナドの場合、通常、バインドの簡単な実装があります。これは、値をボックス化解除してモナド関数を適用することです。
元の質問で難読化された実装を取得するには、最初にkaをCont $ runCont(k a)に置き換えます。次に、これをCont $\c-> runCont(k a)cに置き換えることができます。
これで、whereを部分式に移動できるので、
Cont $ \c-> ( runCont (k a) c where a = runCont m id )
括弧内の式は、\ a-> runCont(k a)c $ runCont midに脱糖できます。
最後に、runContのプロパティf(runCont m g)= runCont m(f.g)を使用して、元の難読化された式に戻ります。