C#では、モナドとは何ですか?


189

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

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

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

ありがとう!

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


これは実際にはC#3.0開発者であることに注意してください。.NET 3.5と間違えないでください。それ以外に、いい質問です。
Razzie、

4
LINQクエリ式は、C#3にモナド行動の一例であることを指摘し、それの価値
エリック・フォーブス

1
私はまだそれは重複した質問だと思います。stackoverflow.com/questions/2366/can-anyone-explain-monadsの回答の1つがchannel9vip.orcsweb.com/shows/Going+Deep/…にリンクし ています。コメントの1つにC#の非常に優れた例があります。:)
2009年

4
それでも、それはSOの質問の1つに対する1つの回答からの1つのリンクにすぎません。C#開発者に焦点を当てた質問に価値があると思います。これは、C#を使用していた関数型プログラマーに知っている人に聞いたものです。しかし、私はあなたの意見に対するあなたの権利も尊重します。
チャーリー花

1
ただし、必要な答えは1つではありませんか?;)私のポイントは、他の質問の1つ(そして今度はこの質問もそうです)がC#固有の回答を持っているということです(これは実際に非常によく記述されているようです。おそらく、私が見た中で最良の説明です)
jalf

回答:


147

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

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

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

a; b

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

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

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

.基本的には「左の計算を評価し、その結果に右側のメソッドを呼び出す」を意味します。これは、関数/計算を組み合わせるもう1つの方法で、より少し複雑です;。そして、物事を連鎖させるという概念は.モナドです。これは、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(...);

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


28
正解です。オリバースティールのモナドをC ++またはC#のオペレーターオーバーロードに関連付けようとして、引用を投げかけます。モナドでは、 ';'をオーバーロードできます。オペレーター。
イェルクWミッターク

6
@JörgWMittag私は以前その引用を読みましたが、それは過度に頭がおかしいように聞こえました。モナドを理解し、この「;」の説明を読みました。1つです。わかりました。しかし、私はそれがほとんどの命令型開発者にとって本当に不合理な発言だと思います。「;」//がほとんどである以上、演算子として見なされません。
ジミーHoffa

2
モナドとは何か知っていますか?モナドは「関数」でも計算でもありません。モナドにはルールがあります。
Luis

あなたの;例では:どのオブジェクト/データ型が;マップされますか?(にListマップTすると思いますList<T>;オブジェクト/データ型間の射/関数をどのようにマップしますか?何がpurejoinbindのために;
Micha Wiedenmann、

44

この質問を投稿してから1年になります。投稿した後、数か月かけてHaskellについて詳しく調べました。ものすごく楽しかったですが、モナドを掘り下げる準備ができたのと同じように、脇に置いておきました。私は仕事に戻り、私のプロジェクトに必要なテクノロジーに集中しました。

そして昨夜、私はこれらの応答を読み、再度読みました。最も重要なの、ブライアンベックマンのビデオの誰かのテキストコメントの特定のC#の例もう一度読んだことです。上記の言及を。私はそれをここに直接投稿することにしました。

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

だから、ここにコメントがあります-これはすべて、シルバンによるコメントの直接の引用ですです:

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

それで、準拠するようにしましょう。見やすくするために、C#で例を示します。最後に同等のHaskellを追加して、IMO、モナドが本当に役立つようになる、かっこいいHaskell構文糖を示します。

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

このタイプの値を組み合わせるためにモナド内に固執するのに役立つのは、失敗の概念です。つまり、複数のnull許容値を調べてnull、いずれか1つが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();
        }
    }

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

あなたはこれで遊ぶとのいずれかを変更することができfgおよびhヌルを返すために、あなたは全部が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 aNullable<A>この場合のC#構文で)、およびから関数aM bFunc<A, Nullable<B>>C#構文で)、それは返しM bNullable<B>)を。

コードは単にnullableに値が含まれているかどうかをチェックし、含まれている場合はそれを抽出して関数に渡し、それ以外の場合はnullを返します。これは、Bindオペレーターがすべてのnullチェックロジックを処理することを意味します。呼び出すBind値がnull以外の場合に限り、その値 はラムダ関数に「渡され」、それ以外の場合は早期に救済され、式全体がnullになります。これにより、モナドを使用して作成するコードにこのnullチェック動作を完全に適用せずBind、モナディック値(fvalgvalおよびhvalサンプルコード内)の値にバインドされた変数を使用して取得するだけで、それらを安全に使用できます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)

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


3
個人的にはF#版のモナドが好きですが、どちらの場合も素晴らしいです。
ChaosPandion

3
ここに戻って投稿を更新していただきありがとうございます。このようなフォローアップは、特定の領域を調べているプログラマーが、単に「どうやってx in yテクノロジーを実行するか」を決めるのではなく、他のプログラマーが最終的にその領域をどのように認識するかを理解するのに役立ちます。あなたダマン!
kappasims

私はあなたが基本的に持っているのと同じ道をたどり、モナドを理解する同じ場所に到着しました、これは私が命令型開発者のために今まで見たモナドのバインディング動作の最も良い説明であると言いました。私はあなたがモナドについてすべてに触れているわけではないと思いますが、これはsthで少し詳しく説明されています。
ジミー・ホッファ

@Jimmy Hoffa-間違いなくあなたが正しい。私はそれらをより深く理解するために、最良の方法はそれらをたくさん使い始めて経験を積むことです。私はまだその機会がありませんでしたが、すぐに望んでいます。
チャーリーフラワーズ

モナドはプログラミングの点でより高いレベルの抽象化であるか、または数学における継続的で微分不可能な関数の定義にすぎないようです。いずれにせよ、それらは、特に数学において、新しい概念ではありません。
リャン2013

11

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

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

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

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


1
I Robotの本のように?科学者がコンピュータに宇宙旅行を計算し、特定のルールをスキップするように依頼する場所はどこですか?:):) :) :)
OscarRyz 2009年

3
うーん、モナド遅延処理と副作用機能をカプセル化するために使用できます。実際、それはHaskellでの最初の実際のアプリケーションでしたが、実際にははるかに一般的なパターンです。その他の一般的な用途には、エラー処理と状態管理が含まれます。構文糖質(Haskellで実行、F#で計算式、C#でLinq構文)は単なるモナドの基本であり、そのようなものです。
Mike Hadlow、2011

@MikeHadlow:エラー処理(MaybeおよびEither e)および状態管理(State sST s)のモナドインスタンスは、「[私にとっての副作用]を実行した場合の結果を計算してください」の特定のインスタンスとして私を襲います。別の例は、非決定論です([])。
ピヨン2015年

これはまさに正しいです。各「モナディック」値は「純粋な」言語自体の有効な値であり、潜在的に不純な「計算」を表すため、E DSL、つまり埋め込み DSL であるという1つ(2つ)の追加があります。さらに、純粋な言語にはモナディック「バインド」構造が存在するため、純粋な言語を連鎖させることができます。、結合された計算全体が「実行」されたときに、それぞれの値がその前の計算の結果で呼び出されるような値のコンストラクター。これは、将来の結果(またはいずれの場合でも、別個の「実行」タイムライン)で分岐できることを意味します。
ネス

プログラマにとっては、純粋な言語の純粋な計算と混合しながら、EDSLでプログラミングできることを意味します。多層サンドイッチのスタックは多層サンドイッチです。それはそのシンプル。
ネスは

4

他のユーザーが詳細に投稿すると確信していますが、このビデオはある程度役に立ったと思いますが、まだ解決できる(または開始すべき)というコンセプトについてはまだ流暢ではありません。モナドで直感的に問題。


1
さらに参考になったのは、ビデオの下にC#の例を含むコメントです。
09年

私はもっと役立つことについては知りませんが、それは確かにアイデアを実践に移しました。
TheMissingLINQ 2009年

0

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


詳しく説明できますか?それをモナドに関連付けるインターフェースについてはどうですか?
Joel Coehoorn 2009年

2
ブログの投稿は、その質問に費やされるいくつかの段落を費やしていると思います。
hao

0

「モナドとは」への私の答えを見てください。

それはやる気を起こさせる例から始まり、例を通して働き、モナドの例を導き出し、正式に「モナド」を定義します。

関数型プログラミングの知識がないことを前提としており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);
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.