OOPプログラマが(関数型プログラミングのバックグラウンドなしで)理解するだろうという点で、モナドとは何ですか?
それはどんな問題を解決し、それが使われている最も一般的な場所は何ですか?
編集:
私が探していた理解の種類を明確にするために、モナドを持つFPアプリケーションをOOPアプリケーションに変換していたとしましょう。モナドの責任をOOPアプリに移植するにはどうしますか?
更新:この質問はあなたが モナド で読むことができる非常に長いブログシリーズの主題でした - すばらしい質問をありがとう!
OOPプログラマが(関数型プログラミングのバックグラウンドなしで)理解するだろうという点で、モナドとは何ですか?
モナドは型のアンプ that 特定の規則に従うおよび特定の演算が提供されるです。
まず、「タイプのアンプ」とは何ですか?それによって私はあなたがタイプを取り、それをより特別なタイプに変えることを可能にするシステムを意味します。たとえば、C#ではNullable<T>
を検討してください。これはタイプのアンプです。それはあなたがint
と言う型を取り、その型に新しい能力を追加することを可能にします。
2番目の例として、IEnumerable<T>
を考えます。タイプのアンプです。 string
などの型を取り、その型に新しい機能を追加できます。つまり、任意の数の単一文字列から一連の文字列を作成できるようになります。
「特定の規則」とは何ですか?手短に言えば、基礎となる型の機能が増幅型で機能するように機能的な方法があり、その結果、それらが機能的構成の通常の規則に従うようになる。たとえば、整数に関する関数があるとします。
int M(int x) { return x + N(x * 2); }
それからNullable<int>
の対応する関数はそこでのすべての演算子と呼び出しがそれらが以前と同じように一緒に働くことを可能にします。
(それは信じられないほど曖昧で不正確です。あなたは機能構成の知識について何も想定していない説明を求めました。)
「操作」とは何ですか?
プレーンタイプから値を取得して同等のモナディック値を作成する、 "unit"操作( "return"操作とも呼ばれます)があります。これは、本質的に、増幅されていないタイプの値を取得し、それを増幅されたタイプの値に変換する方法を提供します。それはOO言語のコンストラクタとして実装することができます。
モナド値とその値を変換することができ、そして新しいモナド値を返すことができる関数をとる "bind"操作があります。バインドはモナドの意味を定義する重要な操作です。これによって、増幅されていないタイプの操作を増幅されたタイプの操作に変換できます。これは、前述の機能構成の規則に従います。
多くの場合、増幅されていないタイプを増幅されたタイプから元に戻す方法があります。厳密に言えば、この操作はモナドを持つ必要はありません。 (ただし、comonadが必要な場合は必要です。この記事ではこれ以上検討しません。)
ここでも、例としてNullable<T>
を取ります。コンストラクタを使ってint
をNullable<int>
に変えることができます。 C#コンパイラはあなたのために最も許容できる "リフティング"の世話をしますが、もしそうでなければ、リフティング変換は簡単です:オペレーション、例えば、
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
のように、int
からM
までの関数があるとします。あなたはそれをint
を取り、Nullable<int>
を返す関数に簡単に作ることができます。なぜならあなたは結果をnull許容コンストラクタを通して実行することができるからです。今度はこの高階メソッドがあるとします。
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
はX
とY
の合成です。 X
はint
を取り、Y
はNullable<int>
を返すので、それはできません。しかし、あなたは "bind"操作をしているので、あなたはこれを動かすことができます:
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;
}
Null許容モナドの規則は、「null許容値を生成する2つの関数を組み合わせて、内部のものがnullになるかどうかを確認し、そうである場合はnullを生成し、そうでない場合は結果とともに外部のものを呼び出す」でした。それがnull許容の望ましい意味論です。
シーケンスモナド規則は、「シーケンスを生成する2つの関数を結合し、内部関数によって生成されたすべての要素に外部関数を適用してから、結果として得られるすべてのシーケンスを連結する」というものです。モナドの基本的な意味はBind
/SelectMany
メソッドで捉えられています。これは、モナドが本当に手段であることを教えてくれるメソッドです。
私たちはもっとうまくやることができます。整数のシーケンスと、整数を受け取り文字列のシーケンスになるメソッドがあるとします。一方の入力が他方の出力と一致する限り、結合操作を一般化して、さまざまな増幅された型を取り込んで返す関数を組み合わせることができます。
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可能パターンは「おそらくモナド」と非常によく似ています。 LINQは完全にモナドから構築されています。 SelectMany
メソッドは、操作の構成という意味的な働きをするものです。 (Erik Meijerは、すべてのLINQ関数が実際に SelectMany
によって実装されることを指摘するのが好きです;他のすべては単に便利です。)
私が探していた理解の種類を明確にするために、モナドを持つFPアプリケーションをOOPアプリケーションに変換していたとしましょう。モナドの責任をOOPアプリに移植するために何をしますか?
ほとんどのOOP言語には、モナドパターン自体を直接表現するための十分に豊富な型システムがありません。ジェネリック型よりも高い型である型をサポートする型システムが必要です。だから私はそうしようとしないでしょう。むしろ、それぞれのモナドを表すジェネリック型を実装し、必要な3つの演算を表すメソッドを実装します。値を増幅値に変換する、(おそらく)増幅値を値に変換する、そして非増幅値の関数を変換する増幅値に対する関数.
開始するのに良い場所は、C#でLINQをどのように実装したかです。 SelectMany
メソッドを調べます。シーケンスモナドがC#でどのように機能するのかを理解することが重要です。これは非常に単純な方法ですが、非常に強力です。
お勧め、さらに読む:
それでは、最初の大きな問題があります。これはプログラムです:
f(x) = 2 * x
g(x,y) = x / y
最初に実行されるのはです。どのようにして、せいぜい関数を使用して、順序付けられた関数のシーケンス(すなわちa program)を形成することができますか?
解決策:関数を作成する。最初にg
、次にf
が必要な場合は、単にf(g(x,y))
と記述してください。はい、でも….
その他の問題:いくつかの関数が失敗する可能性があります(つまりg(2,0)
、0で割る)。FPには「例外」はありません。どのように解決しますか?
解決方法:関数が2種類のものを返すことを許可する:g : Real,Real -> Real
(2つの実数から実数への関数)を持つ代わりに、g : Real,Real -> Real | Nothing
(2つの実数からの関数本物かどうか))。
しかし、関数は(より簡単にするために)one thingだけを返すべきです。
解決策:返される新しいタイプのデータを作成しましょう。 "boxing type"は、実際のものを囲むか、単に何もしないものです。したがって、私たちはg : Real,Real -> Maybe Real
を持つことができます。はい、でも….
f(g(x,y))
はどうなりましたか? f
はMaybe Real
を消費する準備ができていません。そして、私たちがg
で接続できるすべての関数をMaybe Real
を消費するように変更したくはありません。
解決策:に "connect"/"compose"/"link"関数という特別な関数を持たせましょう。こうすることで、舞台裏で、ある関数の出力を適応させて次の関数にフィードすることができます。
我々の場合:g >>= f
(g
をf
に接続/作成)。 >>=
にg
の出力を取得してもらい、それを調べて、Nothing
の場合はf
を呼び出さずにNothing
を返すようにします。または逆に、ボックスで囲まれたReal
を抽出し、それとともにf
を入力します。 (このアルゴリズムは、Maybe
型の>>=
の実装にすぎません)。
この同じパターンを使用して解決できる他の多くの問題が発生します。1. "box"を使用して異なる意味/値を体系化/格納し、それらの "boxed values"を返すg
のような関数を持ちます。 2. g
の出力をf
の入力に接続するのを助けるコンポーザ/リンカg >>= f
がいるので、f
を変更する必要はまったくありません。
この技術を使用して解決することができる注目すべき問題は以下のとおりです。
関数のシーケンス( "プログラム")内のすべての関数が共有できるグローバルな状態を持っている:solution StateMonad
。
「不純な関数」は好きではありません。同じ入力に対して異なる出力をもたらす関数。そのため、これらの関数にマークを付けて、タグ付き/ボックス付きの値を返すようにします。IO
モナド。
総幸せ!!
私はモナドに最も近いOOの類似性を " コマンドパターン "と言うでしょう。
コマンドパターンでは、通常のステートメントまたは式をコマンドオブジェクトで囲みます。コマンドオブジェクトは、ラップされたステートメントを実行するexecuteメソッドを公開します。そのため、文はファーストクラスのオブジェクトに変換され、自由に渡して実行できます。コマンドは合成することができるので、コマンドオブジェクトを連鎖して入れ子にすることによってプログラムオブジェクトを作成できます。
コマンドは別のオブジェクト、呼び出し元によって実行されます。 (単に一連の通常のステートメントを実行するのではなく)コマンドパターンを使用する利点は、呼び出し元が異なればコマンドの実行方法に異なるロジックを適用できることです。
コマンドパターンは、ホスト言語ではサポートされていない言語機能を追加(または削除)するために使用できます。たとえば、例外のない仮想のOO言語では、 "try"および "throw"メソッドをコマンドに公開することで、例外のセマンティクスを追加できます。コマンドがthrowを呼び出すと、呼び出し側は最後の "try"呼び出しまでコマンドのリスト(またはツリー)をバックトラックします。逆に言えば、個々のコマンドによって投げられるすべての例外を捕らえてエラーコードに変換することで、言語から例外の意味を取り除くことができます( 例外は悪い と信じている場合)。次のコマンド.
トランザクション、非決定的実行、または継続のような、より派手な実行セマンティクスは、ネイティブにサポートされていない言語でこのように実装できます。あなたがそれについて考えるなら、それはかなり強力なパターンです。
現在、コマンドパターンはこのような一般的な言語機能としては使用されていません。各ステートメントを別々のクラスに変換することによるオーバーヘッドは、耐え難い量の定型コードを生み出すことになります。しかし原則として、モナドがfpで解くのに使われるのと同じ問題を解くのに使うことができます。
OOPプログラマが(関数型プログラミングのバックグラウンドなしで)理解するだろうという点で、モナドとは何ですか?
それはどんな問題を解決し、それが使用された最も一般的な場所は何ですか?それが使用された最も一般的な場所は何ですか?
OOプログラミングの観点から言えば、モナドは、型によってパラメータ化されたインターフェイス(またはおそらくミックスイン)であり、次の2つのメソッドreturn
とbind
を持ちます。
それが解決する問題はあなたがどんなインターフェースからも期待するのと同じタイプの問題、すなわち、「私は異なることをするたくさんの異なるクラスがありますが、根本的な類似性を持つ方法でそれらの異なることをするようです。たとえクラス自体が実際には「Object」クラス自体に近いもののサブタイプではないとしても、それらの類似性を説明できますか?」
より具体的には、Monad
"interface"は、それ自身が型を取る型を取るという点でIEnumerator
またはIIterator
に似ています。ただし、Monad
の主な「ポイント」は、メインクラスの情報構造を維持しながら、またはさらに強化しながら、内部タイプに基づいた操作を新しい「内部タイプ」を持つ点にも結び付けることができるということです。
最近の発表があります。 "Monadologie - 型不安の専門家によるヘルプ"Christopher League(2010年7月12日)、これは継続とモナドの話題に関して非常に興味深いものです。
この(スライドシェアの)プレゼンテーションに付随するビデオは、実際にはvimeoで入手できます。
モナド部は、この1時間のビデオで約37分で始まり、その58枚のスライドプレゼンテーションのスライド42から始まります。
これは「関数型プログラミングのための主要な設計パターン」として提示されていますが、例で使用されている言語はScalaであり、これはOOPとfunctionalの両方です。
Scalaのモナドのブログ記事でもっと読むことができます " モナド - Scalaで計算を抽象化するもう一つの方法 "、から Debasish Ghosh (2008年3月27日)。
型コンストラクタ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:オプション[A]) (f:A => Option [B]):Option [B] = m一致{ caseなし=>なし case Some(x)=> f(x) ) }
List
はモナドdef unit [A](x:A):List [A] = List(x) def flatMap [A、B](m:List [A]) (f:A => List [B]):List [B] = m match { case Nil => Nil case x :: xs => f(x) ::: flatMap(xs)(f) }
Monad構造体を利用するために構築された便利な構文のために、MonadはScalaで大きな問題になります。
Scalaでのfor
内包表記:
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-内包のシーケンスのすべての項目はこの同じ型制約を尊重しなければなりません。
次のように、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
)を操作し、Option検証の部分を非表示にしておくことができます。これはすべてfor構文のフラットマップのためです。
val confirm = for {
venue <- lookupVenue(name)
user <- getLoggedInUser(session)
confno <- reserveTable(venue, user)
} yield {
mailTo(confno, user)
confno
}
Yield部分は、3つの関数すべてにSome[X]
がある場合にのみ実行されます。どんなNone
も直接confirm
に返されます。
そう:
モナドは、関数型プログラミングの中で順序付けられた計算を可能にします。これは、DSLのように、構造化されたニース形式でアクションのシーケンスをモデル化することを可能にします。
そして最大の力は、さまざまな目的に役立つモナドを、アプリケーション内で拡張可能な抽象化に構成することができることにあります。
モナドによるアクションのこの順序付けとスレッド化は、クロージャの魔法を通して変換を行う言語コンパイラによって行われます。
ところで、モナドはFPで使われている計算モデルだけではありません。
カテゴリー理論は多くの計算モデルを提案します。その中で
- 計算のArrowモデル
- 計算のモナドモデル
- 計算の応用モデル
標準のOOP pythonコードとモナドのpythonコードを比較して、基礎となる計算プロセスを図で説明する短い記事を書きました。これは、FPに関するこれまでの知識がないことを前提としています。あなたがそれが役に立つと思います - http://nikgrozev.com/2013/12/10/monads-in-15-minutes/
モナドは値をカプセル化するデータ型です。基本的に、2つの操作を適用できます。
return x
は、x
をカプセル化するモナド型の値を作成します。m >>= f
( "バインド演算子"と読み替えてください)は、関数f
をモナドm
の値に適用します。それがモナドとは何ですか。 さらにいくつかの専門知識 がありますが、基本的にこれら2つの操作はモナドを定義します。本当の質問は「モナドは何をするのですか?」、そしてそれはモナドによります - リストはモナド、Maybesはモナド、IO操作はモナドです。それらがモナドであると言うとき、それが意味するのは、それらがreturn
と>>=
のモナドインターフェースを持っているということだけです。
から ウィキペディア :
関数型プログラミングでは、モナドは(ドメインモデルのデータではなく)計算を表すために使用される一種の抽象データ型です。モナドを使用すると、プログラマは複数のアクションを連鎖させてパイプラインを構築できます。各アクションは、モナドが提供する追加の処理規則で装飾されています。関数スタイルで書かれたプログラムは、シーケンス操作を含む手続きを構造化するためにモナドを利用することができます。 1 [2]または任意の制御フローを定義することができます。 、継続、または例外).
正式には、モナドは、2つの操作(bindとreturn)と、モナド関数(モナドからの値を引数として使用する関数)を正しく合成するためにいくつかのプロパティを満たさなければならない型コンストラクタMを定義することによって構築されます。 returnオペレーションはプレーンタイプから値を取り、それをタイプMのモナディックコンテナに入れます。bindオペレーションは逆のプロセスを実行し、コンテナから元の値を抽出してそれをパイプライン内の関連する次の関数に渡します。
プログラマは、データ処理パイプラインを定義するためにモナド関数を構成します。モナドは、パイプライン内の特定のモナド関数が呼び出される順序を決定し、計算に必要なすべてのアンダーカバー作業を管理する再利用可能な動作であるため、フレームワークとして機能します。パイプライン内でインターリーブされたバインドおよびリターン演算子は、各モナド関数が制御を返した後に実行され、モナドによって処理される特定の側面を処理します。
私はそれがそれを非常によく説明していると信じています。
OOPという用語を使って管理できる最短の定義を作成しようと思います。
ジェネリッククラスCMonadic<T>
は、少なくとも次のメソッドを定義していればモナドです。
class CMonadic<T> {
static CMonadic<T> create(T t); // a.k.a., "return" in Haskell
public CMonadic<U> flatMap<U>(Func<T, CMonadic<U>> f); // a.k.a. "bind" in Haskell
}
以下の法則がすべての型Tとその可能な値tに適用される場合
左のアイデンティティ:
CMonadic<T>.create(t).flatMap(f) == f(t)
正しいアイデンティティ
instance.flatMap(CMonadic<T>.create) == instance
連想性:
instance.flatMap(f).flatMap(g) == instance.flatMap(t => f(t).flatMap(g))
例:
リストモナドは、
List<int>.create(1) --> [1]
そしてリスト[1,2,3]のflatMapはそのように働くことができる:
intList.flatMap(x => List<int>.makeFromTwoItems(x, x*10)) --> [1,10,2,20,3,30]
IterableとObservablesもPromiseとTasksと同様にモナドにすることができます。
解説:
モナドはそれほど複雑ではありません。 flatMap
関数は、より一般的なmap
とよく似ています。関数の引数(デリゲートとも呼ばれます)を受け取ります。この引数は、ジェネリッククラスからの値を使用して呼び出すことができます(すぐに、または後で、0回以上)。渡された関数もその戻り値を同じ種類の総称クラスでラップすることを想定しています。それを手助けするために、値からそのジェネリッククラスのインスタンスを作成できるコンストラクタcreate
が提供されています。 flatMapの戻り結果も同じタイプのジェネリッククラスであり、多くの場合、flatMapの1つ以上のアプリケーションの戻り結果に含まれていたのと同じ値を、以前に含まれていた値にパックします。これにより、必要に応じてflatMapをチェーンすることができます。
intList.flatMap(x => List<int>.makeFromTwo(x, x*10))
.flatMap(x => x % 3 == 0
? List<string>.create("x = " + x.toString())
: List<string>.empty())
この種の総称クラスが膨大な数の事柄のための基本モデルとして有用であることはまさにそのように起こります。これが(カテゴリ理論の専門用語と合わせて)モナドが理解したり説明したりするのが難しいように見える理由です。それらは非常に抽象的なものであり、専門化した後に初めて明らかに役に立つようになります。
たとえば、モナドコンテナを使用して例外をモデル化できます。各コンテナには、操作の結果または発生したエラーが含まれます。 flatMapコールバックのチェーン内の次の関数(デリゲート)は、前の関数がコンテナに値をパックした場合にのみ呼び出されます。それ以外の場合、エラーがパックされた場合、エラーは.orElse()
というメソッドを介してアタッチされたエラーハンドラ関数を持つコンテナが見つかるまで、連鎖コンテナを介して伝播し続けます(そのようなメソッドは許可された拡張子になります)
注:関数型言語を使用すると、あらゆる種類のモナドジェネリッククラスを操作できる関数を書くことができます。これが機能するためには、モナド用の一般的なインタフェースを書く必要があります。そのようなインターフェースをC#で書くことが可能かどうかはわかりませんが、私の知る限りではそうではありません。
interface IMonad<T> {
static IMonad<T> create(T t); // not allowed
public IMonad<U> flatMap<U>(Func<T, IMonad<U>> f); // not specific enough,
// because the function must return the same kind of monad, not just any monad
}
モナドがOOで「自然な」解釈を持つかどうかは、モナドによって異なります。 Javaのような言語では、おそらくモナドをnullポインタをチェックする言語に翻訳することができます。その結果、失敗した(つまりHaskellでNothingを生成した)計算は結果としてnullポインタを生成します。状態モナドを、可変状態の変数とその状態を変更するためのメソッドを作成することによって生成された言語に変換することができます。
モナドは、endofunctorsのカテゴリの中のモノイドです。
文がまとめる情報はとても深いです。そしてあなたはどんな命令言語でもモナドで働いています。モナドは「シーケンス」ドメイン固有の言語です。それは一緒になってモナドを「命令型プログラミング」の数学的モデルにするある種の興味深い性質を満足させる。 Haskellを使用すると、小規模(または大規模)の命令型言語を簡単に定義できます。これらの言語はさまざまな方法で組み合わせることができます。
OOプログラマとして、あなたは自分の言語のクラス階層を使用して、コンテキスト内で呼び出すことができる種類の関数や手続き、つまりオブジェクトと呼ぶものを体系化します。さまざまなモナドを任意の方法で組み合わせることができ、サブモナドのすべてのメソッドをスコープに効果的に「インポート」できる限り、モナドもこの概念の抽象概念です。
アーキテクチャ的には、型シグネチャを使用して、値の計算に使用できるコンテキストを明示的に表現します。
この目的のためにモナド変換子を使うことができ、そしてすべての「標準」モナドの高品質なコレクションがあります。
対応するモナド変換子と型クラスがあります。型クラスは、それらのインターフェースを統合することによってモナドを組み合わせる補完的なアプローチを可能にします。その結果、具体的なモナドはモナドの「種類」のための標準インターフェースを実装できます。たとえば、モジュールControl.Monad.StateはクラスMonadState s mを含み、(State s)は次の形式のインスタンスです。
instance MonadState s (State s) where
put = ...
get = ...
長い話では、モナドは値に「コンテキスト」を結び付けるファンクタであり、モナドに値を注入する方法があり、それに付随するコンテキストに関して値を評価する方法があります。制限された方法で。
そう:
return :: a -> m a
型aの値を型m aのモナド「アクション」に注入する関数です。
(>>=) :: m a -> (a -> m b) -> m b
モナドアクションを取り、その結果を評価し、その結果に関数を適用する関数です。 (>> =)について素晴らしいことは、結果が同じモナドにあるということです。つまり、m >> = fでは、(>> =)はmから結果を取り出し、それをfにバインドするので、結果はモナドになります。 (あるいは、(>> =)がfをmに引き込み、結果に適用すると言うこともできます。)その結果、f :: a - > mbとg :: b - > mcがあれば、 「シーケンス」アクション:
m >>= f >>= g
あるいは、「do記法」を使う
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
もっと重要なことは、それはモナドが "layer stacking"操作の下で閉じられていることを意味します。これはモナド変換子がどのように機能するかです:それらはのような型のために "join-like"メソッドを提供することによってモナドを結合します
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
(MaybeT m)のアクションをmのアクションに変換し、効果的にレイヤを折りたたむことができます。この場合、runMaybeT :: MaybeT m - > m(たぶんa)がjoin-likeメソッドです。 (MaybeT m)はモナドです、そして、MaybeT :: m(たぶんa) - > MaybeT m aは事実上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アクションを順番に実行することによって残りの計算を "推進"します) 。
Joinの型は、モナドがendofunctorsのカテゴリの中のモノイドであるというステートメントを得る場所でもあります。結合は、そのタイプのために、通常は理論上の目的でより重要です。しかしタイプを理解することはモナドを理解することを意味します。結合およびモナド変換子の結合に似た型は、機能構成の観点から、事実上、内部関数の構成要素です。 Haskellのような疑似言語で書くと、
Foo :: m(m a) - (m。m)a
モナドは関数の配列です
(Pst:関数の配列は単なる計算です).
実際には、真の配列(1つのセル配列内の1つの関数)の代わりに、それらの関数が別の関数>> =によって連鎖されています。 >> =は、関数iからの結果を関数i + 1にフィードしたり、それらの間で計算を実行したり、あるいは関数i + 1を呼び出さないようにすることを可能にします。
ここで使用されている型は「コンテキストを持つ型」です。これは、「タグ」を持つ値です。連鎖されている関数は「裸の値」を取り、タグ付けされた結果を返さなければなりません。 >> =の義務の1つは、そのコンテキストから裸の値を抽出することです。 "return"という関数もあります。これは裸の値を取り、それをタグで囲みます。
たぶんの例。計算を行う単純な整数を格納するためにそれを使用しましょう。
-- 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)
Marvelのケーススタディを使った簡単なモナドの説明はここ です 。
モナドは、効果的な依存関数を順序付けるために使用される抽象概念です。ここで有効なのは、それらがフォームコンストラクタと呼ばれる、Option [F]であるOption [A]のような形式F [A]でタイプを返すことを意味します。これを2つの簡単なステップで見てみましょう
A => C = A => B andThen B => C
しかし、関数がOption [A]のようなエフェクトタイプ、つまりA => F [B]を返す場合、コンポジションはBに行くようには機能しません。A=> Bが必要ですが、A => F [B]です。
F [A]を返すこれらの関数をどのように融合するかを知っている特別な演算子 "bind"が必要です。
A => F[C] = A => F[B] bind B => F[C]
"bind" 関数は特定の _ f _ に対して定義されています。
"return" というタイプの A => F [A] もあり、 _ a _ に対しても、その特定の _ f _ に対して定義されています。モナドになるには、 _ f _ にこれらの2つの関数を定義する必要があります。
したがって、任意の純関数 A => B から効果的な関数 A => F [B] を構築できます。
A => F[B] = A => B andThen return
しかし、与えられた _ f _ は、ユーザが自分で定義できないような( pure 言語で)そのようなタイプのそれ自身の不透明な "組み込み"特殊関数も定義できます。
典型的な用法のモナドは、手続き型プログラミングの例外処理メカニズムと機能的に同等です。
現代の手続き型言語では、一連の文の周りに例外ハンドラを置きます。これらの文はいずれも例外をスローする可能性があります。いずれかのステートメントが例外をスローすると、一連のステートメントの通常の実行は停止し、例外ハンドラに転送されます。
しかし、関数型プログラミング言語は、その "goto"のような性質のために、哲学的に例外処理機能を避けます。関数型プログラミングの観点では、関数はプログラムの流れを妨げる例外のような「副作用」を持つべきではありません。
実際には、主にI/Oが原因で、現実の世界で副作用を排除することはできません。関数型プログラミングのモナドは、一連の連鎖された関数呼び出し(そのうちの1つでも予期しない結果が生じる可能性があります)を受け取り、予期しない結果を残りの関数呼び出しを通して安全に流れることができるカプセル化データに変換します。
制御の流れは維持されますが、予期しないイベントは安全にカプセル化されて処理されます。
OOという用語では、モナドは流暢な容器です。
最小要件は、コンストラクターSomething(A a)
と少なくとも1つのメソッドSomething<B> flatMap(Function<A, Something<B>>)
をサポートするclass <A> Something
の定義です。
おそらく、あなたのモナドクラスがクラスの規則を守るシグニチャSomething<B> work()
を持つメソッドを持っているかどうかもカウントします - コンパイラはコンパイル時にflatMapを使います。
モナドはなぜ便利ですか?それは意味論を保存する連鎖可能な操作を可能にするコンテナだからです。たとえば、Optional<?>
は、Optional<String>
、Optional<Integer>
、Optional<MyClass>
などのisPresentのセマンティクスを保持します。
大まかな例として、
Something<Integer> i = new Something("a")
.flatMap(doOneThing)
.flatMap(doAnother)
.flatMap(toInt)
文字列で始まり、整数で終わることに注意してください。かなりクール。
オブジェクト指向では、少し手を振る必要がありますが、Somethingの別のサブクラスを返すSomethingのメソッドは、元の型のコンテナを返すコンテナ関数の基準を満たしています。
つまり、コンテナの意味や操作は変わりません。コンテナ内のオブジェクトをラップして拡張するだけです。
"モナドとは何ですか?"に対する私の 答え を見てください。
それはやる気を起こさせる例から始まり、例を通して働き、モナドの例を導き出し、そして正式に "モナド"を定義します。
これは関数型プログラミングの知識がないことを前提としており、最も単純な式でfunction(argument) := expression
構文の擬似コードを使用します。
このC++プログラムは、擬似コードモナドの実装です。 (参考:M
は型コンストラクタ、feed
は "bind"操作、そしてwrap
は "return"操作です。)
#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;
}
Powershellを使ったことがあるなら、Ericが書いたパターンはおなじみのはずです。 PowerShellコマンドレット はモナドです。機能構成は パイプライン で表されます。
Jeffrey SnoverのErik Meijerとのインタビュー がさらに詳しく説明されています。
私はモナドの理解を共有していますが、これは理論的には完璧ではないかもしれません。モナドはコンテキスト伝播についてです。 Monadは、あるデータのコンテキストを定義し、その処理パイプライン全体でデータと共にコンテキストをどのように運ぶかを定義します。そして、コンテキスト伝播の定義は、主に複数のコンテキストのマージ方法の定義に関するものです。また、コンテキストが誤ってデータから削除されることはありません。この単純な概念は、プログラムのコンパイル時の正確性を保証するために使用できます。
実用的な観点から(これまでの多くの答えと関連記事で述べられてきたことを要約すると)、モナドの基本的な「目的」(または有用性)の1つは再帰的メソッド呼び出しに含まれる依存関係を利用することです。別名関数合成(つまり、f1がf2を呼び出し、f3が呼び出されると、f3はf1より前に評価される必要があります)。特に遅延評価モデルの文脈では、自然な方法で順次合成を表す必要があります。例えば、Cでは "f3(); f2(); f1();" - もしあなたがf3、f2、f1が実際に何も返さない場合を考えれば、トリックは特に明白です[f1(f2(f3)としての連鎖))人工的であり、純粋にシーケンスを作成することを意図している。
これは、副作用が関係している場合、つまり何らかの状態が変更されている場合に特に関係があります(f1、f2、f3に副作用がなければ、それらが評価される順序は関係ありません。これはpureの優れた特性です)これらの計算を並列化できるようにするための関数型言語より純粋な関数は、より良いです。
私はその狭い観点から考えると、(コードの提示に頼らない順序に従って、絶対評価が必要なときにだけ事物を評価する)遅延評価を支持する言語にとっては、モナドは構文上の糖と見なすことができると思います。シーケンシャル構成を表す他の手段その結果、「不純な」(つまり、副作用がある)コードセクションは、命令的な方法で自然に表示できますが、純粋な関数(副作用のない)とは明確に区別されます。遅延評価した。
ただし、警告 がここ にあるので、これは1つの側面にすぎません。