OOP プログラマーが (関数型プログラミングのバックグラウンドがなくても) 理解できる用語で言えば、モナドとは何ですか?
どのような問題を解決し、最も一般的に使用される場所は何ですか?
アップデート
私が探していた理解の種類を明確にするために、モナドを持つ FP アプリケーションを OOP アプリケーションに変換していたとしましょう。モナドの役割を OOP アプリに移植するにはどうしますか?
OOP プログラマーが (関数型プログラミングのバックグラウンドがなくても) 理解できる用語で言えば、モナドとは何ですか?
どのような問題を解決し、最も一般的に使用される場所は何ですか?
私が探していた理解の種類を明確にするために、モナドを持つ FP アプリケーションを OOP アプリケーションに変換していたとしましょう。モナドの役割を OOP アプリに移植するにはどうしますか?
更新: この質問は、 Monadsで読むことができる非常に長いブログ シリーズの主題でした — 素晴らしい質問をありがとう!
OOP プログラマーが (関数型プログラミングのバックグラウンドがなくても) 理解できる用語で言えば、モナドとは何ですか?
モナドは、特定の規則に従い、特定の操作が提供される型の「増幅器」です。
まず、「型の増幅器」とは何ですか?つまり、型を取得して、それをより特殊な型に変換できるシステムを意味します。たとえば、C# では次のようになりNullable<T>
ます。タイプのアンプです。たとえば、型を取り、int
その型に新しい機能を追加できます。つまり、以前は null にできなかったのに、今では null にできるようになります。
2 番目の例として、 を考えてみましょうIEnumerable<T>
。タイプのアンプです。たとえば、型を取り、string
その型に新しい機能を追加できます。つまり、任意の数の単一文字列から一連の文字列を作成できるようになります。
「一定のルール」とは?簡単に言えば、基になる型の関数が増幅された型で動作し、機能合成の通常の規則に従うようにするための賢明な方法があることです。たとえば、整数に関する関数がある場合は、次のようにします。
int M(int x) { return x + N(x * 2); }
次に、対応する関数 onNullable<int>
は、そこにあるすべての演算子と呼び出しを、以前と同じように連携させることができます。
(それは信じられないほど曖昧で不正確です。機能構成の知識について何も想定していない説明を求めました。)
「操作」とは何ですか?
単純な型から値を取得し、同等のモナド値を作成する「ユニット」操作 (紛らわしく「戻り」操作と呼ばれることもあります) があります。これは本質的に、増幅されていない型の値を取得し、増幅された型の値に変換する方法を提供します。オブジェクト指向言語のコンストラクターとして実装できます。
モナド値と値を変換できる関数を取り、新しいモナド値を返す「バインド」操作があります。Bind は、モナドのセマンティクスを定義する重要な操作です。これにより、増幅されていない型の操作を増幅された型の操作に変換できます。これは、前述の機能合成の規則に従います。
多くの場合、増幅された型から増幅されていない型を取り戻す方法があります。厳密に言えば、この操作はモナドを持つ必要はありません。(ただし、 comonadが必要な場合は必要です。この記事ではこれ以上考慮しません。)
繰り返しますNullable<T>
が、例として取り上げます。コンストラクタでint
a を a に変換できます。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
までの関数があるとします。null 許容コンストラクターを介して結果を実行できるため、これを を取り、を返す関数に簡単に作成できます。次に、次の高階メソッドがあるとします。int
M
int
Nullable<int>
static Nullable<T> Bind<T>(Nullable<T> amplified, Func<T, Nullable<T>> func)
{
if (amplified == null)
return null;
else
return func(amplified.Value);
}
あなたはそれで何ができるかを参照してください?を受け取ってを返すメソッド、または を受け取って を返すメソッドには、null 許容セマンティクスを適用できるようになりましたint
int
int
Nullable<int>
。
さらに:2つの方法があるとします
Nullable<int> X(int q) { ... }
Nullable<int> Y(int r) { ... }
そして、あなたはそれらを構成したい:
Nullable<int> Z(int s) { return X(Y(s)); }
つまり、とZ
の合成です。しかし、 を受け取り、 を返すため、それを行うことはできません。ただし、「バインド」操作があるため、これを機能させることができます。X
Y
X
int
Y
Nullable<int>
Nullable<int> Z(int s) { return Bind(Y(s), X); }
モナドに対するバインド操作は、増幅された型に対する関数の合成を機能させるものです。上で手短に説明した「ルール」は、モナドが通常の関数構成のルールを保持するというものです。恒等関数で構成すると元の関数になること、構成が連想的であることなどです。
C# では、「バインド」は「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 を生成し、そうでない場合は、その結果で外側の関数を呼び出す」というものでした。これが、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# は設計にモナドを使用します。すでに述べたように、null 許容パターンは "maybe モナド" に非常に似ています。LINQ は完全にモナドから構築されています。メソッドは、SelectMany
操作の構成のセマンティックな作業を行うものです。(Erik Meijer は、すべての LINQ 関数は実際には で実装できると指摘するのが好きですSelectMany
。それ以外はすべて便利です。)
私が探していた理解の種類を明確にするために、モナドを持つ FP アプリケーションを OOP アプリケーションに変換していたとしましょう。モナドの役割を OOP アプリに移植するにはどうしますか?
ほとんどの OOP 言語には、モナド パターン自体を直接表現するのに十分な豊富な型システムがありません。ジェネリック型より上位の型をサポートする型システムが必要です。だから私はそれをやろうとはしません。むしろ、各モナドを表すジェネリック型を実装し、必要な 3 つの操作を表すメソッドを実装します: 値を増幅された値に変換する、(おそらく) 増幅された値を値に変換する、増幅されていない値の関数を増幅された値の関数。
C# で LINQ を実装する方法から始めるのがよいでしょう。方法を研究してSelectMany
ください。これは、シーケンス モナドが C# でどのように機能するかを理解するための鍵です。非常にシンプルな方法ですが、非常に強力です。
推奨される、さらに読む:
モナドに最も近いオブジェクト指向の類推は「コマンドパターン」だと思います。
コマンド パターンでは、通常のステートメントまたは式をコマンドオブジェクトでラップします。コマンド オブジェクトは、ラップされたステートメントを実行するexecuteメソッドを公開します。したがって、ステートメントは、自由に受け渡して実行できるファーストクラスのオブジェクトに変換されます。コマンドオブジェクトを連鎖させたり入れ子にしたりしてプログラムオブジェクトを作成できるように、コマンドを構成することができます。
コマンドは、別のオブジェクトであるinvokerによって実行されます。(一連の通常のステートメントを単に実行するのではなく) コマンド パターンを使用する利点は、さまざまな呼び出し元がコマンドの実行方法にさまざまなロジックを適用できることです。
コマンド パターンは、ホスト言語でサポートされていない言語機能を追加 (または削除) するために使用できます。たとえば、例外のない架空の OO 言語では、「try」および「throw」メソッドをコマンドに公開することで、例外セマンティクスを追加できます。コマンドが throw を呼び出すと、呼び出し側はコマンドのリスト (またはツリー) を最後の "try" 呼び出しまでバックトラックします。逆に、個々のコマンドによってスローされたすべての例外をキャッチし、それらをエラー コードに変換して、次のコマンドに渡すことにより、言語から例外セマンティックを削除できます (例外が悪いと思われる場合)。
トランザクション、非決定論的実行、または継続などのさらに高度な実行セマンティクスは、ネイティブでサポートされていない言語でこのように実装できます。そう考えるとかなり有力なパターンです。
実際には、コマンドパターンはこのような一般的な言語機能として使用されていません。各ステートメントを個別のクラスに変換するオーバーヘッドは、耐え難い量のボイラープレート コードにつながります。しかし、原則として、モナドが fp で解決するために使用されるのと同じ問題を解決するために使用できます。
OOP プログラマーが (関数型プログラミングのバックグラウンドがなくても) 理解できる用語で言えば、モナドとは何ですか?
どのような問題を解決し、最も一般的に使用される場所は?最も一般的に使用される場所は?
OO プログラミングの観点から言えば、モナドはインターフェース (またはミックスインの可能性が高い) であり、型によってパラメーター化され、2 つのメソッドがreturn
あり、bind
以下を記述します。
それが解決する問題は、あらゆるインターフェースから予想されるのと同じタイプの問題です。クラス自体が実際には「オブジェクト」クラス自体よりも近いもののサブタイプではない場合でも、それらの間の類似性を説明できますか?」
より具体的には、Monad
「インターフェース」は、それ自体が型を取る型を取るという点で類似していますIEnumerator
。IIterator
ただし、主な「ポイント」はMonad
、メインクラスの情報構造を維持または強化しながら、新しい「内部タイプ」を持つポイントまで、内部タイプに基づいて操作を接続できることです。
クリストファー・リーグ(2010 年 7 月 12 日)による最近のプレゼンテーション " Monadologie -- 型不安に関する専門的なヘルプ"がありますが、これは継続とモナドのトピックに関して非常に興味深いものです。
この (スライドシェア) プレゼンテーションのビデオは、実際には vimeo で入手できます。
モナドの部分は、この 1 時間のビデオの約 37 分で始まり、58 枚のスライド プレゼンテーションのスライド 42 から始まります。
これは「関数型プログラミングの主要な設計パターン」として提示されていますが、例で使用されている言語は OOP と関数型の両方である Scala です。Debasish Ghosh (2008 年 3 月 27 日)のブログ記事「Monads - Scala
で計算を抽象化する別の方法」で、Scala の Monad について詳しく読むことができます。
型コンストラクタM は、次の操作をサポートする場合、モナドです。
# the return function
def unit[A] (x: A): M[A]
# called "bind" in Haskell
def flatMap[A,B] (m: M[A]) (f: A => M[B]): M[B]
# Other two can be written in term of the first two:
def map[A,B] (m: M[A]) (f: A => B): M[B] =
flatMap(m){ x => unit(f(x)) }
def andThen[A,B] (ma: M[A]) (mb: M[B]): M[B] =
flatMap(ma){ x => mb }
たとえば(Scalaの場合):
Option
モナドですdef unit[A] (x: A): オプション[A] = Some(x) def flatMap[A,B](m:Option[A])(f:A =>Option[B]): Option[B] = m マッチ { ケースなし => なし ケース Some(x) => f(x) }
List
モナドですdef unit[A] (x: A): リスト[A] = リスト(x) def flatMap[A,B](m:List[A])(f:A =>List[B]): List[B] = m マッチ { case Nil => Nil case x::xs => f(x) ::: flatMap(xs)(f) }
モナド構造を利用するために構築された便利な構文のため、モナドは Scala で重要です。
for
Scala での理解:
for {
i <- 1 to 4
j <- 1 to i
k <- 1 to j
} yield i*j*k
コンパイラによって次のように変換されます。
(1 to 4).flatMap { i =>
(1 to i).flatMap { j =>
(1 to j).map { k =>
i*j*k }}}
鍵となる抽象化は でありflatMap
、連鎖によって計算をバインドします。
を呼び出すたびflatMap
に、チェーン内の次のコマンドへの入力として機能する同じデータ構造型 (ただし値は異なる) が返されます。
上記のスニペットでは、flatMap は入力としてクロージャーを受け取り(SomeType) => List[AnotherType]
、List[AnotherType]
. 注意すべき重要な点は、すべての flatMap が入力と同じクロージャ タイプを取り、出力と同じタイプを返すことです。
これは、計算スレッドを「バインド」するものです。for-comprehension 内のシーケンスのすべての項目は、この同じ型制約を尊重する必要があります。
次のように、2 つの操作 (失敗する可能性があります) を実行し、結果を 3 番目に渡すとします。
lookupVenue: String => Option[Venue]
getLoggedInUser: SessionID => Option[User]
reserveTable: (Venue, User) => Option[ConfNo]
しかし、Monad を利用しないと、次のような複雑な OOP コードが得られます。
val user = getLoggedInUser(session)
val confirm =
if(!user.isDefined) None
else lookupVenue(name) match {
case None => None
case Some(venue) =>
val confno = reserveTable(venue, user.get)
if(confno.isDefined)
mailTo(confno.get, user.get)
confno
}
一方、Monad では、すべての操作が機能するように実際の型 ( Venue
, User
) を操作し、オプションの検証を隠しておくことができます。これはすべて、for 構文のフラットマップのおかげです。
val confirm = for {
venue <- lookupVenue(name)
user <- getLoggedInUser(session)
confno <- reserveTable(venue, user)
} yield {
mailTo(confno, user)
confno
}
yield 部分は、3 つの関数すべてにSome[X]
;がある場合にのみ実行されます。anyNone
は に直接返されconfirm
ます。
そう:
モナドを使用すると、関数型プログラミング内で順序付けされた計算が可能になり、DSL のように、構造化された優れた形式でアクションのシーケンスをモデル化できます。
そして最大の力は、さまざまな目的を果たすモナドを構成して、アプリケーション内で拡張可能な抽象化を実現する機能です。
このモナドによるアクションの順序付けとスレッド化は、クロージャの魔法による変換を行う言語コンパイラによって行われます。
ところで、Monad は FP で使用される計算のモデルだけではありません。
圏論は多くの計算モデルを提案します。その中で
- 計算のアローモデル
- 計算のモナドモデル
- 計算の応用モデル
読みの早い読者を尊重するために、最初に正確な定義から始め、より「平易な英語」での簡単な説明を続けてから、例に移ります。
以下は、簡潔で正確な定義を少し言い換えたものです。
モナド(コンピューター サイエンスにおける) は、正式には次のマップです。
X
特定のプログラミング言語のすべてのタイプを新しいタイプに送信します(「値を含む計算T(X)
のタイプ」と呼ばれます)。T
X
f:X->T(Y)
フォームと関数の 2 つの関数を構成するための規則を備えてg:Y->T(Z)
いますg∘f:X->T(Z)
。明白な意味で連想的であり、 と呼ばれる特定の単位関数に関して単位的である方法で、
pure_X:X->T(X)
単純にその値を返す純粋な計算に値を取得すると考えられます。
簡単に言えば、モナドは任意の型X
から別の型T(X)
に渡す規則であり、2 つの関数f:X->T(Y)
and g:Y->T(Z)
(作成したいができない) から新しい関数h:X->T(Z)
に渡す規則です。ただし、これは厳密な数学的意味での構成ではありません。基本的に、関数の構成を「曲げる」か、関数の構成方法を再定義しています。
さらに、「明白な」数学的公理を満たすために、モナドの合成規則が必要です。
f
性: (外側から)g
を使用して構成することは、(内側から)を使用して構成するh
ことと同じである必要があります。g
h
f
f
です。f
繰り返しますが、簡単に言えば、好きなように関数の構成を再定義することに夢中になることはできません。
f(g(h(k(x)))
、関数ペアを構成する順序を指定する必要はありません。モナド規則は関数のペアを構成する方法のみを規定しているため、その公理がなければ、どのペアが最初に構成されるかなどを知る必要があります。( は、でf
構成された可換性プロパティとは異なることに注意してください。これは必須ではありません)。g
g
f
もう一度簡単に言うと、モナドは型拡張の規則であり、結合性と単一性という 2 つの公理を満たす関数を構成します。
実際には、モナドは、関数の作成を処理する言語、コンパイラ、またはフレームワークによって実装される必要があります。そのため、関数の実行がどのように実装されるかを心配するのではなく、関数のロジックを書くことに集中できます。
一言で言えば、それは本質的にそれです。
プロの数学者として、 とh
の「合成」とf
呼ぶことは避けたいと思いg
ます。数学的にそうではないからです。h
それを「合成」と呼ぶことは、それが真の数学的合成であると誤って仮定していますが、そうではありません。と によっても一意に決定されませf
んg
。代わりに、モナドの関数の新しい「構成規則」の結果です。これは、実際の数学的構成が存在したとしても、実際の数学的構成とはまったく異なる可能性があります!
乾きにくくするために、小さなセクションで注釈を付けている例を示してみましょう。要点までスキップできます。
2 つの関数を構成するとします。
f: x -> 1 / x
g: y -> 2 * y
しかしf(0)
定義されていないため、例外e
がスローされます。では、構成値をどのように定義できますg(f(0))
か? もちろん、再び例外をスローします。多分同じe
。たぶん、新しく更新された例外e1
です。
ここで正確に何が起こりますか?まず、新しい例外値 (異なるか同じか) が必要です。nothing
それらを何と呼んでもかまいませんnull
が、本質は同じままです。たとえば、ここでの例では a ではありませんnumber
。特定の言語で実装するnull
方法との混乱を避けるために、それらを呼び出さないことを好みます。null
同様に私は避けることを好みます。なぜなら、それは原則としてすべきことであるnothing
と関連付けられることが多いからです。null
null
これは、経験豊富なプログラマーにとって些細な問題ですが、混乱のワームをなくすために、いくつかの単語を省略したいと思います。
Exception は、無効な実行結果がどのように発生したかに関する情報をカプセル化したオブジェクトです。
これは、詳細を破棄して単一のグローバル値 (NaN
または などnull
) を返すこと、長いログ リストを生成すること、または正確に何が起こったのかをデータベースに送信し、分散データ ストレージ レイヤー全体に複製することまで、さまざまです;)
これら 2 つの極端な例外の例の重要な違いは、最初のケースには副作用がないことです。2番目にはあります。これは(千ドルの)質問に私たちをもたらします:
短い答え: はい、ただし、副作用につながらない場合に限ります。
より長い答え。純粋であるためには、関数の出力はその入力によって一意に決定される必要があります。そのため、例外と呼ぶ新しい抽象値にf
送信することで関数を修正します。値には、入力によって一意に決定されない外部情報が含まれていないことを確認します。したがって、副作用のない例外の例を次に示します。0
e
e
x
e = {
type: error,
message: 'I got error trying to divide 1 by 0'
}
そして、ここに副作用のあるものがあります:
e = {
type: error,
message: 'Our committee to decide what is 1/0 is currently away'
}
実際には、そのメッセージが将来変更される可能性がある場合にのみ、副作用があります。しかし、決して変わらないことが保証されている場合、その値は一意に予測可能になるため、副作用はありません。
それをさらに愚かにするために。42
ever を返す関数は明らかに純粋です。しかし、狂った誰か42
が値が変化する可能性のある変数を作成することを決定した場合、まったく同じ関数が新しい条件下で純粋ではなくなります。
本質を説明するために、簡単にするためにオブジェクト リテラル表記を使用していることに注意してください。残念ながら、JavaScript のような言語では混乱が生じます。ここでerror
は、関数合成に関してここで希望するように動作する型ではありませんが、実際の型はこのように動作するnull
かNaN
、このように動作せず、人工的で常に直感的であるとは限りません。型変換。
例外内のメッセージを変更したいので、実際にはE
例外オブジェクト全体の新しい型を宣言しています。それが、型または新しい例外型のmaybe number
いずれかであるという紛らわしい名前を除いて、 が行うことです。ですので、実際には と の結合です。特に、 をどのように構築したいかによって異なりますが、これは の名前にも反映されていません。number
E
number | E
number
E
E
maybe number
これは、関数を取得し、それらの構成を関数f: X -> Y
を満たす関数として構築する数学的操作
です。この定義の問題は、結果が の引数として許可されていない場合に発生します。g: Y -> Z
h: X -> Z
h(x) = g(f(x))
f(x)
g
数学では、これらの関数は余分な作業なしでは構成できません。上記のf
との例に対する厳密な数学的解決策は、 の定義のセットg
から削除することです。その新しい定義セット (新しいより限定的な のタイプ) により、は で構成可能になります。0
f
x
f
g
ただし、そのような定義のセットを制限することは、プログラミングではあまり実用的ではありませんf
。代わりに、例外を使用できます。
NaN
または、別のアプローチとして、undefined
、null
、などの人為的な値が作成されInfinity
ます。したがって、およびに評価1/0
します。そして、例外をスローする代わりに、新しい値を強制的に式に戻します。予測できる場合とそうでない場合がある結果につながります。Infinity
1/-0
-Infinity
1/0 // => Infinity
parseInt(Infinity) // => NaN
NaN < 0 // => false
false + 1 // => 1
そして、次に進む準備ができている通常の数に戻りました;)
JavaScript を使用すると、上記の例のようにエラーをスローすることなく、どんな犠牲を払っても数値式を実行し続けることができます。つまり、関数を構成することもできます。これはまさにモナドのことです-この回答の冒頭で定義された公理を満たす関数を構成する規則です。
しかし、数値エラーを処理するための JavaScript の実装から生じる、関数を構成する規則はモナドですか?
この質問に答えるために必要なのは、公理を確認することだけです (ここでは質問の一部ではないため、演習として残します)。
実際、より有用なモナドは代わりに、 iff
が some に対して例外をスローx
し、その合成も any でスローすることを規定するルールg
です。E
さらに、これまでに可能な値が 1 つしかない例外をグローバルに一意にします (圏論の終端オブジェクト)。これで、2 つの公理が即座にチェック可能になり、非常に便利なモナドが得られます。そしてその結果が、 maybe モナドとしてよく知られているものです。
モナドは、値をカプセル化するデータ型であり、基本的に、次の 2 つの操作を適用できます。
return x
カプセル化するモナド型の値を作成しますx
m >>= f
(「バインド演算子」と読みます) 関数f
をモナドの値に適用しますm
それがモナドです。もう少し技術的なことはありますが、基本的にはこれら 2 つの操作でモナドを定義します。本当の問題は「モナドは何をするのか?」であり、それはモナドに依存します — リストはモナドであり、Maybes はモナドであり、IO 操作はモナドです。return
それらがモナドであると言うとき、それが意味するのは、それらがandのモナドインターフェースを持っているということだけ>>=
です。
ウィキペディアから:
関数型プログラミングでは、モナドは (ドメインモデルのデータの代わりに) 計算を表すために使用される一種の抽象データ型です。モナドを使用すると、プログラマーはアクションを連鎖させてパイプラインを構築できます。パイプラインでは、各アクションがモナドによって提供される追加の処理規則で装飾されます。関数型スタイルで書かれたプログラムは、モナドを利用して、順序付けられた操作を含む手続きを構築したり、1 [2] 任意の制御フロー (同時実行、継続、または例外の処理など) を定義したりできます。
正式には、モナドは、2 つの操作 (bind と return) と、モナド関数 (つまり、モナドからの値を引数として使用する関数) を正しく構成できるようにいくつかのプロパティを満たす必要がある型コンストラクター M を定義することによって構築されます。return 操作はプレーンな型から値を取得し、それを型 M のモナド コンテナーに入れます。bind 操作は逆のプロセスを実行し、コンテナーから元の値を抽出して、パイプライン内の関連する次の関数に渡します。
プログラマーは、データ処理パイプラインを定義するためにモナド関数を構成します。モナドは、パイプライン内の特定のモナド関数が呼び出される順序を決定し、計算に必要なすべての秘密の作業を管理する再利用可能な動作であるため、フレームワークとして機能します。[3] パイプラインにインターリーブされた bind 演算子と return 演算子は、各モナド関数が制御を返した後に実行され、モナドによって処理される特定の側面を処理します。
とてもよく説明されていると思います。
モナドがOOで「自然な」解釈をするかどうかは、モナドに依存します。Javaのような言語では、おそらくモナドをnullポインターをチェックする言語に変換できるため、失敗した(つまり、HaskellでNothingを生成する)計算は結果としてnullポインターを出力します。状態モナドを、その状態を変更するための可変変数とメソッドを作成することによって生成された言語に変換できます。
モナドは、エンドファンクターのカテゴリーのモノイドです。
文がまとめる情報は非常に深いです。そして、あなたは命令型言語のモナドで働きます。モナドは「シーケンスされた」ドメイン固有言語です。これは、モナドを「命令型プログラミング」の数学的モデルにする特定の興味深い特性を満たしています。Haskellを使用すると、さまざまな方法で組み合わせることができる小さな(または大きな)命令型言語を簡単に定義できます。
オブジェクト指向プログラマーは、言語のクラス階層を使用して、オブジェクトと呼ばれるコンテキストで呼び出すことができる関数またはプロシージャの種類を整理します。モナドは、さまざまなモナドを任意の方法で組み合わせて、サブモナドのすべてのメソッドをスコープに効果的に「インポート」できる限り、このアイデアの抽象化でもあります。
次に、アーキテクチャ上、型シグネチャを使用して、値の計算に使用できるコンテキストを明示的に表現します。
この目的のためにモナド変換子を使用することができ、すべての「標準」モナドの高品質なコレクションがあります。
対応するモナド変換子と型クラスを使用します。型クラスを使用すると、インターフェイスを統合することでモナドを結合するための補完的なアプローチが可能になり、具象モナドはモナドの「種類」の標準インターフェイスを実装できます。たとえば、モジュールControl.Monad.StateにはクラスMonadState smが含まれており、(State s)はフォームのインスタンスです。
instance MonadState s (State s) where
put = ...
get = ...
長い話は、モナドは値に「コンテキスト」を付加するファンクターであり、モナドに値を注入する方法があり、少なくともそれに付加されたコンテキストに関して値を評価する方法があるということです。制限された方法で。
それで:
return :: a -> m a
タイプaの値をタイプmaのモナド「アクション」に注入する関数です。
(>>=) :: m a -> (a -> m b) -> m b
モナドアクションを実行し、その結果を評価し、結果に関数を適用する関数です。(>> =)の良い点は、結果が同じモナドにあることです。言い換えると、m >> = fでは、(>> =)は結果をmから引き出し、それをfにバインドして、結果がモナドに含まれるようにします。(あるいは、(>> =)はfをmに引き込み、それを結果に適用すると言うことができます。)結果として、f :: a-> mb、およびg :: b-> mcがある場合、次のことができます。 「シーケンス」アクション:
m >>= f >>= g
または、「表記を行う」を使用する
do x <- m
y <- f x
g y
(>>)のタイプが点灯している可能性があります。です
(>>) :: m a -> m b -> m b
これは、Cなどの手続き型言語の(;)演算子に対応します。次のような表記を使用できます。
m = do x <- someQuery
someAction x
theNextAction
andSoOn
数学的および哲学的論理では、モナディズムで「自然に」モデル化されたフレームとモデルがあります。解釈は、モデルの定義域を調べて、命題(または一般化の下の式)の真理値(または一般化)を計算する関数です。必要性の様相論理では、「すべての可能な世界」でそれが真実である場合、つまりすべての許容される領域に関して真実である場合、命題が必要であると言うかもしれません。これは、命題の言語のモデルを、そのドメインが別個のモデルのコレクション(それぞれの可能な世界に対応するもの)で構成されるモデルとして具体化できることを意味します。すべてのモナドには、レイヤーを平坦化する「join」という名前のメソッドがあります。これは、結果がモナドアクションであるすべてのモナドアクションをモナドに埋め込むことができることを意味します。
join :: m (m a) -> m a
さらに重要なことは、モナドが「レイヤースタッキング」操作で閉じられることを意味します。これがモナド変換子の仕組みです。モナド変換子は、次のようなタイプに「結合のような」メソッドを提供することでモナドを組み合わせます。
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
(MaybeT m)のアクションをmのアクションに変換して、レイヤーを効果的に折りたたむことができるようにします。この場合、runMaybeT :: MaybeT ma-> m(Maybe a)は結合のようなメソッドです。(MaybeT m)はモナドであり、MaybeT :: m(Maybe a)-> MaybeT maは、事実上、mの新しいタイプのモナドアクションのコンストラクターです。
ファンクターのフリーモナドは、fをスタックすることによって生成されるモナドであり、fのコンストラクターのすべてのシーケンスがフリーモナドの要素(より正確には、コンストラクターのシーケンスのツリーと同じ形状のもの)であることを意味します。 f)。無料のモナドは、最小限のボイラープレートで柔軟なモナドを構築するための便利な手法です。Haskellプログラムでは、無料のモナドを使用して「高レベルのシステムプログラミング」用の単純なモナドを定義し、型の安全性を維持するのに役立てることができます(型とその宣言を使用しているだけです。コンビネーターを使用すると、実装が簡単になります)。
data RandomF r a = GetRandom (r -> a) deriving Functor
type Random r a = Free (RandomF r) a
type RandomT m a = Random (m a) (m a) -- model randomness in a monad by computing random monad elements.
getRandom :: Random r r
runRandomIO :: Random r a -> IO a (use some kind of IO-based backend to run)
runRandomIO' :: Random r a -> IO a (use some other kind of IO-based backend)
runRandomList :: Random r a -> [a] (some kind of list-based backend (for pseudo-randoms))
モナディズムは、「インタプリタ」または「コマンド」パターンと呼ばれるものの基盤となるアーキテクチャであり、最も明確な形式に抽象化されています。これは、すべてのモナディック計算を少なくとも自明に「実行」する必要があるためです。(ランタイムシステムはIOモナドを実行し、Haskellプログラムへのエントリポイントです。IOはIOアクションを順番に実行することにより、残りの計算を「駆動」します)。
結合のタイプは、モナドがエンドファンクターのカテゴリーのモノイドであるというステートメントを取得する場所でもあります。結合は、そのタイプのおかげで、通常、理論的な目的にとってより重要です。しかし、タイプを理解することは、モナドを理解することを意味します。結合およびモナド変換子の結合のようなタイプは、関数合成の意味で、事実上、エンドファンクターの合成です。Haskellのような疑似言語でそれを置くために、
Foo :: m(ma)<->(m .m)a
オブジェクト指向の用語では、モナドは流暢なコンテナです。
最小要件は、コンストラクターと少なくとも 1 つのメソッドclass <A> Something
をサポートするの定義です。Something(A a)
Something<B> flatMap(Function<A, Something<B>>)
おそらく、モナドクラスに、クラスのルールを保持する署名付きのメソッドがあるかどうかもカウントSomething<B> work()
されます-コンパイラーはコンパイル時に flatMap を焼き込みます。
モナドはなぜ便利なのですか?セマンティクスを保持する連鎖可能な操作を可能にするコンテナであるためです。たとえば、、、 などOptional<?>
の isPresent のセマンティクスを保持します。Optional<String>
Optional<Integer>
Optional<MyClass>
大まかな例として、
Something<Integer> i = new Something("a")
.flatMap(doOneThing)
.flatMap(doAnother)
.flatMap(toInt)
文字列で始まり、整数で終わることに注意してください。かなりクール。
OO では少し手がかかるかもしれませんが、Something の別のサブクラスを返す Something のメソッドは、元の型のコンテナーを返すコンテナー関数の基準を満たしています。
これがセマンティクスを保持する方法です。つまり、コンテナーの意味と操作は変更されず、コンテナー内のオブジェクトをラップして拡張するだけです。
典型的な使用法におけるモナドは、手続き型プログラミングの例外処理メカニズムと機能的に同等です。
現代の手続き型言語では、一連のステートメントの周りに例外ハンドラーを配置します。これらのステートメントのいずれかが例外をスローする可能性があります。いずれかのステートメントが例外をスローすると、一連のステートメントの通常の実行が停止し、例外ハンドラーに転送されます。
ただし、関数型プログラミング言語は、「goto」のような性質があるため、哲学的に例外処理機能を回避します。関数型プログラミングの観点では、関数には、プログラム フローを混乱させる例外のような「副作用」があってはなりません。
実際には、主に I/O が原因で、現実の世界で副作用を排除することはできません。関数型プログラミングのモナドは、チェーンされた一連の関数呼び出し (いずれも予期しない結果を生成する可能性があります) を取り、予期しない結果をカプセル化されたデータに変換することで、これを処理するために使用されます。
制御の流れは維持されますが、予期しないイベントは安全にカプセル化されて処理されます。
モナドは関数の配列です
(Pst: 関数の配列は単なる計算です)。
実際には、真の配列 (1 つのセル配列に 1 つの関数) の代わりに、これらの関数を別の関数 >>= で連結しています。>>= を使用すると、関数 i の結果を関数 i+1 にフィードしたり、それらの間で計算を実行したり、関数 i+1 を呼び出さないようにしたりすることができます。
ここで使用される型は「コンテキストを持つ型」です。これは、「タグ」付きの値です。連鎖される関数は、「裸の値」を取り、タグ付けされた結果を返す必要があります。>>= の役割の 1 つは、そのコンテキストから裸の値を抽出することです。ネイキッド値をタグで取得する関数「return」もあります。
Maybe の例。これを使用して、計算を行う単純な整数を格納しましょう。
-- a * b
multiply :: Int -> Int -> Maybe Int
multiply a b = return (a*b)
-- divideBy 5 100 = 100 / 5
divideBy :: Int -> Int -> Maybe Int
divideBy 0 _ = Nothing -- dividing by 0 gives NOTHING
divideBy denom num = return (quot num denom) -- quotient of num / denom
-- tagged value
val1 = Just 160
-- array of functions feeded with val1
array1 = val1 >>= divideBy 2 >>= multiply 3 >>= divideBy 4 >>= multiply 3
-- array of funcionts created with the do notation
-- equals array1 but for the feeded val1
array2 :: Int -> Maybe Int
array2 n = do
v <- divideBy 2 n
v <- multiply 3 v
v <- divideBy 4 v
v <- multiply 3 v
return v
-- array of functions,
-- the first >>= performs 160 / 0, returning Nothing
-- the second >>= has to perform Nothing >>= multiply 3 ....
-- and simply returns Nothing without calling multiply 3 ....
array3 = val1 >>= divideBy 0 >>= multiply 3 >>= divideBy 4 >>= multiply 3
main = do
print array1
print (array2 160)
print array3
モナドがヘルパー操作を伴う関数の配列であることを示すために、実際の関数の配列を使用した上記の例と同等のものを考えてみましょう
type MyMonad = [Int -> Maybe Int] -- my monad as a real array of functions
myArray1 = [divideBy 2, multiply 3, divideBy 4, multiply 3]
-- function for the machinery of executing each function i with the result provided by function i-1
runMyMonad :: Maybe Int -> MyMonad -> Maybe Int
runMyMonad val [] = val
runMyMonad Nothing _ = Nothing
runMyMonad (Just val) (f:fs) = runMyMonad (f val) fs
そして、それは次のように使用されます:
print (runMyMonad (Just 160) myArray1)
Powershell を使用したことがある場合、Eric が説明したパターンはおなじみのはずです。 Powershell コマンドレットはモナドです。機能構成はパイプラインで表されます。
Jeffrey Snover の Erik Meijer とのインタビューで、さらに詳しく説明しています。
「モナドとは何ですか?」に対する私の回答を参照してください。
動機付けとなる例から始まり、例を通して作業し、モナドの例を導出し、「モナド」を正式に定義します。
関数型プログラミングの知識がないことを前提としてfunction(argument) := expression
おり、可能な限り単純な式の構文で擬似コードを使用します。
この C++ プログラムは、擬似コード モナドの実装です。(参考までに:M
は型コンストラクター、feed
は「バインド」操作、wrap
は「戻り」操作です。)
#include <iostream>
#include <string>
template <class A> class M
{
public:
A val;
std::string messages;
};
template <class A, class B>
M<B> feed(M<B> (*f)(A), M<A> x)
{
M<B> m = f(x.val);
m.messages = x.messages + m.messages;
return m;
}
template <class A>
M<A> wrap(A x)
{
M<A> m;
m.val = x;
m.messages = "";
return m;
}
class T {};
class U {};
class V {};
M<U> g(V x)
{
M<U> m;
m.messages = "called g.\n";
return m;
}
M<T> f(U x)
{
M<T> m;
m.messages = "called f.\n";
return m;
}
int main()
{
V x;
M<T> m = feed(f, feed(g, wrap(x)));
std::cout << m.messages;
}
私が考えることができる最も簡単な説明は、モナドは装飾された結果を持つ関数を構成する方法であるということです (別名 Kleisli 構成)。「装飾された」関数には、シグネチャa -> (b, smth)
wherea
とb
are 型 (考えてくださいInt
, Bool
) があり、互いに異なる可能性がありますが、必ずしもそうではありません - andsmth
は「コンテキスト」または「装飾」です。
このタイプの関数は、「装飾」に相当するa -> m b
場所にも記述できます。したがって、これらはコンテキスト内で値を返す関数です (ログ メッセージがどこにあるか、アクションをログに記録する関数、または入出力を実行し、その結果が IO アクションの結果に依存する関数を考えてください)。m
smth
smth
モナドは、そのような関数を構成する方法を実装者に指示させるインターフェース (「型クラス」) です。実装者は、インターフェイスを実装したい(a -> m b) -> (b -> m c) -> (a -> m c)
任意の型に対して合成関数を定義する必要があります (これが Kleisli 合成です)。m
したがって、「装飾」(アクションのログ)である s(Int, String)
の計算の結果を表すタプル型と、アクションのログである 2 つの関数があり、構成である関数を取得したい場合ログも考慮に入れる2つの機能の。Int
(_, String)
increment :: Int -> (Int, String)
twoTimes :: Int -> (Int, String)
incrementThenDouble :: Int -> (Int, String)
与えられた例では、2 つの関数のモナド実装が整数値 2 incrementThenDouble 2
(これは に等しい) に適用され、 と等しい中間結果twoTimes (increment 2)
が返されます。(6, " Adding 1. Doubling 3.")
increment 2
(3, " Adding 1.")
twoTimes 3
(6, " Doubling 3.")
このクライスリ合成関数から、通常の単項関数を導き出すことができます。
実用的な観点から (以前の多くの回答や関連記事で述べられていることを要約すると)、モナドの基本的な「目的」(または有用性) の 1 つは、再帰的なメソッド呼び出しで暗黙的な依存関係を活用することであると思われます。別名関数合成 (つまり、f1 が f2 を呼び出して f3 を呼び出すとき、f3 は f1 の前に f2 の前に評価する必要がある) は、特に遅延評価モデルのコンテキスト (つまり、単純なシーケンスとしての順次合成) で、自然な方法で順次合成を表現します。 、例えば、C の "f3(); f2(); f1();" - f3、f2、および f1 が実際に何も返さない場合 [f1(f2(f3)) としての連鎖) を考えると、トリックは特に明白です。人為的であり、純粋にシーケンスを作成することを意図しています])。
これは、副作用が関係する場合、つまり、何らかの状態が変更された場合に特に関連します (f1、f2、f3 に副作用がなければ、それらがどの順序で評価されるかは問題ではありません。これは、pure の優れた特性です)。関数型言語 (たとえば、これらの計算を並列化できるようにする)。関数が純粋であればあるほどよい。
その狭い観点から見ると、モナドは遅延評価 (コードの表示に依存しない順序に従って、絶対に必要な場合にのみ評価する) を好む言語の構文糖衣と見なすことができると思います。シーケンシャル構成を表す他の手段。最終的な結果として、コードの「不純」な (つまり、副作用がある) セクションは、命令型の方法で自然に提示できますが、純粋な関数から (副作用なしで) きれいに分離されます。ざっくり評価。
ただし、ここで警告されているように、これは 1 つの側面にすぎません。