モナドは通常、return
とbind
の順番で説明されます。ただし、bind
(およびjoin
?)に関してfmap
を実装することもできます。
ファーストクラスの関数が不足しているプログラミング言語では、bind
を使用するのは非常に厄介です。一方、join
は非常に簡単に見えます。
ただし、join
の仕組みを完全に理解しているとは思いません。明らかに、それは[Haskell]タイプを持っています
結合::モナドm => m(m x)-> m x
リストモナドの場合、これは簡単で明らかにconcat
です。しかし、一般的なモナドでは、操作上、このメソッドは実際に何をするのでしょうか?型シグネチャの機能はわかりますが、たとえばJavaまたは同様のもの)でこのように書く方法を理解しようとしています。
(実際、それは簡単です。私はそうしません。ジェネリックスが壊れているからです。;-)しかし、原則として、問題はまだ残っています...)
おっとっと。これは以前に尋ねられたようです:
誰かがreturn
、fmap
およびjoin
を使用して一般的なモナドのいくつかの実装をスケッチできますか? (つまり、>>=
まったく。)おそらく、それが私の愚かな脳に沈み込むのに役立つかもしれないと思います...
比喩の深みを掘り下げることなく、典型的なモナドm
を「生成する戦略」と読むことをお勧めします。したがって、型_m value
_は、「値を生成する戦略」の最初のクラスです。計算または外部の相互作用の概念が異なれば、異なるタイプの戦略が必要になりますが、一般的な概念では、意味のある規則的な構造が必要です。
return :: v -> m v
_)を生成する戦略があります。fmap :: (v -> u) -> m v -> m u
)に引き上げることができます。join :: m (m v) -> m v
)を生成する戦略を構築できます。値に至るまで。例を見てみましょう:葉ラベル付きの二分木...
_data Tree v = Leaf v | Node (Tree v) (Tree v)
_
...コインを投げて物を生産する戦略を表します。戦略が_Leaf v
_の場合、v
があります。戦略が_Node h t
_の場合、コインを投げて、戦略がh
(コインが「表」である場合はt
、「尾」である場合は続行します。
_instance Monad Tree where
return = Leaf
_
戦略を生成する戦略は、ツリーのラベルが付けられた葉を持つツリーです。このような各葉の代わりに、ラベルを付けたツリーに移植することができます...
_ join (Leaf tree) = tree
join (Node h t) = Node (join h) (join t)
_
...そしてもちろん、葉のラベルを変更するfmap
があります。
_instance Functor Tree where
fmap f (Leaf x) = Leaf (f x)
fmap f (Node h t) = Node (fmap f h) (fmap f t)
_
以下は、Int
を生成する戦略を生成する戦略です。
コインを投げる:「ヘッズ」の場合は、別のコインを投げて2つの戦略を決定します(それぞれ、「0を生産するか1を生産する」または「2を生産する」ためにコインを投げます)。それが「尾」である場合、3分の1が生成されます(「3を生成する場合はコインをトスするか、4または5の場合はコインをトスする」)。
これは明らかにjoin
sがInt
を生成する戦略を立てるまでです。
私たちが利用しているのは、「価値を生み出す戦略」それ自体が価値と見なすことができるという事実です。 Haskellでは、戦略を値として埋め込むことは静かですが、英語では、引用符を使用して、戦略の使用とそれを話すことだけを区別しています。 join
演算子は、「何らかの方法で生産してから戦略に従う」という戦略、または「もしあなたがtold戦略なら、---se itでよい」と表現します。
(メタ。この「戦略的」アプローチがモナドと値/計算の区別について考えるのに適切に一般的な方法であるかどうか、またはそれが単なる別の卑劣なメタファーであるかどうかはわかりません。 freeモナドであるため、おそらく直感のソースです。これは、モナドになるのに十分なだけの構造ですが、それ以上はありません。)
PS「バインド」のタイプ
_(>>=) :: m v -> (v -> m w) -> m w
_
「v
を生成する戦略があり、vごとにw
を生成する後続の戦略がある場合、w
を生成する戦略があります」と言います。 join
の観点からそれをどのように捉えることができますか?
_mv >>= v2mw = join (fmap v2mw mv)
_
_v2mw
_によってv
- producing戦略のラベルを変更し、各v
値の代わりに、それに続くw
- producing戦略を生成することができます— join
の準備ができました!
join = concat -- []
join f = \x -> f x x -- (e ->)
join f = \s -> let (s', f') = f s in f' s' -- State
join (Just (Just a)) = Just a; join _ = Nothing -- Maybe
join (Identity (Identity a)) = Identity a -- Identity
join (Right (Right a)) = Right a; join (Right (Left e)) = Left e;
join (Left e) = Left e -- Either
join (m, (m', a)) = (m `mappend` m', a) -- Writer
join f = \k -> f (\f' -> f' k) -- Cont
OK、あなた自身の質問に答えるのはあまり良い形ではありませんが、誰かが気づいた場合に備えて私の考えを書き留めておきます。 (疑わしい...)
モナドを「コンテナ」と見なすことができる場合、return
とjoin
はどちらもかなり明白なセマンティクスを持っています。 return
は1要素のコンテナーを生成し、join
はコンテナーのコンテナーを単一のコンテナーに変換します。それについて難しいことは何もありません。
それでは、より自然に「アクション」と考えられるモナドに焦点を当てましょう。その場合、_m x
_は、「実行」するとx
型の値を生成する何らかのアクションです。 _return x
_は特別な処理を行わず、x
を生成します。 _fmap f
_は、x
を生成するアクションを実行し、x
を計算してf
を適用するアクションを作成し、結果を返します。ここまでは順調ですね。
f
自体がアクションを生成する場合、最終的にはm (m x)
になることは明らかです。つまり、別のアクションを計算するアクションです。ある意味で、アクションを実行する_>>=
_関数や「アクションを生成する関数」などよりも、心を包み込む方が簡単かもしれません。
したがって、論理的に言えば、join
は最初のアクションを実行し、それが生成したアクションを実行してから、それを実行するようです。 (または、join
は、ヘアを分割したい場合、今説明したとおりのアクションを返します。)
それが中心的な考えのようです。 join
を実装するには、アクションを実行する必要があります。これにより、別のアクションが表示され、それを実行します。 (「実行」が何であれ、この特定のモナドにとって意味があります。)
この洞察があれば、いくつかのjoin
の実装を書くのに少し時間がかかります。
_join Nothing = Nothing
join (Just mx) = mx
_
外部アクションがNothing
の場合はNothing
を返し、それ以外の場合は内部アクションを返します。繰り返しになりますが、Maybe
はアクションというよりコンテナなので、別のことを試してみましょう...
_newtype Reader s x = Reader (s -> x)
join (Reader f) = Reader (\ s -> let Reader g = f s in g s)
_
それは...痛みがありませんでした。 Reader
は、実際にはグローバルな状態を取り、その結果のみを返す関数です。したがって、スタックを解除するには、グローバル状態を外部アクションに適用します。これにより、新しいReader
が返されます。次に、この内部関数にも状態を適用します。
ある意味で、それはおそらく通常の方法より簡単です。
_Reader f >>= g = Reader (\ s -> let x = f s in g x)
_
さて、どちらがリーダー関数で、どれが次のリーダーを計算する関数ですか...?
では、古き良きState
モナドを試してみましょう。ここで、すべての関数は初期状態を入力として受け取りますが、出力とともに新しい状態も返します。
_data State s x = State (s -> (s, x))
join (State f) = State (\ s0 -> let (s1, State g) = f s0 in g s1)
_
それはそれほど難しくありませんでした。それは基本的に実行され、続いて実行されます。
入力をやめます。私の例のすべての不具合とタイプミスを指摘してください...:-/
モナドについて「カテゴリ理論について何も知る必要はありません。実際には、モナドをブリトー/宇宙服/なんでも考えてみてください」という説明がたくさんあります。
本当に、私にとってモナドを分かりやすく説明した記事は、カテゴリが何であるかを述べ、モナド(結合とバインドを含む)をカテゴリの観点から説明しており、偽のメタファーに悩まされていませんでした。
数学の知識があまりなくても、記事はとても読みやすいと思います。
fmap (f :: a -> m b) (x ::
を呼び出していますm
_a)
_は値を生成します_(y ::
_m
_(m b))
_したがって、join
を使用して値_(z :: m b)
_を取得するのは非常に自然なことです。
次に、bindは単にbind ma f = join (fmap f ma)
として定義されているため、_(:: a -> m b)
_多様な関数の Kleisly構成性 を実現しています。
_ma `bind` (f >=> g) = (ma `bind` f) `bind` g -- bind = (>>=)
= (`bind` g) . (`bind` f) $ ma
= join . fmap g . join . fmap f $ ma
_
したがって、flip bind = (=<<)
を使用すると、 we have
_ ((g <=< f) =<<) = (g =<<) . (f =<<) = join . (g <$>) . join . (f <$>)
_
Haskell doesの型シグネチャを尋ねるのは、Java does。
それは、文字通りの意味では「しない」です。 (もちろん、通常は、それに関連付けられたある種の目的がありますが、それは主にあなたの頭の中にあり、ほとんどの場合実装にはありません。)
どちらの場合も、後の定義で使用される言語でシンボルの有効なシーケンスを宣言します。
もちろん、Javaでは、インターフェイスはVMに文字通り実装される型シグネチャに対応すると言えます。このようにしてポリモーフィズムを取得できます。インターフェースを受け入れる名前を定義し、別のインターフェースを受け入れる名前に別の定義を提供できます。 Haskellでも同様のことが起こり、1つのタイプを受け入れる名前の宣言を提供し、次に、別のタイプを処理するその名前の別の宣言を提供できます。