web-dev-qa-db-ja.com

C#開発者に理解を助ける:モナドとは何ですか?

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

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

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

ありがとう!

編集フォーカス。ありがとう。)

183
Charlie Flowers

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

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

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

a; b

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

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

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

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

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

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を返し、それ以外の場合は右側の関数を呼び出す」というルールによる関数呼び出しを組み合わせた別のモナドです。このことを行う演算子>>=があれば、次のように書くことができます。

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

それは物事をより読みやすくし、関数を結合する特別な方法を抽象化するのに役立ちますので、何度も自分自身を繰り返す必要はありません。

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

たとえば、上記の>>=を拡張して「エラーチェックを行い、入力として取得したソケットの右側を呼び出す」ことで、socketロットを明示的に指定する必要がなくなります。時の:

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

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

146
sth

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

そして昨夜、私は来てこれらの回答を読み直しました。 最も重要なこと、読み直し 特定のC#の例 のテキストコメントで ブライアンベックマンの動画 誰か 上記の説明 =。それは非常に完全で明確であったため、ここに直接投稿することにしました。

このコメントのために、私はexactly Monadsが何であるかを理解しているように感じるだけでなく、実際にC#でare Monads…または少なくとも非常に近く、同じ問題の解決に努めています。

だから、ここにコメントがあります。これは、すべて the comment here by sylvanからの直接引用です

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

それで、私はそれを順守しようとし、本当にはっきりさせるために、C#で例を見ます。同等のHaskellを最後に追加し、IMOのモナドが本当に有用になり始めるクールなHaskell構文糖を示します。

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

このタイプの値を結合するためにモナド内に固執する便利なことは、失敗の概念です。つまり複数のNULL値を許可し、それらのいずれかがNULLになるとすぐにnullを返すことができるようにしたいのです。これは、たとえば、辞書などで多くのキーを検索し、最後にすべての結果を処理して何らかの方法で結合したい場合に役立ちますが、いずれかのキーが辞書にない場合は、全体に対してnullを返します。 nullの各ルックアップを手動でチェックして返す必要があるため、バインド演算子内でこのチェックを非表示にできます(モナドのポイントのように、バインド演算子でブックキーピングを非表示にします)詳細を忘れてしまう可能性があるため、コードが使いやすくなります)。

全体を動機付けるプログラムを次に示します(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();
        }
    }

ここで、C#のNullableに対してこれを行うためのサポートが既にあることをしばらく無視します(null可能なintを一緒に追加でき、どちらかがnullの場合はnullを取得します)。そのような機能はなく、特別な魔法のないユーザー定義のクラスであると仮定しましょう。ポイントは、Bind関数を使用して変数をNullable値のコンテンツにバインドし、何も変わっていないふりをして、通常のintのように使用してそれらを加算することができるということです。 。結果を最後にnullableにラップし、そのnullableはnull(fg、またはhのいずれかがnullを返す場合)または加算の結果になります。 fg、およびhを一緒に。 (これは、データベースの行をLINQの変数にバインドし、それを処理する方法に似ています。Bind演算子は、変数が有効なもののみを渡すことを保証するため、安全です。行の値)。

これで遊んで、fg、および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;
}

ここのタイプは、ビデオのようです。 M a(この場合はC#構文のNullable<A>)、およびaからM bの関数(C#構文のFunc<A, Nullable<B>>)、およびM bNullable<B>)を返します。

コードは、nullableに値が含まれているかどうかを確認し、値が含まれている場合はそれを抽出して関数に渡します。これは、Bind演算子がすべてのnullチェックロジックを処理することを意味します。 Bind onを呼び出す値がnull以外の場合にのみ、その値はラムダ関数に「渡され」、それ以外の場合は早期に救済され、式全体がnullになります。これにより、モナドを使用して記述するコードは、このnullチェック動作から完全に自由になります。Bindを使用し、モナド値(fvalgvalhvalはサンプルコードで)、それらを渡す前にBindがnullのチェックを処理するという知識で安全に使用できます。

モナドでできることの他の例があります。たとえば、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)

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

42
Charlie Flowers

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

それは一種の不正行為です。

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

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

11
MarkusQ

他のユーザーが詳細に投稿すると確信していますが、 このビデオ はある程度役立つと思いましたが、 Monadsで直観的に問題の解決を開始できる(またはすべき)。

4
TheMissingLINQ

answer 「モナドとは」を参照してください。

動機付けの例から始まり、例を通して働き、モナドの例を導き出し、正式に「モナド」を定義します。

関数型プログラミングの知識がないことを前提とし、function(argument) := expression構文で可能な限り単純な式で擬似コードを使用します。

このC#プログラムは、擬似コードモナドの実装です。 (参照:Mは型コンストラクター、feedは「バインド」操作、wrapは「リターン」操作です。)

using System.IO;
using System;

class Program
{
    public class M<A>
    {
        public A val;
        public string messages;
    }

    public static M<B> feed<A, B>(Func<A, M<B>> f, M<A> x)
    {
        M<B> m = f(x.val);
        m.messages = x.messages + m.messages;
        return m;
    }

    public static M<A> wrap<A>(A x)
    {
        M<A> m = new M<A>();
        m.val = x;
        m.messages = "";
        return m;
    }

    public class T {};
    public class U {};
    public class V {};

    public static M<U> g(V x)
    {
        M<U> m = new M<U>();
        m.messages = "called g.\n";
        return m;
    }

    public static M<T> f(U x)
    {
        M<T> m = new M<T>();
        m.messages = "called f.\n";
        return m;
    }

    static void Main()
    {
        V x = new V();
        M<T> m = feed<U, T>(f, feed(g, wrap<V>(x)));
        Console.Write(m.messages);
    }
}
0
Jordan

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

0
hao