197

最近、モナドについて多くの話があります。私はいくつかの記事/ブログ投稿を読みましたが、概念を完全に理解するためにそれらの例を十分に理解することはできません。その理由は、モナドは関数型言語の概念であり、したがって、例は私が使用したことのない言語であるためです(私は関数型言語を深く使用していないため)。記事を完全に理解するのに十分なほど構文を深く理解することはできません...しかし、そこには理解する価値のある何かがあると言えます。

ただし、ラムダ式やその他の機能機能を含め、C#についてはよく知っています。C#には機能機能のサブセットしかないため、モナドをC#で表現できない可能性があります。

しかし、確かにそのコンセプトを伝えることは可能ですか?少なくとも私はそう願っています。たぶん、C#の例を基礎として提示し、C#開発者がそこからできることを望んでいるが、言語には関数型プログラミング機能がないためにできないことを説明することができます。モナドの意図と利点を伝えるので、これは素晴らしいでしょう。だからここに私の質問があります:あなたがC#3開発者にモナドについて与えることができる最も良い説明は何ですか?

ありがとう!

(編集:ちなみに、SOにはすでに少なくとも3つの「モナドとは何か」の質問があることを知っています。しかし、私はそれらについて同じ問題に直面しています...したがって、C#開発者のためにこの質問が必要です。フォーカス。ありがとう。)

4

6 に答える 6

154

プログラミングで一日中行うことのほとんどは、いくつかの関数を組み合わせて、それらからより大きな関数を構築することです。通常、ツールボックスには関数だけでなく、演算子、変数の割り当てなどの他のものもありますが、通常、プログラムは多くの「計算」を結合して、さらに結合されるより大きな計算にします。

モナドは、この「計算の組み合わせ」を行うための何らかの方法です。

通常、2 つの計算を組み合わせる最も基本的な「演算子」は次の;とおりです。

a; b

あなたがこれを言うとき、あなたは「まずやるa、それからやるb」という意味です。結果a; bは基本的に、より多くのものと組み合わせることができる計算です。これは単純なモナドで、小さな計算を大きな計算に結合する方法です。「;左のことをしてから、右のことをしなさい」と言います。

オブジェクト指向言語でモナドと見なされるもう 1 つのものは、.. 多くの場合、次のようなものを見つけます。

a.b().c().d()

基本的には、.「左側の計算を評価し、その結果に対して右側のメソッドを呼び出す」ことを意味します。これは、関数/計算を組み合わせる別の方法であり、 よりも少し複雑です;。そして、物事を連鎖させるという概念は.モナドです。これは、2 つの計算を組み合わせて新しい計算にする方法だからです。

特別な構文を持たない別のかなり一般的なモナドは、次のパターンです。

rv = socket.bind(address, port);
if (rv == -1)
  return -1;

rv = socket.connect(...);
if (rv == -1)
  return -1;

rv = socket.send(...);
if (rv == -1)
  return -1;

戻り値 -1 は失敗を示しますが、この方法で組み合わせる必要のある API 呼び出しが多数ある場合でも、このエラー チェックを抽象化する実際の方法はありません。これは基本的に、「左側の関数が -1 を返した場合、自分自身で -1 を返し、それ以外の場合は右側の関数を呼び出す」という規則によって関数呼び出しを組み合わせたもう 1 つのモナドです。このことを行うオペレーター>>=があれば、次のように簡単に書くことができます。

socket.bind(...) >>= socket.connect(...) >>= socket.send(...)

これにより、物事が読みやすくなり、関数を組み合わせる特別な方法を抽象化するのに役立ちます。これにより、何度も何度も繰り返す必要がなくなります。

また、一般的なパターンとして有用であり、モナドで抽象化できる関数/計算を組み合わせる方法は他にもたくさんあります。モナドのユーザーは、より簡潔で明確なコードを書くことができます。使用される関数はモナドで行われます。

たとえば、上記>>=は「エラー チェックを実行し、入力として取得したソケットの右側を呼び出す」ように拡張できるためsocket、何度も明示的に指定する必要はありません。

new socket() >>= bind(...) >>= connect(...) >>= send(...);

ある関数の結果を次の関数への入力として取得する方法を心配する必要があるため、正式な定義はもう少し複雑です。その関数がその入力を必要とする場合、および結合する関数が適合することを確認する必要があるためです。モナドでそれらを結合しようとする方法。しかし、基本的な概念は、関数を組み合わせるさまざまな方法を形式化することです。

于 2009-03-23T21:15:36.313 に答える
47

この質問を投稿してから1年が経ちました。それを投稿した後、私は Haskell を数か月掘り下げました。私はそれを大いに楽しんだが、モナドを掘り下げる準備ができたので、それを脇に置いた。私は仕事に戻り、プロジェクトに必要なテクノロジーに集中しました。

そして昨夜、私は来て、これらの回答を読み直しました。最も重要なことは、誰かが上で言及した Brian Beckman のビデオのテキスト コメントにある特定の C# の例を読み直したということです。それは完全に明確で明快だったので、ここに直接投稿することにしました.

このコメントのおかげで、モナドが何であるかを正確に理解しているように感じるだけでなく、モナドである何かを C# で実際に書いたことがあることに気付きました…または少なくとも非常に近く、同じ問題を解決しようと努力しています。

それで、ここにコメントがあります – これはすべて、sylvanによるコメントからの直接の引用です。

これはかなりクールです。少し抽象的ですが。実際の例がないため、モナドが何であるかを知らない人はすでに混乱していると想像できます。

ですから、従うようにしましょう。明確にするために、C# で例を示しますが、見栄えは悪くなります。同等の Haskell を最後に追加し、モナドが本当に便利になり始めるクールな Haskell シンタックス シュガーを紹介します。

さて、最も簡単なモナドの 1 つは、Haskell では「Maybe モナド」と呼ばれます。C# では、Maybe 型は と呼ばれNullable<T>ます。これは基本的に、有効で値を持つ値、または「null」で値を持たない値の概念をカプセル化するだけの小さなクラスです。

この型の値を結合するためにモナド内に固執するのに役立つのは、失敗の概念です。つまり、複数の null 許容値を見てnull、それらのいずれかが null になるとすぐに戻ることができるようにしたいのです。これは、たとえば、辞書などで多くのキーを検索し、最後にすべての結果を処理して何らかの方法で結合したい場合に役立ちますが、いずれかのキーが辞書にない場合、あなたnullはすべてのために戻りたいです。ルックアップとリターンのそれぞれを手動でチェックしなければならないのは面倒な nullので、このチェックを bind 演算子の内部に隠すことができます (これは一種のモナドのポイントです。bind 演算子に簿記を隠して、コードを簡単に実行できるようにします)。詳細を忘れることができるので使用してください)。

これが、全体の動機となるプログラムです (後で定義します Bind。これは、なぜそれが優れているかを示すためのものです)。

 class Program
    {
        static Nullable<int> f(){ return 4; }        
        static Nullable<int> g(){ return 7; }
        static Nullable<int> h(){ return 9; }


        static void Main(string[] args)
        {
            Nullable<int> z = 
                        f().Bind( fval => 
                            g().Bind( gval => 
                                h().Bind( hval =>
                                    new Nullable<int>( fval + gval + hval ))));

            Console.WriteLine(
                    "z = {0}", z.HasValue ? z.Value.ToString() : "null" );
            Console.WriteLine("Press any key to continue...");
            Console.ReadKey();
        }
    }

ここで、NullableC# でこれを行うためのサポートが既にあることをしばらく無視してください (null 許容 int を一緒に追加すると、どちらかが null の場合は null になります)。そのような機能はなく、特別な魔法のない単なるユーザー定義クラスであるとしましょう。要点は、関数を使用しBindて変数を値の内容にバインドし、Nullable何も変わっていないふりをして、通常の int のように使用し、それらを足し合わせることができるということです。結果を最後に nullable でラップします。その nullable は null ( の場合fgまたはnull を返す) か、 、、およびhを合計した結果になります。fgh一緒。(これは、データベース内の行を LINQ 内の変数にバインドし、それを処理する方法に似ています。Bindオペレーターが変数に有効な行値のみが渡されることを確認することを知っていれば安全です)。

これで遊んでf、 、g、およびのいずれかを変更hして null を返すと、全体が null を返すことがわかります。

そのため、バインド オペレーターがこのチェックを行い、null 値に遭遇した場合は null を返す必要があり、それ以外の場合はNullable構造内の値をラムダに渡す必要があります。

Bind演算子は次のとおりです。

public static Nullable<B> Bind<A,B>( this Nullable<A> a, Func<A,Nullable<B>> f ) 
    where B : struct 
    where A : struct
{
    return a.HasValue ? f(a.Value) : null;
}

ここのタイプはビデオのようです。(この場合の C# 構文では) と関数 from to ( C# 構文では)を取り、M a ( )を返します。Nullable<A>aM bFunc<A, Nullable<B>>M bNullable<B>

このコードは、nullable に値が含まれているかどうかを単純にチェックし、含まれている場合はそれを抽出して関数に渡し、そうでない場合は単に null を返します。これは、Bindオペレータがすべての null チェック ロジックを処理することを意味します。呼び出す値が null でない場合にのみ、その値 Bindはラムダ関数に「渡され」ます。それ以外の場合は、早期に救済され、式全体が null になります。これにより、モナドを使用して記述したコードは、この null チェック動作から完全に解放されますBind。モナド値内の値にバインドされた変数を使用して取得するだけで (コード例ではfval, )、それらを安全に使用できます。それらを渡す前に null のチェックを処理する知識の中で。gvalhvalBind

モナドでできることの例は他にもあります。たとえば、Bindオペレーターに文字の入力ストリームを処理させ、それを使用してパーサー コンビネーターを記述することができます。各パーサー コンビネーターは、バックトラッキングやパーサーの失敗などに完全に気付かず、小さなパーサーを組み合わせるだけで問題が起こらないかのようにできますBind。難しいビット。その後、誰かがモナドにロギングを追加するかもしれませんが、モナドを使用するコードは変更されません。すべての魔法はBind演算子の定義で発生し、残りのコードは変更されないためです。

最後に、Haskell での同じコードの実装を次に示します (-- コメント行で始まります)。

-- Here's the data type, it's either nothing, or "Just" a value
-- this is in the standard library
data Maybe a = Nothing | Just a

-- The bind operator for Nothing
Nothing >>= f = Nothing
-- The bind operator for Just x
Just x >>= f = f x

-- the "unit", called "return"
return = Just

-- The sample code using the lambda syntax
-- that Brian showed
z = f >>= ( \fval ->
     g >>= ( \gval ->  
     h >>= ( \hval -> return (fval+gval+hval ) ) ) )

-- The following is exactly the same as the three lines above
z2 = do 
   fval <- f
   gval <- g
   hval <- h
   return (fval+gval+hval)

ご覧のとおりdo、末尾の素敵な表記により、まっすぐな命令型コードのように見えます。実際、これは設計によるものです。モナドは、命令型プログラミング (変更可能な状態、IO など) ですべての有用なものをカプセル化するために使用でき、この素晴らしい命令型のような構文を使用して使用できますが、カーテンの後ろでは、すべてモナドであり、bind 演算子の巧妙な実装です! クールなのは、 and を実装することで独自のモナドを実装できること>>=ですreturn。そうすれば、それらのモナドもdo表記法を使用できるようになります。つまり、基本的には、2 つの関数を定義するだけで、独自の小さな言語を作成できます。

于 2010-03-20T17:56:47.843 に答える
11

モナドは本質的に遅延処理です。副作用(I / Oなど)のあるコードを、それらを許可せず、純粋な計算のみを許可する言語で記述しようとしている場合、1つの回避策は、「わかりました。副作用は発生しないことを知っています。私にとっては、でも、そうしたらどうなるか計算していただけませんか?」

それは一種の浮気です。

さて、その説明はモナドの全体像を理解するのに役立ちますが、悪魔は詳細にあります。結果をどの程度正確に計算ますか?時々、それはきれいではありません。

誰かが命令型プログラミングに慣れている方法の概要を説明する最良の方法は、DSLに入れて、モナドの外で慣れているものと構文的に似ている操作を使用して、実行する関数を作成することです。 (たとえば)出力ファイルに書き込むことができれば、何が必要か。後で評価するために文字列でコードを構築しているかのように(実際にはそうではありませんが)ほとんどです。

于 2009-03-23T19:25:31.477 に答える
4

他のユーザーが詳細に投稿することは間違いありませんが、このビデオはある程度役に立ちましたが、私はまだ解決を開始できる(またはすべきである)という概念に堪能ではないと言いますモナドで直感的に問題。

于 2009-03-23T19:25:49.837 に答える
0

モナドは、クラスが実装しなければならないC#interfaceと考えることができます。これは、インターフェイスでこれらの宣言を選択する理由の背後にあるすべてのカテゴリの理論的な数学を無視し、副作用を回避しようとする言語でモナドを使用するすべての理由を無視する実用的な答えです。しかし、(C#) インターフェイスを理解している人としては良いスタートだと思いました。

于 2009-03-23T19:36:10.937 に答える