web-dev-qa-db-ja.com

Haskellモナドバインド演算子の混乱

さて、私はHaskellプログラマーではありませんが、Haskellの背後にある多くのアイデアに絶対に興味をそそられ、それを学ぶことを検討しています。しかし、私は正直なところに固執しています。かなり基本的なように見えるモナドに頭を包むことができないようです。 SOモナドの説明を求める質問が100万件あることは知っているので、私を悩ませているものについてもう少し具体的に説明します。

この素晴らしい記事( Javascriptの紹介 )を読んで、モナドを完全に理解したと思いました。それから私はモナドのウィキペディアのエントリを読んで、これを見ました:

Haskellが中置演算子>> =で表す、ポリモーフィック型(M t)→(t→M u)→(M u)のバインディング操作。 その最初の引数はモナド型の値であり、その2番目の引数は最初の引数の基になる型から別のモナド型にマップする関数であり、その結果は他のモナド型になります。

さて、私が引用した記事では、bindは1つの引数のみを取る関数でした。ウィキペディアは2つと言っています。私が考えたモナドについて理解したのは次のとおりです。

  1. モナドの目的は、さまざまな入力タイプと出力タイプを持つ関数を取得し、それを構成可能にすることです。これは、入力タイプと出力タイプを単一のモナドタイプでラップすることによって行われます。
  2. モナドは、バインドとユニットという2つの相互に関連する関数で構成されています。 Bindは、構成不可能な関数fを受け取り、入力としてモナド型を受け入れ、モナド型を返す新しい関数gを返します。 gは構成可能です。単位関数は、fが期待する型の引数を取り、それをモナド型でラップします。次に、これをg、またはgなどの関数の任意の合成に渡すことができます。

しかし、バインドの概念には1つの引数、つまり関数が含まれているため、何か問題があるはずです。しかし(ウィキペディアによると)Haskellのバインドは実際には2つの引数を取ります!私の間違いはどこにありますか?

32
Ord

あなたは間違いを犯していません。ここで理解する重要なアイデアはカリー化です-2つの引数のHaskell関数は2つの方法で見ることができます。 1つ目は、単に2つの引数の関数です。たとえば、(+)がある場合、これは通常、2つの引数を取り、それらを追加するものと見なされます。それを見る別の方法は、加算機のプロデューサーとしてです。 (+)は、xなどの数値を取り、xを追加する関数を作成する関数です。

(+) x = \y -> x + y
(+) x y = (\y -> x + y) y = x + y

モナドを扱うときは、前述のように、=<<の反転バージョンである>>=を考える方がよい場合があります。これを確認する方法は2つあります。

(=<<) :: (a -> m b) -> m a -> m b

これは2つの引数の関数であり、

(=<<) :: (a -> m b) -> (m a -> m b)

これは、前述の記事のように、入力関数を簡単に構成できるバージョンに変換します。これらは、前に説明した(+)と同じです。

25
gereeter

モナドについてのあなたの信念を壊させてください。私が失礼なことをしようとしているのではないことをあなたが理解してくれることを心から願っています。私は単に言葉を細かく刻むのを避けようとしています。

モナドの目的は、さまざまな入力タイプと出力タイプを持つ関数を取得し、それを構成可能にすることです。これは、入力タイプと出力タイプを単一のモナドタイプでラップすることによって行われます。

ではない正確に。 「モナドの目的」で文を始めるとき、あなたはすでに間違った立場にいます。モナドは必ずしも「目的」を持っているわけではありません。 Monadは単なる抽象化であり、特定のタイプに適用され、他のタイプには適用されない分類です。 Monad抽象化の目的は、単にその抽象化です。

モナドは、バインドとユニットという2つの相互に関連する関数で構成されています。

はいといいえ。モナドを定義するには、bindunitの組み合わせで十分ですが、joinfmap、およびunitの組み合わせも同様です。十分。後者は、実際、モナドが圏論で一般的に説明されている方法です。

Bindは、構成不可能な関数fを受け取り、入力としてモナド型を受け入れ、モナド型を返す新しい関数gを返します。

繰り返しますが、正確ではありません。モナド関数f :: a -> m bは、特定のタイプで完全に構成可能です。関数g :: m b -> cを使用してポストコンポジションしてg . f :: a -> cを取得するか、関数h :: c -> aを使用してプリコンポジションしてf . h :: c -> m bを取得できます。

しかし、2番目の部分は完全に正しいです:(>>= f) :: m a -> m b。他の人が指摘しているように、Haskellのbind関数は逆の順序で引数を取ります。

gは構成可能です。

はい、そうです。 g :: m a -> m bの場合は、関数f :: c -> m aを使用して事前に作成してg . f :: c -> m bを取得するか、関数h :: m b -> cを使用して事後作成してh . g :: m a -> cを取得できます。 ccouldm vの形式であることに注意してください。ここで、mはモナドです。 「コンポーザブル」とは、「この形の関数のチェーンを任意に長く作れる」という意味だと思いますが、それは事実です。

単位関数は、fが期待する型の引数を取り、それをモナド型でラップします。

回りくどい言い方ですが、そうです、その通りです。

この[unitをある値に適用した結果]は、g、またはgのような関数の任意の合成に渡すことができます。

繰り返しますが、はい。一般に、unit(またはHaskellではreturn)を呼び出して、それを(>>= f)に渡すのは慣用的なHaskellではありません。

-- instead of
return x >>= f >>= g
-- simply go with
f x >>= g

-- instead of
\x -> return x >>= f >>= g
-- simply go with
f >=> g
-- or
g <=< f
20
Dan Burton

リンクする記事は、バインドの定義を反転させたsigfpeの記事に基づいています。

まず、bindの定義を反転して、Word'bind 'として記述しましたが、通常は演算子>>=として記述しました。したがって、bind f xは通常x >>= fと表記されます。

したがって、Haskell bindはモナドで囲まれた値を取り、関数を返します。関数は関数を受け取り、抽出された値でそれを呼び出します。私は不正確な用語を使用している可能性があるので、コードの方が良いかもしれません。

あなたが持っている:

sine x = (sin x,     "sine was called.")
cube x = (x * x * x, "cube was called.")

ここで、JSバインドを変換します(Haskellは自動カリー化を行うため、bind fを呼び出すとタプルを受け取る関数が返され、パターンマッチングによってxsにアンパックされます。 、私はそれが理解できることを願っています):

bind f (x, s) = (y, s ++ t)
                where (y, t) = f x

あなたはそれが機能しているのを見ることができます:

*Main> :t sine
sine :: Floating t => t -> (t, [Char])
*Main> :t bind sine
bind sine :: Floating t1 => (t1, [Char]) -> (t1, [Char])
*Main> (bind sine . bind cube) (3, "")
(0.956375928404503,"cube was called.sine was called.")

それでは、bindの引数を逆にしましょう。

bind' (x, s) f = (y, s ++ t)
                 where (y, t) = f x

まだ同じことをしていることがはっきりとわかりますが、構文が少し異なります。

*Main> bind' (bind' (3, "") cube) sine
(0.956375928404503,"cube was called.sine was called.")

現在、Haskellには、任意の関数を中置演算子として使用できる構文トリックがあります。だからあなたは書くことができます:

*Main> (3, "") `bind'` cube `bind'` sine
(0.956375928404503,"cube was called.sine was called.")

bind'の名前を>>=(3, "") >>= cube >>= sine)に変更すると、探していたものが得られます。ご覧のとおり、この定義を使用すると、個別の合成演算子を効果的に取り除くことができます。

新しいものをJavaScriptに戻すと、次のようになります(ここでも、引数の順序を逆にするだけです)。

var bind = function(Tuple) {
    return function(f) {
        var x  = Tuple[0],
            s  = Tuple[1],
            fx = f(x),
            y  = fx[0],
            t  = fx[1];

        return [y, s + t];
    };
};

// ugly, but it's JS, after all
var f = function(x) { return bind(bind(x)(cube))(sine); }

f([3, ""]); // [0.956375928404503, "cube was called.sine was called."]

これがお役に立てば幸いですが、混乱が生じることはありません。重要なのは、これら2つのバインド定義は同等であり、呼び出し構文のみが異なるということです。

9
Cat Plus Plus