web-dev-qa-db-ja.com

Haskell Contモナドはどのようにそしてなぜ機能するのですか?

これは、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

これがどのように、そしてなぜ機能するのか説明していただけますか?何してるの?

74
monb

継続モナドについて最初に気付くのは、基本的に、それは実際には何でもないということです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つの中断された計算を新しい中断された計算にチェーンするために、混乱する方法で物事をシャッフルすることですが、本質的には実際には異常なことは何も起こっていません!関数を引数に適用する、ほんとうに、関数型プログラマーの人生の別の日。

109
C. A. McCann

これがフィボナッチです:

_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の すべてのモナドの母 を参照してください。それはおそらくそれを意味するものではありませんでしたが、私はそれを良い継続モナドチュートリアルと見なします。

継続とモナドは両方向で非常に強く関連しているので、モナドに適用されることは継続にも当てはまると思います。ハードワークだけがそれらを教えてくれ、ブリトーの比喩や類推を読むことはありません。

39
sdcvvc

編集:記事は以下のリンクに移行されました。

このトピックに直接対処するチュートリアルを作成しましたので、お役に立てば幸いです。 (それは確かに私の理解を固めるのに役立ちました!)スタックオーバーフローのトピックに快適に収まるには少し長すぎるので、HaskellWikiに移行しました。

参照してください: ボンネットの下のMonadCont

17
Owen S.

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つにすぎません。 valuerでポリモーフィックである必要はなく、タイプ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を呼び出し、残りの計算を実行してから、生成されたrkを呼び出した結果、ラムダに戻ります。そう:

  • kを複数回呼び出した場合、残りの計算は複数回実行され、結果は必要に応じて組み合わせることができます。
  • kをまったく呼び出さなかった場合、残りの計算全体はスキップされ、それを囲むrunCont呼び出しは、管理したタイプrの値を返すだけです。合成。つまり、計算の他の部分がtheirkからyouを呼び出して、結果...

この時点でまだ私と一緒にいる場合は、これが非常に強力であることが簡単にわかります。少し要点を述べるために、いくつかの標準型クラスを実装しましょう。

instance Functor (Cont r) where
  fmap f (Cont c) = Cont $ \k -> ...

タイプContのバインド結果xa値と、関数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とまったく同じです!唯一の違いは、型コンストラクターの種類、すべてが同一)またはcallCCkを無視する便利な方法を提供し、サブブロックからの早期終了を実装する便利なコンビネーター)。

シンプルでもっともらしいアプリケーションについては、Edward Z. Yangのブログ投稿を実装してみてください breakとラベル付けされてforループで続行

9
Ben Millwood

他の答えを補完しようとしています:

ネストされたラムダは読みやすさのために恐ろしいです。これが、中間変数を使用してネストされたラムダを削除するために、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)を使用して、元の難読化された式に戻ります。

0
saolof