OOPプログラマーが(関数型プログラミングのバックグラウンドなしで)理解できるという点で、モナドとは何ですか?
それはどのような問題を解決し、それが使用される最も一般的な場所は何ですか?
編集:
私が探していた理解の種類を明確にするために、モナドを持つFPアプリケーションをOOPアプリケーションに変換するとします。モナドの責任をOOPアプリに移植するにはどうしますか?
OOPプログラマーが(関数型プログラミングのバックグラウンドなしで)理解できるという点で、モナドとは何ですか?
それはどのような問題を解決し、それが使用される最も一般的な場所は何ですか?
編集:
私が探していた理解の種類を明確にするために、モナドを持つFPアプリケーションをOOPアプリケーションに変換するとします。モナドの責任をOOPアプリに移植するにはどうしますか?
回答:
更新:この質問は、Monadsで読むことができる非常に長いブログシリーズの主題でした。すばらしい質問をありがとう!
OOPプログラマーが(関数型プログラミングのバックグラウンドなしで)理解できるという点で、モナドとは何ですか?
モナドはあるタイプの「増幅器」特定の規則に従うと、提供される特定の動作を有します。
まず、「タイプのアンプ」とは何ですか?つまり、型を取得してそれをより特殊な型に変換できるシステムを意味します。たとえば、C#ではを検討してくださいNullable<T>
。これはタイプのアンプです。たとえばint
、型を取得して、その型に新しい機能を追加できます。つまり、以前はできなかったときにnullにできるようになりました。
2番目の例として、を検討してくださいIEnumerable<T>
。タイプのアンプです。たとえば、型を取得して、string
その型に新しい機能を追加できます。つまり、任意の数の単一の文字列から文字列のシーケンスを作成できるようになります。
「特定のルール」とは何ですか?簡単に言うと、基礎となる型の関数が、関数型合成の通常の規則に従うように、増幅型で機能する賢明な方法があるということです。たとえば、整数に対する関数がある場合、
int M(int x) { return x + N(x * 2); }
次に、対応する関数on Nullable<int>
は、そこで行われたすべての演算子と呼び出しを、以前と同じように連動させます。
(これは信じられないほどあいまいで不正確です。機能構成の知識について何も想定していない説明を求めました。)
「操作」とは何ですか?
プレーン型から値を取り、同等のモナド値を作成する「ユニット」操作(混乱して「リターン」操作と呼ばれることもあります)があります。これは、本質的に、非増幅型の値を取り、それを増幅型の値に変換する方法を提供します。オブジェクト指向言語のコンストラクターとして実装できます。
モナドの値と値を変換できる関数を受け取り、新しいモナドの値を返す「バインド」演算があります。バインドは、モナドのセマンティクスを定義する主要な操作です。これにより、前述の関数構成の規則に従って、非増幅型の演算を増幅型の演算に変換できます。
多くの場合、非増幅型を増幅型から戻す方法があります。厳密に言えば、この操作はモナドを持つ必要はありません。(ただし、コマンドを使用する場合は必要です。この記事ではこれについては考慮しません。)
もう一度、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
to があるとします。これは、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);
}
あなたはそれで何ができるか見てください?を受け取って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>
。しかし、「バインド」操作があるので、これを機能させることができます。
Nullable<int> Z(int s) { return Bind(Y(s), X); }
モナドのバインド操作は、増幅された型の関数の構成を機能させるものです。上記で私が手を振った「ルール」は、モナドが通常の関数構成のルールを保持するということです。恒等関数で構成すると、元の関数になり、その構成は連想になります。
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を生成し、そうでない場合は、外側の関数を結果で呼び出します」。これが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#はその設計でモナドを使用します。すでに述べたように、ヌル可能パターンは「たぶんモナド」に非常に似ています。LINQは完全にモナドで構築されています。SelectMany
この方法は、オペレーションの構図の意味の作業を行うものです。(Erik Meijerは、すべてのLINQ関数が実際にによって実装される可能性があることを指摘するのが好きSelectMany
です。それ以外はすべて便利です。)
私が探していた理解の種類を明確にするために、モナドを持つFPアプリケーションをOOPアプリケーションに変換するとします。モナドの責任をOOPアプリに移植するにはどうしますか?
ほとんどのOOP言語には、モナドパターン自体を直接表現するための十分な型システムがありません。ジェネリック型よりも高い型である型をサポートする型システムが必要です。だから私はそれをしようとはしません。むしろ、各モナドを表すジェネリック型を実装し、必要な3つの操作を表すメソッドを実装します。値を増幅値に変換する(たぶん)増幅値を値に変換する、および非増幅値の関数をに変換する増幅された値の関数。
開始するには、C#でLINQを実装する方法が適切です。SelectMany
メソッドを研究します。これは、C#でシーケンスモナドがどのように機能するかを理解するための鍵です。これは非常に単純な方法ですが、非常に強力です!
推奨、さらに読む:
次に、最初の大きな問題があります。これはプログラムです:
f(x) = 2 * x
g(x,y) = x / y
最初に何を実行するかをどのように言うことができ ますか?関数だけを使用して、順序付けられた関数のシーケンス(つまり、プログラム)をどのように形成できますか?
解決策:関数の作成。最初に必要な場合g
はf
、次のように書いてくださいf(g(x,y))
。OK、でも...
その他の問題:一部の関数は失敗する可能性があります(つまりg(2,0)
、0で除算)。FPには「例外」はありません。どうすれば解決できますか?
解決策:関数が2つの種類のものを返すことを許可しましょう:g : Real,Real -> Real
(2つの実数から1つの実数への関数)を持つ代わりに、(g : Real,Real -> Real | Nothing
2つの実数から(実数または何もない)への関数)を許可しましょう。
しかし、関数は(より簡単に)1つのものだけを返す必要があります。
解決策:返される新しいタイプのデータを作成してみましょう。「ボクシングタイプ」は、実在するものか、まったく存在しないものを囲みます。したがって、私たちはを持つことができますg : Real,Real -> Maybe Real
。OK、でも...
今はどうなりf(g(x,y))
ますか?f
を使用する準備ができていませんMaybe Real
。そして、私たちはと接続することができ、すべての機能を変更したくないg
消費するようにMaybe Real
。
解決策:関数を「接続」/「作成」/「リンク」する特別な関数を用意しましょう。このようにして、背後で1つの関数の出力を調整して、次の関数にフィードできます。
私たちの場合:(に g >>= f
接続/構成g
するf
)。の出力>>=
を取得しg
、それを検査したい場合は、Nothing
単に呼び出しf
て返さない場合に限りますNothing
。または逆に、箱に入っているものを取り出し、それと一緒にReal
餌f
をやります。(このアルゴリズムは>>=
、Maybe
型のの実装にすぎません)。
同じパターンを使用して解決できる他の多くの問題が発生します。1.「ボックス」を使用してさまざまな意味/値をコード化/保存し、g
それらの「ボックス化された値」を返すような関数を使用します。2. の出力をの入力にg >>= f
接続するのに役立つコンポーザー/リンカーがあるため、まったく変更する必要はありません。g
f
f
この手法を使用して解決できる注目すべき問題は次のとおりです。
関数のシーケンス内のすべての関数(「プログラム」)が共有できるグローバルな状態:ソリューションStateMonad
。
「純粋でない関数」:同じ入力に対して異なる出力を生成する関数は好きではありません。したがって、これらの関数にマークを付けて、タグ付き/ボックス化された値を返すようにします:モナド。IO
トータルハピネス!!!!
State
とIO
特定されたそれらのどれも同様に「モナド」の正確な意味で、
モナドに最も近いオブジェクト指向のアナロジーは「コマンドパターン」です。
コマンドパターンでは、通常のステートメントまたは式をコマンドオブジェクトでラップします。コマンドオブジェクトは、ラップされたステートメントを実行する実行メソッドを公開します。したがって、ステートメントは、自由に渡して実行できるファーストクラスオブジェクトに変換されます。コマンドを構成して、コマンドオブジェクトをチェーンおよびネストしてプログラムオブジェクトを作成できます。
コマンドは、個別のオブジェクトであるinvokerによって実行されます。(一連の通常のステートメントを実行するだけでなく)コマンドパターンを使用する利点は、呼び出し元ごとにコマンドの実行方法に異なるロジックを適用できることです。
コマンドパターンを使用して、ホスト言語でサポートされていない言語機能を追加(または削除)できます。たとえば、例外のない架空のオブジェクト指向言語では、「try」メソッドと「throw」メソッドをコマンドに公開することで、例外セマンティクスを追加できます。コマンド呼び出しがスローする場合、呼び出し元は最後の「試行」呼び出しまでコマンドのリスト(またはツリー)を逆戻りします。逆に、個々のコマンドによってスローされたすべての例外をキャッチし、それらをエラーコードに変換して次のコマンドに渡すことにより、言語から例外セマンティックを削除できます(例外が悪いと思われる場合)。
トランザクション、非決定的実行、または継続などのさらに豪華な実行セマンティクスは、ネイティブではサポートされていない言語でこのように実装できます。考えてみればかなりパワフルなパターンです。
現在、実際には、コマンドパターンはこのような一般的な言語機能として使用されていません。各ステートメントを個別のクラスに変換するオーバーヘッドは、耐えがたい量の定型コードにつながります。しかし、原則として、モナドがfpで解決するために使用されるのと同じ問題を解決するために使用できます。
OOPプログラマーが(関数型プログラミングのバックグラウンドなしで)理解できるという点で、モナドとは何ですか?
それはどのような問題を解決し、それが最もよく使用される場所は何ですか?それが最もよく使用される場所ですか?
オブジェクト指向プログラミングの観点から、モナドは、2つのメソッドを持つタイプによってパラメーター化されたインターフェース(または、より可能性の高いミックスイン)でreturn
あり、以下bind
を記述します。
それが解決する問題は、どのインターフェイスからも予想されるのと同じタイプの問題です。つまり、「私はさまざまなことをするさまざまなクラスがたくさんありますが、根本的な類似性を持つ方法でそれらのさまざまなことをしているようです。方法クラス自体が実際には「オブジェクト」クラス自体よりも近いもののサブタイプでなくても、それらの類似性を説明できますか?」
より具体的には、Monad
「インターフェース」は、それ自体が型をとる型と似ている、IEnumerator
またはIIterator
それをとるという点で似ています。Monad
ただし、メインの「ポイント」は、メインのクラスの情報構造を維持または強化しながら、内部タイプに基づいて操作を新しい「内部タイプ」を持つポイントに接続できることです。
return
モナドのインスタンスを引数として取りませんので、実際にはモナドのメソッドではありません。(つまり、これ/自分はありません)
あなたは最近のプレゼンテーション「Monadologie-型不安に関する専門家の助け」をChristopher League(2010年7月12日)で発表しました。これは継続とモナドのトピックについて非常に興味深いものです。
この(スライドシェア)プレゼンテーションに関連するビデオは、実際にはvimeoで入手できます。
モナドのパートは、この1時間のビデオで約37分で始まり、58枚のスライドのプレゼンテーションのスライド42から始まります。
「関数型プログラミングの主要な設計パターン」として提示されていますが、例で使用されている言語はOOPであり関数型でもあるScalaです。
あなたはブログ記事「でScalaでモナドで続きを読むことができます- Scalaでは抽象計算する別の方法モナドから、」Debasishゴーシュ(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):Option [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):List [A] = List(x) def flatMap [A、B](m:List [A])(f:A => List [B]):List [B] = mマッチ{ ケースNil => Nil ケースx :: xs => f(x)::: flatMap(xs)(f) }
Monadの構造を利用するために構築された便利な構文のため、Monadは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内包表記のシーケンスのすべての項目は、この同じ型の制約を守る必要があります。
次のように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
}
一方、モナドでは、すべての操作が機能するように実際の型(Venue
、User
)を操作し、オプションの検証を非表示にしておくことができます。これは、すべてfor構文のフラットマップが原因です。
val confirm = for {
venue <- lookupVenue(name)
user <- getLoggedInUser(session)
confno <- reserveTable(venue, user)
} yield {
mailTo(confno, user)
confno
}
収量部分は、3つの関数すべてにがある場合にのみ実行されますSome[X]
。いずれもNone
直接に返されconfirm
ます。
そう:
モナドを使用すると、関数型プログラミング内で順序付けされた計算が可能になります。これにより、DSLのように、構造化された形式でアクションのシーケンスをモデル化できます。
そして、最大の力は、さまざまな目的に役立つモナドを、アプリケーション内の拡張可能な抽象化に構成する機能にあります。
モナドによるこのシーケンスとアクションのスレッド化は、クロージャの魔法を通して変換を行う言語コンパイラによって行われます。
ちなみに、モナドは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)
に渡すルールf:X->T(Y)
g:Y->T(Z)
h:X->T(Z)
であり、2つの関数と(作成したいができない)新しい関数に渡すルールです。しかし、これは厳密な数学的意味での構成ではありません。基本的に、関数の構成を「曲げる」か、関数の構成方法を再定義します。
さらに、「明白な」数理公理を満たすためには、モナドの構成規則が必要です。
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
または何が、本質は同じまま-彼らはそれはすべきではない例えば、新しい値である必要がありnumber
、ここで私たちの例では。特定の言語でどのように実装できるnull
かとの混乱を避けるために、それらを呼び出さないようにしていnull
ます。同様に私は避けることを好むnothing
ことが多い関連付けられているためnull
、原理的には、何をしている、null
行う必要があります、しかし、その原理は、多くの場合、どのような実際的な理由のために曲げられます。
これは経験豊富なプログラマにとってはささいなことですが、混乱のワームを消滅させるために少しだけ語りたいと思います。
例外は、無効な実行結果が発生した方法に関する情報をカプセル化するオブジェクトです。
これは、詳細を破棄して単一のグローバル値(NaN
またはnull
)を返すことから、長いログリストまたは正確に何が起こったかを生成してデータベースに送信し、分散データストレージレイヤー全体に複製することまで、さまざまです;)
これら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
これまでに戻る関数は明らかに純粋です。しかし、狂った誰かが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の実装から生じる関数を構成するルールは何ですか?
この質問に答えるには、公理をチェックするだけです(ここでは、質問の一部ではなく、演習として残します)。
実際、より有用なモナドは代わりにif f
がsome x
に対して例外をスローするように規定するルールであり、そのため、anyを使用してその構成を行いますg
。さらに、例外をE
グローバルに一意にして、考えられる値を1つだけにします(カテゴリ理論の最終オブジェクト)。これで2つの公理は即座にチェック可能になり、非常に便利なモナドが得られます。そしてその結果は、多分モナドとして知られているものです。
モナドは値をカプセル化するデータ型であり、基本的に2つの操作を適用できます。
return x
カプセル化するモナド型の値を作成します x
m >>= f
(「バインド演算子」と読みます)f
モナドの値に関数を適用しますm
それがモナドです。さらにいくつかの技術がありますが、基本的にこれらの2つの演算はモナドを定義します。本当の質問は、「モナドは何をするのか?」であり、それはモナドに依存します。リストはモナドであり、たぶんモナドであり、IO操作はモナドです。私たちはそれらのものがモナドであると言うとき、それが意味するすべては、彼らがのモナドのインタフェースを持っていることであるreturn
と>>=
。
bind
各モナド型に対して定義されなければならない関数に依存しますね?バインドの構成が1つしかなく、バインド関数の定義が1つだけではなく、モナディックタイプごとに1つあるため、バインドと構成を混同しないようにしてください。
ウィキペディアから:
関数型プログラミングでは、モナドは、(ドメインモデルのデータの代わりに)計算を表すために使用される一種の抽象的なデータ型です。モナドを使用すると、プログラマーはアクションをチェーンしてパイプラインを構築できます。パイプラインでは、各アクションがモナドによって提供される追加の処理ルールで装飾されます。関数型で記述されたプログラムは、モナドを使用して、シーケンス演算を含む手続きを構築できます1 [2]たり、任意の制御フロー(同時実行、継続、例外の処理など)を定義したりできます。
正式には、モナドは2つの操作(バインドとリターン)と型コンストラクタMを定義することによって構築されます。型コンストラクタMは、モナド関数(モナドの値を引数として使用する関数)の正しい構成を可能にするいくつかのプロパティを満たす必要があります。戻り操作は、プレーンタイプから値を取得し、タイプMのモナディックコンテナーに入れます。バインド操作は、逆のプロセスを実行し、コンテナーから元の値を抽出して、パイプライン内の関連する次の関数に渡します。
プログラマーはデータ処理パイプラインを定義するモナド関数を作成します。モナドは、パイプライン内の特定のモナド関数が呼び出される順序を決定し、計算に必要なすべての潜伏作業を管理する再利用可能な動作であるため、フレームワークとして機能します。[3] パイプラインでインターリーブされたバインド演算子と戻り演算子は、各モナド関数が制御を返した後に実行され、モナドによって処理される特定の側面を処理します。
それはそれを非常によく説明していると思います。
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]
IterablesとObservablesは、約束とタスクだけでなく、モナドにすることもできます。
解説:
モナドはそれほど複雑ではありません。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
}
モナドがオブジェクト指向で「自然な」解釈を持つかどうかは、モナドに依存します。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の値をタイプ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 notation」を使用します
do x <- m
y <- f x
g y
(>>)のタイプは照らしている可能性があります。です
(>>) :: m a -> m b -> m b
これは、Cのような手続き型言語の(;)演算子に対応しています。
m = do x <- someQuery
someAction x
theNextAction
andSoOn
数学的および哲学的ロジックには、モナディズムで「自然に」モデル化されたフレームとモデルがあります。解釈とは、モデルのドメインを調べ、命題(または一般化の下での公式)の真理値(または一般化)を計算する関数です。必要に応じたモーダルロジックでは、「すべての可能な世界」でそれが真である場合、つまりすべての許容可能なドメインに関して真である場合、命題が必要であると言えます。これは、命題のための言語のモデルは、そのドメインが別個のモデル(考えられる各世界に対応するモデル)のコレクションで構成されるモデルとして具体化できることを意味します。すべてのモナドには、レイヤーを平坦化する「結合」という名前のメソッドがあります。これは、結果がモナドアクションであるすべてのモナドアクションをモナドに埋め込むことができることを意味します。
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(Maybea)-> 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
モナドは関数の配列です
(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)
>>=
オペレーターです
\x -> x >>= k >>= l >>= m
関数の配列である、そうであるh . g . f
すべてのモナドを伴いません、。
オブジェクト指向の用語では、モナドは流暢なコンテナです。
最小要件は、class <A> Something
コンストラクタSomething(A a)
と少なくとも1つのメソッドをサポートするの定義です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が原因で、現実の世界で副作用を排除することはできません。関数型プログラミングのモナドを使用して、一連の連鎖関数呼び出し(予期しない結果を生成する可能性がある)を取り、予期しない結果をカプセル化されたデータに変換して、残りの関数呼び出しを安全に通過できるようにします。
制御のフローは保持されますが、予期しないイベントは安全にカプセル化されて処理されます。
Marvelのケーススタディを使用した簡単なモナドの説明はこちらです。
モナドは、効果的な従属関数の順序付けに使用される抽象化です。ここで効果的とは、型コンストラクターと呼ばれる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]を返すこれらの関数を融合する方法を知っている特別な演算子「バインド」が必要です。
A => F[C] = A => F[B] bind B => F[C]
「バインド」機能は、特定のために定義されるF。
また、特定のFに対して定義された、任意のAに対してタイプA => F [A]の「return」もあります。モナドになるために、Fはこれらの2つの関数を定義する必要があります。
したがって、純粋な関数A => Bから効果的な関数A => F [B]を構築できます。
A => F[B] = A => B andThen return
ただし、特定のFは、ユーザーが自分で(純粋な言語で)定義できないような、そのようなタイプの独自の不透明な「組み込み」特殊関数を定義することもできます。
理論的に完全ではないかもしれないモナドについての私の理解を共有しています。モナドは、コンテキストの伝播に関するものです。モナドは、いくつかのデータ(またはデータ型)のコンテキストを定義し、そのコンテキストがデータとともに処理パイプライン全体にどのように運ばれるかを定義します。そして、コンテキスト伝播の定義は、ほとんどの場合、(同じタイプの)複数のコンテキストをマージする方法を定義することです。モナドを使用すると、これらのコンテキストが誤ってデータから削除されないようにすることも意味します。一方、他のコンテキストレスデータは、新規または既存のコンテキストに取り込むことができます。次に、この単純な概念を使用して、プログラムのコンパイル時の正確さを保証できます。
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;
}
実用的な観点(これまでの多くの回答と関連記事で述べられていることを要約する)から、モナドの基本的な「目的」(または有用性)の1つは、再帰的なメソッド呼び出しで暗黙的な依存関係を利用することであるように思えます別名関数構成(つまり、f1がf2を呼び出し、f3を呼び出す場合、f1の前にf2の前にf3を評価する必要があります)は、特に遅延評価モデルのコンテキスト(つまり、単純なシーケンスとしての順次構成)で自然に順次構成を表す、たとえばCの「f3(); f2(); f1();」-f3、f2、f1が実際には何も返さない場合を考えると、トリックは特に明白です[それらのチェーンはf1(f2(f3))として連鎖します)人工的なものであり、純粋にシーケンスを作成することを目的としています])。
これは、副作用が関係している場合、つまり一部の状態が変更された場合に特に関連します(f1、f2、f3に副作用がない場合、それらが評価される順序は関係ありません。これは、pureの優れたプロパティです。これらの計算を並列化できるようにするための関数型言語など)。より純粋な関数であるほど良いです。
その狭い観点から、モナドは遅延評価を好む言語(絶対に必要な場合にのみ物事を評価し、コードの表示に依存しない順序に従う)の構文糖と見なすことができ、シーケンシャルコンポジションを表す他の方法。最終的な結果は、「純粋でない」(つまり、副作用がある)コードのセクションは、命令型の方法で自然に提示でき、しかも純粋な関数(副作用のない)から明確に分離できるということです。怠惰に評価した。
ここで警告されているように、これは1つの側面にすぎません。
私が考えることができる最も簡単な説明は、モナドは装飾された結果(別名Kleisli合成)で関数を構成する方法であるということです。「embelished」関数には、a -> (b, smth)
どこにa
、そしてb
タイプ(思うありInt
、Bool
互いに異なるかもしれない)、必ずしもそうではない-とsmth
「コンテキスト」または「エンベリッシュメント」です。
このタイプの関数はa -> m b
、次の場所にも記述できます。m
、「埋め込み」と同等のsmth
ます。したがって、これらはコンテキストで値を返す関数です(アクションをログに記録する関数(smth
ログメッセージはです)。または、入出力を実行する関数とその結果は、IOアクションの結果によって異なります)。
モナドは、そのような関数を作成する方法を実装者に指示するインターフェース(「タイプクラス」)です。実装者は合成関数を定義する必要があります(a -> m b) -> (b -> m c) -> (a -> m c)
m
実装者は、インターフェースを実装したいすべての型のあります(これはKleisli構成です)。
したがって、s (Int, String)
の計算結果を表すタプルタイプがあり、Int
そのアクションもログに記録されます。これ(_, String)
は、「埋め込み」-アクションのログ-と2つの関数increment :: Int -> (Int, String)
とtwoTimes :: Int -> (Int, String)
我々は、機能取得するincrementThenDouble :: Int -> (Int, String)
組成物でありますログも考慮に入れる2つの関数の。
与えられた例では、2つの関数のモナド実装は整数値2 incrementThenDouble 2
(これはに等しいtwoTimes (increment 2)
)に適用され(6, " Adding 1. Doubling 3.")
、中間結果increment 2
は次(3, " Adding 1.")
とtwoTimes 3
等しいか等しい(6, " Doubling 3.")
このクライスリ合成関数から、通常のモナド関数を導出できます。