平易な英語でモナドとは?(FP の知識のない OOP プログラマ向け)質問する

平易な英語でモナドとは?(FP の知識のない OOP プログラマ向け)質問する

OOP プログラマーが理解できる言葉で言えば (関数型プログラミングのバックグラウンドがなくても)、モナドとは何でしょうか?

それはどのような問題を解決し、最もよく使用される場所はどこですか?

アップデート

私が求めていた理解を明確にするために、モナドを持つ FP アプリケーションを OOP アプリケーションに変換するとします。モナドの役割を OOP アプリケーションに移植するにはどうすればよいでしょうか。

ベストアンサー1

更新:この質問は非常に長いブログシリーズのテーマでした。こちらで読むことができます。モナド— 素晴らしい質問をありがとう!

OOP プログラマーが理解できる言葉で言えば (関数型プログラミングのバックグラウンドがなくても)、モナドとは何でしょうか?

モナドは、特定のルールに従い特定の操作が提供される型の「増幅器」です。

まず、「型の増幅器」とは何でしょうか。それは、型を取得して、それをより特殊な型に変換できるシステムのことです。たとえば、C# では、 を考えてみましょうNullable<T>。これは型の増幅器です。これにより、たとえば という型を取得してint、その型に新しい機能、つまり、以前は null にできなかったものを null にできる機能を追加できます。

2 番目の例として、 を考えてみましょうIEnumerable<T>。これは型の増幅器です。 を使用すると、たとえば型 を取得し、stringその型に新しい機能、つまり任意の数の単一文字列から文字列のシーケンスを作成できる機能を追加できます。

「一定のルール」とは何でしょうか?簡単に言うと、基底型の関数が増幅された型で機能し、関数合成の通常のルールに従うような合理的な方法があるということです。たとえば、整数の関数があるとします。

int M(int x) { return x + N(x * 2); }

すると、対応する関数により、Nullable<int>そこにあるすべての演算子と呼び出しが、以前と同じように連携して動作できるようになります。

(それは非常に曖昧で不正確です。関数構成の知識を何も前提としない説明を求めています。)

「操作」とは何ですか?

  1. プレーンな型から値を取得して、同等のモナド値を作成する「ユニット」操作 (紛らわしいことに「戻り」操作と呼ばれることもあります) があります。これは、本質的には、増幅されていない型の値を取得して、増幅された型の値に変換する方法を提供します。これは、OO 言語のコンストラクターとして実装できます。

  2. モナド値と、その値を変換できる関数を受け取り、新しいモナド値を返す「bind」操作があります。bind は、モナドのセマンティクスを定義する重要な操作です。これにより、増幅されていない型の操作を、前述の関数合成のルールに従う増幅された型の操作に変換できます。

  3. 増幅された型から増幅されていない型を戻す方法がよくあります。厳密に言えば、この操作はモナドを持つために必須ではありません。(ただし、コモナドが必要な場合は必要ですこの記事ではこれ以上検討しません。)

もう一度、Nullable<T>例を挙げてみましょう。コンストラクタを使っintて を に変換できますNullable<int>。C# コンパイラはほとんどの null 許容型の「持ち上げ」を自動的に処理しますが、処理しない場合でも、持ち上げ変換は簡単です。たとえば、

int M(int x) { whatever }

に変換されます

Nullable<int> M(Nullable<int> x) 
{ 
    if (x == null) 
        return null; 
    else 
        return new Nullable<int>(whatever);
}

そしてNullable<int>背中を背中に変えるintにはValue財産。

重要なのは関数の変換です。null 許容操作の実際のセマンティクス (操作が にnull伝播するnull) が変換でどのようにキャプチャされるかに注目してください。これを一般化できます。

元の のように、intからへの関数があるとします。結果を nullable コンストラクターで実行するだけでよいため、これを を受け取ってを返す関数に簡単に変更できます。次に、次の高階メソッドがあるとします。intMintNullable<int>

static Nullable<T> Bind<T>(Nullable<T> amplified, Func<T, Nullable<T>> func)
{
    if (amplified == null) 
        return null;
    else
        return func(amplified.Value);
}

これで何ができるかわかりますか?を受け取ってintを返すメソッドint、または を受け取ってintを返すメソッドNullable<int>に、null 許容セマンティクスを適用できるようになりました

さらに、2つの方法があるとします

Nullable<int> X(int q) { ... }
Nullable<int> Y(int r) { ... }

そして、それらを構成したいとします:

Nullable<int> Z(int s) { return X(Y(s)); }

つまり、は とZの合成です。しかし、は を受け取り、を返すため、これを行うことはできません。ただし、「bind」操作があるため、これを機能させることができます。XYXintYNullable<int>

Nullable<int> Z(int s) { return Bind(Y(s), X); }

モナドの bind 操作は、増幅された型での関数の合成を機能させるものです。上で私が軽く触れた「ルール」は、モナドが通常の関数合成のルールを保持すること、恒等関数と合成すると元の関数になること、合成は結合的であること、などです。

C# では、Bindは と呼ばれますSelectMany。シーケンス モナドでどのように動作するかを見てみましょう。値をシーケンスに変換することと、シーケンスに操作をバインドすることの 2 つが必要です。ボーナスとして、「シーケンスを値に戻す」操作もあります。これらの操作は次のとおりです。

static IEnumerable<T> MakeSequence<T>(T item)
{
    yield return item;
}
// Extract a value
static T First<T>(IEnumerable<T> sequence)
{
    // let's just take the first one
    foreach(T item in sequence) return item; 
    throw new Exception("No first item");
}
// "Bind" is called "SelectMany"
static IEnumerable<T> SelectMany<T>(IEnumerable<T> seq, Func<T, IEnumerable<T>> func)
{
    foreach(T item in seq)
        foreach(T result in func(item))
            yield return result;            
}

nullable モナドのルールは、「nullable を生成する 2 つの関数を結合し、内側の関数が null を返すかどうかを確認し、null を返す場合は null を生成し、そうでない場合はその結果を使用して外側の関数を呼び出す」というものでした。これが nullable の望ましいセマンティクスです。

シーケンス モナドのルールは、「シーケンスを生成する 2 つの関数を組み合わせ、外側の関数を内側の関数によって生成されたすべての要素に適用し、結果として得られるすべてのシーケンスを連結する」というものです。モナドの基本的なセマンティクスはBind/メソッドで表現されます。これは、モナドが実際に何を意味するSelectManyかを示すメソッドです。

さらに良い方法があります。int のシーケンスと、int を受け取って文字列のシーケンスを返すメソッドがあるとします。バインディング操作を一般化して、一方の入力がもう一方の出力と一致する限り、異なる増幅された型を受け取って返す関数を合成できるようにすることができます。

static IEnumerable<U> SelectMany<T,U>(IEnumerable<T> seq, Func<T, IEnumerable<U>> func)
{
    foreach(T item in seq)
        foreach(U result in func(item))
            yield return result;            
}

したがって、次のように言うことができます。「この個々の整数の束を整数のシーケンスに増幅します。この特定の整数を文字列の束に変換し、文字列のシーケンスに増幅します。次に、両方の操作を組み合わせて、この整数の束をすべての文字列のシーケンスの連結に増幅します。」モナドを使用すると、増幅を構成できます。

それはどのような問題を解決し、最もよく使用される場所はどこですか?

それはむしろ「シングルトン パターンはどのような問題を解決するのか?」と尋ねるようなものですが、試してみます。

モナドは通常、次のような問題を解決するために使用されます。

  • このタイプに新しい機能を作成し、新しい機能を使用するためにこのタイプの古い関数を組み合わせる必要があります。
  • 型に対する一連の操作をキャプチャし、それらの操作を構成可能なオブジェクトとして表現し、適切な一連の操作が表現されるまで、より大きな構成を構築する必要があります。その後、結果を取得する必要があります。
  • 副作用を嫌う言語で副作用のある操作をきれいに表現する必要がある

C#は設計にモナドを使用しています。すでに述べたように、nullableパターンは「maybeモナド」に非常に似ています。LINQは完全にモナドで構築されています。SelectManyメソッドは、操作の合成の意味論的な作業を行うものです。(Erik Meijerは、すべてのLINQ関数は実際には次のように実装できると指摘しています。SelectMany; その他はすべて便利なものだけです。

私が求めていた理解を明確にするために、モナドを持つ FP アプリケーションを OOP アプリケーションに変換するとします。モナドの役割を OOP アプリケーションに移植するにはどうすればよいでしょうか。

ほとんどの OOP 言語には、モナド パターン自体を直接表現できるほど豊富な型システムがありません。ジェネリック型よりも高次の型をサポートする型システムが必要です。そのため、私はそうしようとはしません。代わりに、各モナドを表すジェネリック型を実装し、必要な 3 つの操作を表すメソッドを実装します。つまり、値を増幅された値に変換する、(おそらく) 増幅された値を値に変換する、増幅されていない値の関数を増幅された値の関数に変換する、です。

まず最初に、C#でLINQを実装する方法を学びましょう。SelectManyメソッド。これは、C# でシーケンス モナドがどのように動作するかを理解するための鍵です。非常に単純なメソッドですが、非常に強力です。


推奨される追加資料:

  1. C#のモナドについてより詳細かつ理論的に説明されたい方は、私の(エリック・リパートこの件については、同僚の Wes Dyer 氏の記事をお読みください。この記事は、私にとってモナドがようやく理解できたときに役立ちました。
  1. モナドがなぜ必要なのかを示す良い例です(例では Haskell を使用しています)
  1. いわば、前回の記事を JavaScript に「翻訳」したものです。

おすすめ記事