関数型プログラミングでは、モナドは計算を一連のステップとして構造化する方法です。各ステップでは、値に加えて、潜在的な失敗、非決定性、副作用などの計算に関する追加情報が生成されます。より厳密には、モナドは、値をモナドのコンテキストに持ち上げ、モナド計算を連鎖させる2つの操作を備えた型コンストラクタMです。より簡単に言えば、モナドは型コンストラクタに実装されたインターフェースと考えることができ、関数がモナドを実装するさまざまな型コンストラクタのバリアント(例: 、など)を抽象化できるようにします。[ 1 ] [ 2 ]return:<A>(a:A)->M(A)bind:<A,B>(m_a:M(A),f:A->M(B))->M(B)OptionList
モナドの概念と用語はどちらも元々は圏論に由来し、圏論ではモナドは追加構造を持つ自己関手として定義されます。 [ a ] [ b ] 1980年代後半から1990年代初頭にかけての研究により、モナドは一見異質なコンピュータサイエンスの問題を統一された関数型モデルの下にまとめることができることが明らかになりました。圏論はまた、モナド法則として知られるいくつかの形式的な要件を提供しており、これはすべてのモナドが満たすべきものであり、モナドコードの検証に使用できます。[ 3 ] [ 4 ]
モナドはある種の計算の意味を明示するため、便利な言語機能を実装するためにも使用できます。Haskellなどの一部の言語では、コアライブラリに一般的なモナド構造と共通インスタンスの事前定義を提供しています。[ 1 ] [ 5 ]
「モナドの場合m、型の値は、モナドのコンテキスト内でm a型の値にアクセスできることを意味します。」—CA McCann [ 6 ]a
より正確に言うと、モナドは、シナリオ固有の理由で値への無制限のアクセスが不適切な場合に使用できます。Maybe モナドの場合は、値が存在しない可能性があるためです。入出力( IO) モナドの場合は、モナドがプロンプトが表示された後でのみ提供されるユーザー入力を表す場合など、値がまだ不明である可能性があるためです。いずれの場合も、アクセスが意味をなすシナリオは、モナドに定義されている bind 操作によって捉えられます。Maybe モナドでは、値が存在する場合にのみバインドされ、IO モナドでは、シーケンス内の以前の操作が実行された後にのみ値がバインドされます。
モナドは、型コンストラクタMと2つの操作を定義することによって作成できます。
return :: a -> M a(しばしばユニットとも呼ばれる)は、型の値を受け取りa、それを型のモナド値M aにラップし、bind :: (M a) -> (a -> M b) -> (M b)(通常は と表記される>>=)は、型のモナド値と、基本型 の値を受け入れるM a関数を受け取ります。Bind は をアンラップし、を適用し、 の結果をモナド値として処理できます。faM affM b (演算子の代わりに関数を用いた、同等の代替構造については、後のセクション「ファンクタからの導出」を参照してください。)joinbind
これらの要素を用いて、プログラマは式の中で複数のバインド演算子を連結した一連の関数呼び出し(「パイプライン」)を構成します。各関数呼び出しは入力されたプレーン型の値を変換し、バインド演算子は返されたモナド値を処理し、その値がシーケンスの次のステップに渡されます。
通常、バインド演算子には、>>=引数として受け取った関数では利用できない追加の計算ステップを実行する、モナド固有のコードが含まれる場合があります。合成された関数呼び出しの各ペアの間で、バインド演算子は、m a関数内ではアクセスできない追加情報をモナド値に注入しf、パイプラインに沿って渡すことができます。また、特定の条件下でのみ関数を呼び出したり、関数呼び出しを特定の順序で実行したりするなど、実行フローをより細かく制御することもできます。
モナドの一例として、Maybe型が挙げられます。未定義の null 結果は、多くの手続き型言語が対処するための専用ツールを提供していないため、特に厄介な問題です。未定義値を処理するには、 null オブジェクト パターンMaybeを使用するか、各操作で無効な値をテストするチェックを行う必要があります。これはバグの原因となり、エラーを適切に処理する堅牢なソフトウェアの構築を困難にします。型は、結果の 2 つの状態 ( Just ⌑result⌑、または) を明示的に定義することで、プログラマNothingにこれらの潜在的に未定義の結果に対処することを強制します。たとえば、プログラマは中間結果を返すパーサーを構築しているかもしれません。そうでない場合は、パーサーが検出した条件を通知し、プログラマもその条件を処理する必要があります。少しだけ関数型プログラミングの要素を加えるだけで、このMaybe型は完全な機能を持つモナドへと変化します。[ c ] : 12.3 148–151 ページ
ほとんどの言語では、Maybe モナドはオプション型とも呼ばれ、値が含まれているかどうかを示す型です。通常、列挙型として表現されます。Rust プログラミング言語では、この型は と呼ばれ、この型のバリアントは、汎用型Option<T>の値、または空のバリアントのいずれかになります。TNone
// <T> は汎用型 "T" を表しますenum Option < T > { Some ( T ), None , }Option<T>Maybe型は「ラッパー」型と理解することもでき、ここにモナドとの関連性があります。Maybe型を持つ言語では、モナド関数同士を合成したり、Maybe型に値が含まれているかどうかをテストしたりするなど、Maybe型の使用を支援する関数が用意されています。
次のハードコードされた例では、Maybe 型が失敗する可能性のある関数の結果として使用されています。この場合、ゼロ除算が発生した場合は、この型は何も返しません。
fn divide ( x : Decimal , y : Decimal ) -> Option < Decimal > { if y == 0 { None } else { Some ( x / y ) } } // divide(1.0, 4.0) -> は Some(0.25) を返します// divide(3.0, 0.0) -> は None を返しますMaybeに値が含まれているかどうかをテストする方法の1つは、ifステートメントを使用することです。
let m_x = divide ( 3.14 , 0.0 ); // 上記の divide 関数を参照// if 文は、m_x が Maybe の Just バリアントの場合、m_x から x を抽出しますif let Some ( x ) = m_x { println! ( "回答: {}" , x ); } else { println! ( "除算に失敗しました。ゼロ除算エラーです..." ); }他の言語にもパターンマッチング機能があるかもしれない
let result = divide ( 3.0 , 2.0 ); match result { Some ( x ) => println! ( "答え: {}" , x ), None => println! ( "割り算に失敗しました。次回は成功します。" ), }モナドは、Maybeを返す関数を合成して組み合わせることができます。具体的な例としては、1つの関数が複数のMaybeパラメータを受け取り、いずれかのパラメータがNothingの場合に値がNothingとなる単一のMaybeを返す、といったものが考えられます。
fn chainable_division ( maybe_x : Option < Decimal > , maybe_y : Option < Decimal > ) -> Option < Decimal > { let x = maybe_x ? ; let y = maybe_y ? ;// 両方の入力が None でない場合、// ゼロ除算をチェックし、それに応じて除算します// それ以外の場合は None を返しますif y == 0 { None } else { Some ( x / y ) } } chainable_division ( chainable_division ( Some ( 2.0 ), Some ( 0.0 )), Some ( 1.0 )); // chainable_division の内部では失敗し、外部では None を返します 式を繰り返す代わりに、バインドSome演算子と呼ばれるものを使用できます。(「map」、「flatmap」、「shove」とも呼ばれます[ 8 ] : 2205s)。この操作は、モナドとモナドを返す関数を受け取り、渡されたモナドの内部値に対して関数を実行し、関数からモナドを返します。
// Rust の ".map" を使用した例。maybe_x は、それぞれ Decimal と String を返す 2 つの関数に渡されます。//通常の関数合成と同様に、互いに入力される関数の入力と出力は、ラップされた型と一致する必要があります。(つまり、add_one 関数は Decimal を返す必要があり、それを decimal_to_string 関数に渡すことができます)let maybe_x : Option < Decimal > = Some ( 1.0 ); let maybe_result = maybe_x . map ( add_one ). map ( decimal_to_string );Haskellには、関数合成に似た、より洗練された形式でこのモナド合成を可能にする演算子bind、または( )が存在する。[ d ]: 150-151>>=
halve :: Int -> Maybe Int halve x | even x = Just ( x ` div ` 2 ) | odd x = Nothing -- このコードは x を 2 回半分にします。x が 4 の倍数でない場合は Nothing と評価されますhalve x >>= halveが>>=利用可能であれば、匿名関数chainable_division(つまりラムダ式)を使ってはるかに簡潔に表現できます。以下の式では、2 つのネストされたラムダ式がそれぞれ、渡されたモナド内のラップされた値に対して、bind 演算子を使用してどのように操作しているかに注目してください。 [ e ] : 93Maybe
chainable_division ( mx , my ) = mx >>= ( λx -> my >>= ( λy -> Just ( x / y )) )これまで示してきたのは基本的にモナドですが、より簡潔にするために、次のセクションで定義されるモナドに必要な特性の厳密なリストを以下に示します。
Maybe)[ c ]: 148–151Just(x))[ e ]: 93>>=または.flatMap())[ d ]: 150–151これらはモナドを形成するために必要な3つの要素です。他のモナドは異なる論理プロセスを具現化したり、追加のプロパティを持つ場合もありますが、それらはすべてこの3つの類似した要素を持ちます。[ 1 ] [ 9 ]
上記の例で使用されている、関数型プログラミングにおけるモナドのより一般的な定義は、実際には圏論の標準的な定義ではなく、クライスリの三つ組⟨T, η, μ⟩に基づいています。しかし、この2つの構成要素は数学的に等価であるため、どちらの定義でも有効なモナドが得られます。適切に定義された基本型TとUが与えられた場合、モナドは次の3つの部分から構成されます。
unit : T → M T[ g ]>>=と呼ばれるメソッドで表現され、モナド変数を展開し、それをモナド関数/式に挿入して、新しいモナド値を生成します。しかし、完全なモナドとして認められるためには、これら3つの要素はいくつかの法則も遵守しなければならない。
代数的に言えば、これは任意のモナドが、ファンクターの圏(値から計算への圏)において、圏(クライスリ圏と呼ばれる)とモノイドの両方を生み出すことを意味し、モノイドではモナド合成が二項演算子として、単位がモノイドの恒等演算子として存在します[ 8 ]: 2450s 。
モナドパターンの価値は、単にコードを簡潔にし、数学的推論へのリンクを提供するだけにとどまりません。開発者がどの言語やデフォルトのプログラミングパラダイムを使用していても、モナドパターンに従うことで、純粋関数型プログラミングの多くの利点が得られます。特定の種類の計算を具体化することで、モナドはその計算パターンの面倒な詳細をカプセル化するだけでなく、宣言的な方法でカプセル化することでコードの明瞭性を向上させます。モナド値は計算された値だけでなく、計算された効果も明示的に表すため、モナド式は純粋式と同様に、参照透過的な位置でその値に置き換えることができ、書き換えに基づく多くのテクニックや最適化が可能になります。[ 4 ]
通常、プログラマーはbindを使用してモナド関数をシーケンスに連結します。そのため、モナドを「プログラム可能なセミコロン」と表現する人もいます。これは、多くの命令型言語がセミコロンを使用してステートメントを区切る方法に由来しています。[ 1 ] [ 5 ] しかし、モナドは実際には計算の順序を決定しません。モナドを中心的な機能として使用する言語でも、より単純な関数合成によってプログラム内のステップを配置できます。モナドの一般的な有用性は、抽象化によってプログラムの構造を簡素化し、関心の分離を改善することにあります。[ 4 ] [ 11 ]
モナド構造は、デコレータパターンの数学的かつコンパイル時における独自のバリエーションと見なすこともできます。一部のモナドは関数からアクセスできない追加データを渡すことができ、また、特定の条件下でのみ関数を呼び出すなど、実行をより細かく制御できるものもあります。アプリケーションプログラマが定型コードを事前に開発されたモジュールにオフロードしながらドメインロジックを実装できるため、モナドはアスペクト指向プログラミングのツールとみなすこともできます。[ 12 ]
モナドのもう1つの注目すべき用途は、純粋関数型コードにおいて、入力/出力や可変状態などの副作用を分離することです。純粋関数型言語でも、関数合成と継続渡しスタイル(CPS)の複雑な組み合わせによって、モナドなしでこれらの「不純な」計算を実装できます。 [ 2 ] しかし、モナドを使用すると、CPSコード内の繰り返しパターンをそれぞれ別のモナドにまとめることで、この足場の多くを抽象化できます。[ 4 ]
言語がデフォルトでモナドをサポートしていない場合でも、パターンを実装することは可能であり、多くの場合、それほど困難ではありません。圏論からプログラミング用語に翻訳すると、モナド構造は汎用的な概念であり、境界付き多相性の同等の機能をサポートする言語であれば、どの言語でも直接定義できます。概念が基となる型を操作しながら操作の詳細について中立性を保つことができるのは強力ですが、モナドの独自の機能と厳密な動作は、他の概念とは一線を画しています。[ 13 ]
特定のモナドに関する議論は、通常、特定の計算形式を表すモナドの限定的な実装問題の解決に焦点を当てます。しかし、状況によっては、アプリケーションはコアロジック内で適切なモナドを使用することで、より高レベルの目標を達成することも可能です。
以下に、モナドを設計の中核に据えているアプリケーションのほんの一例を示します。
プログラミングにおける「モナド」という用語は、純粋関数型プログラミング言語であるAPLとJに由来する。しかし、これらの言語では、「モナド」は1つのパラメータを取る関数の略記にすぎない(2つのパラメータを持つ関数は「ダイアド」など)。[ 19 ]
数学者のロジャー・ゴドマンは、1950年代後半にモナドの概念を初めて定式化し(「標準構成」と呼んだ)、支配的になった「モナド」という用語は圏論者のサンダース・マック・レーンによって普及した。しかし、上記でbindを使用して定義された形式は、もともと1965年に数学者のハインリヒ・クライスリによって、任意のモナドは2つの(共変)ファンクター間の随伴として特徴付けられることを証明するために記述されたものである。[ 20 ]
1980年代から、モナドパターンの漠然とした概念がコンピュータ科学コミュニティで表面化し始めた。プログラミング言語研究者のフィリップ・ワドラーによれば、コンピュータ科学者のジョン・C・レイノルズは、継続渡しスタイルの価値、形式意味論の豊富な源泉としての圏論、値と計算の型区別について議論した1970年代から1980年代初頭に、そのいくつかの側面を予見していた。[ 4 ] 1990年まで活発に設計されていた 研究言語Opalも、入出力をモナド型に基づいて効果的に構築していたが、その関連性は当時認識されていなかった。[ 21 ]
コンピュータ科学者のエウジェニオ・モッジは、1989年の会議論文で初めて圏論のモナドと関数型プログラミングを明確に結びつけ、 1991年にはより洗練された論文をジャーナルに投稿した。 [ 22 ]以前の研究では、複数のコンピュータ科学者が圏論を用いてラムダ計算のセマンティクスを提供していた。モッジの重要な洞察は、現実世界のプログラムは単に値から別の値への関数ではなく、それらの値に対する計算を形成する変換であるという点だった。圏論的な用語で形式化すると、モナドがこれらの計算を表す構造であるという結論に至る。[ 3 ]
このアイデアを普及させ、発展させた人は他にも何人かおり、その中にはHaskellの仕様策定に関わったPhilip WadlerとSimon Peyton Jonesも含まれる。特にHaskellは、より柔軟なモナドインターフェースに切り替えるまで、 v1.2まで問題のある「遅延ストリーム」モデルを使用してI/Oと遅延評価を調和させていた。 [ 23 ] Haskellコミュニティは、関数型プログラミングの多くの問題にモナドを適用し、2010年代には、Haskellに取り組む研究者が最終的にモナドがアプリカティブファンクターであること[ 24 ] 、そしてモナドとアローの両方がモノイドであること[ 26 ]を認識した。
当初、モナドを用いたプログラミングは主にHaskellとその派生言語に限られていましたが、関数型プログラミングが他のパラダイムに影響を与えるにつれ、多くの言語が(名称こそ違えど、その精神は)モナドパターンを取り入れるようになりました。現在では、Scheme、Perl、Python、Racket、Clojure、Scala、F#などでその手法が用いられており、 MLの新標準規格への採用も検討されています。
モナドパターンの利点の1つは、計算の構成に数学的な精度をもたらすことです。モナド法則を使用してインスタンスの妥当性をチェックできるほか、サブタイピングを通じて関連する構造(ファンクターなど)の機能を使用することもできます。
先の例に戻るとMaybe、その構成要素はモナドを構成すると宣言されていたが、それがモナドの法則を満たすという証明は示されていなかった。
Maybeこれは、一般法則の一方の側に具体的な値を代入し、代数的に等式の連鎖を構築してもう一方の側に到達することで修正できます。
法則 1: eta(a) >>= f(x) ⇔ (単に a) >>= f(x) ⇔ f(a)
法則2: ma >>= eta(x) ⇔ ma maが(Just a)の場合、 eta(a) ⇔ Just a、 そうでなければ、またはNothing ⇔ Nothing end if
法則 3: ( ma >>= f(x) ) >>= g(y) ⇔ ma >>= ( f(x) >>= g(y) )もし(ma >>= f(x))が(単に b)ならば、もしmaが(単に a)ならば 、 g(ma >>= f(x)) (f(x) >>= g(y)) a そうでなければ、他には何もなし、 そう でなければ、何もなし、そうでなければ、もしも、もしも、もしも、もしも、もしも、もしも 、もしも、もしも、もしも、もしも、もしも、もしも、もしも、ならば ⇔ maが(Just a)でf(a)が(Just b)の場合、 (g ∘ f) a それ以外の場合、 maが(Just a)でf(a)が Nothing の場合、 Nothing それ以外の場合 、Nothing end if
コンピュータサイエンスでは稀ではあるが、圏論を直接用いることも可能である。圏論では、モナドは2つの自然変換が追加されたファンクターとして定義される。[ k ] まず、構造がファンクターとして認められるためには、マップという名前の高階関数(または「関数的」)が必要となる。
map : (a → b) → (ma → mb)しかし、これは必ずしも大きな問題ではありません。特に、モナドが既存のファンクターから派生する場合、モナドは自動的にマップを継承します。(歴史的な理由から、Haskellではこれをmapと呼びますfmap。)
モナドの最初の変換は実際にはクライスリ三重項の同じ単位ですが、構造の階層を厳密にたどると、単位はモナドと基本ファンクターの中間構造であるアプリカティブファンクターを特徴付けることがわかります。アプリカティブの文脈では、単位は純粋と呼ばれることもありますが、それでも同じ関数です。この構成で異なるのは、単位が満たさなければならない法則です。bindは定義されていないため、制約は代わりにmapで与えられます。
(unit ∘ φ) x ↔ ((map φ) ∘ unit) x ↔ x[ 27 ]アプリカティブファンクターからモナドへの最後の飛躍は、2番目の変換である結合関数(圏論ではこれは通常μと呼ばれる自然変換である)によってもたらされ、これはモナドの入れ子になった適用を「平坦化」する。
join(mma) : M (M T) → M T特性関数として、join はモナド法則の次の 3 つのバリエーションも満たさなければならない。
(join ∘ (map join)) mmma ↔ (join ∘ join) mmma ↔ ma(join ∘ (map unit)) ma ↔ (join ∘ unit) ma ↔ ma(join ∘ (map map φ)) mma ↔ ((map φ) ∘ join) mma ↔ mb開発者が直接モナドを定義するか、クライスリ・トリプルを定義するかにかかわらず、基盤となる構造は同じであり、これらの形式は互いに容易に導出できる。
(map φ) ma ↔ ma >>= (unit ∘ φ)join(mma) ↔ mma >>= idma >>= f ↔ (join ∘ (map f)) ma[ 28 ]リストモナドは、より単純なファンクターからモナドを導出することがいかに便利であるかを自然に示しています。多くの言語では、リスト構造はいくつかの基本的な機能とともに事前に定義されているため、ここではList型コンストラクタと追加演算子(++中置記法で表されます)が既に与えられていると想定します。
ほとんどのプログラミング言語では、リストに単純な値を埋め込むのも簡単です。
単位(x) = [x]
ここから先は、リスト内包表記を使って関数を繰り返し適用し、 bindを使ってリストを完全なモナドに変換するのは簡単な選択肢のように思えるかもしれません。しかし、このアプローチの難点は、bind はモナド関数を期待しているのに対し、この場合は出力がリストになってしまうことです。関数を適用するたびにネストされたリストの層が蓄積され、基本的な内包表記以上の処理が必要になります。
しかし、リスト全体に任意の単純な関数を適用する手順、つまりマップ処理は簡単です。
(マップ φ) xlist = [ φ(x1), φ(x2), ..., φ(xn) ]
さて、これら2つの手順は既にListアプリカティブファンクターへと昇格しています。モナドとして完全に成立するには、繰り返し構造を平坦化するための正しい結合の概念が必要ですが、リストの場合、それは外側のリストを展開して、値を含む内側のリストを追加することを意味します。
join(xlistlist) = join([xlist1, xlist2, ..., xlistn]) = xlist1 ++ xlist2 ++ ... ++ xlistn
結果として得られるモナドは単なるリストではなく、関数が適用されるにつれて自動的にサイズ変更および圧縮されるリストです。bind は数式だけで導出できるようになり、その後、Listモナド関数のパイプラインを通して値を渡すために使用できます。

Listは、複素根などの多値関数の使用を大幅に簡素化できます。[ 29 ](xlist >>= f) = join ∘ (map f) xlist
このモナドリストの応用例の1つは、非決定論的計算の表現です。 Listアルゴリズム内のすべての実行パスの結果を保持し、各ステップで自身を凝縮して、どのパスがどの結果につながったかを「忘れる」ことができます(これは、決定論的で網羅的なアルゴリズムとは、場合によっては重要な違いとなります)。 もう1つの利点は、チェックをモナドに埋め込むことができることです。特定のパスは、最初の失敗ポイントで透過的に剪定でき、パイプライン内の関数を書き直す必要はありません。[ 28 ]
が真価を発揮する2つ目の状況は、多値関数のList合成です。たとえば、ある数のn乗複素根はn個の異なる複素数を生成するはずですが、その結果からさらにm乗根を取ると、最終的なm・nの値はm・n乗根 の出力と同一になるはずです。はこの問題を完全に自動化し、各ステップの結果を数学的に正しいフラットなリストに集約します。[ 29 ]List
モナドは、プログラムロジックの整理にとどまらず、興味深い手法の可能性を広げます。モナドは有用な構文機能の基礎を築くことができ、その高レベルかつ数学的な性質は高度な抽象化を可能にします。
bind を明示的に使用することは多くの場合理にかなっていますが、多くのプログラマーは命令文を模倣した構文(Haskell ではdo 記法、 OCamlではperform 記法、F#では計算式[ 30 ]、Scalaではfor 内包表記と呼ばれる)を好みます。これは、モナドパイプラインをコードブロックとして偽装する単なる構文糖衣です。コンパイラはこれらの式を自動的に基となる関数型コードに変換します。
addこの関数をからHaskellに翻訳すると、Maybeこの機能が実際にどのように動作するかがわかります。Haskellにおけるの非モナド版はadd次のようになります。
add mx my = case mx of Nothing -> Nothing Just x -> case my of Nothing -> Nothing Just y -> Just ( x + y )モナド型Haskellでは、はユニットreturnの標準的な名称であり、ラムダ式は明示的に処理する必要があるが、これらの技術的な制約があっても、モナドはより簡潔な定義をもたらす。Maybe
add mx my = mx >>= ( \ x -> my >>= ( \ y -> return ( x + y )))しかし、do記法を用いると、これはさらに非常に直感的なシーケンスに集約できます。
add mx my = do x <- mx y <- my return ( x + y )2つ目の例は、Maybe全く異なる言語であるF#での使用方法を示しています。計算式を使用すると、None未定義のオペランドまたはゼロ除算に対して を返す「安全な除算」関数は次のように記述できます。
let readNum ( ) = let s = Console.ReadLine ( ) let succ , v = Int32.TryParse ( s ) if ( succ ) then Some ( v ) else Nonelet secure_div = maybe { let ! x = readNum () let ! y = readNum () if ( y = 0 ) then None else return ( x / y ) }ビルド時に、コンパイラは内部的にこの関数を「デシュガー」して、より密度の高いbind呼び出しの連鎖に変換します。
おそらく。遅延( fun ( ) -> maybe.Bind ( readNum ( ) , fun x - > maybe.Bind ( readNum ( ) , fun y - > if ( y = 0 ) then None else maybe.Return ( x / y ) )))最後に、一般的なモナド法則自体もdo記法で表現できます。
do { x <- return v ; f x } == do { f v } do { x <- m ; return x } == do { m } do { y <- do { x <- m ; f x }; g y } == do { x <- m ; y <- f x ; g y }すべてのモナドは、モナドの法則を満たす特定の実装を必要としますが、他の構造との関係や言語内の標準的な慣用表現など、その他の側面はすべてのモナドで共有されます。そのため、言語やライブラリは、関数プロトタイプMonad、サブタイピング関係、その他の一般的な事実を含む汎用インターフェースを提供する場合があります。このインターフェースは、開発のスタートダッシュを容易にし、新しいモナドがスーパータイプ(ファンクターなど)から機能を継承することを保証するだけでなく、モナドの設計をインターフェースに照らし合わせてチェックすることで、品質管理の新たな層を追加します。
モナドコードは、演算子を適切に使用することで、さらに簡略化できる場合が多い。map関数は、アドホックなモナド関数だけでなく、より複雑なものにも適用できるため、特に有用である。モナド関数が定義済みの演算子と類似した動作をする限り、map関数を使用することで、より単純な演算子を瞬時にモナド演算子に「変換」できる。 [ l ]addこの手法を用いると、例の 定義はMaybe次のように簡略化できる。
add(mx,my) = map (+)
addこのプロセスは、だけでMaybeなくインターフェース全体に対しても定義することで、さらに一歩進めることができますMonad。こうすることで、構造インターフェースに一致し、独自のマップを実装する新しいモナドはすべて、すぐにリフトされたバージョンも継承しますadd。関数に必要な変更は、型シグネチャを一般化することだけです。
add : (Monad Number, Monad Number) → Monad Number [ 31 ]
解析に役立つもう1つのモナド演算子は、モナド合成(>=>ここでは中置記法で表す)であり、これによりモナド関数をより数学的なスタイルで連結することができる。
(f >=> g)(x) = f(x) >>= g
この演算子を用いることで、モナドの法則を関数のみで記述することができ、結合法則と同一性の存在との対応関係が強調される。
(単位 >=> g) ↔ g (f >=> 単位) ↔ f (f >=> g) >=> h ↔ f >=> (g >=> h) [ 1 ]
以上が、Haskellにおける「do」ブロックの意味を示しています。
する _p <- f(x) _q <- g(_p) h(_q) ↔ ( f >=> g >=> h )(x)
最も単純なモナドはアイデンティティモナドであり、これは単に通常の値と関数に注釈を付けてモナドの法則を満たすようにするだけです。
新しいタイプId T = T 単位(x) = x (x >>= f) = f(x)
Identityしかし、実際には有効な用途があり、例えば再帰的なモナドトランスフォーマーの基本ケースを提供するなどです。また、命令型ブロック内で基本的な変数代入を実行するためにも使用できます。[ m ]
適切なappendを持つコレクションは既にモノイドですが、結合が明確に定義されていてモナドとして適格なコレクションListはそれだけではないことがわかっています。appendに特別なプロパティを課すだけで、これらの他のモナド的コレクションに変異させることさえできます。[ n ] [ o ]List
すでに述べたように、純粋なコードには管理されていない副作用があってはなりませんが、だからといってプログラムが副作用を明示的に記述して管理することを妨げるものではありません。この考え方は Haskell のIO モナドの中心であり、型 のオブジェクトはIO a、オプションで型 の世界に関する情報を提供する、世界で実行されるアクションを記述するものと見なすことができますa。世界に関する情報を提供しないアクションは型 を持ちIO ()、ダミー値 を「提供」します()。プログラマがIO値を関数にバインドすると、関数は前のアクションによって提供された世界に関する情報 (ユーザー、ファイルなどからの入力) に基づいて、次に実行されるアクションを計算します。[ 23 ]最も重要なのは、IO モナドの値は別の IO モナドを計算する関数にのみバインドできるため、bind 関数は、アクションの結果が次に実行されるアクションを計算する関数にのみ提供されるという一連のアクションの規律を課すということです。これは、実行する必要のない行動は決して実行されず、実行する必要のある行動には明確に定義された順序があることを意味する。
例えば、Haskellには、ファイルが存在するかどうかを確認する関数やファイルを削除する関数など、ファイルシステム全体を操作する関数がいくつかあります。それらの2つの型シグネチャは次のとおりです。
doesFileExist :: FilePath -> IO Bool removeFile :: FilePath -> IO ()最初の関数は、指定されたファイルが実際に存在するかどうかに関心があり、その結果としてモナド内にブール値IOを出力します。一方、2番目の関数はファイルシステムに対して操作を行うことのみに関心があり、IO出力されるコンテナは空になります。
IOただし、ファイル入出力だけに限定されるわけではありません。ユーザー入出力も可能で、命令型構文糖衣と組み合わせることで、典型的な「Hello, world !」プログラムを模倣することもできます。
main :: IO () main = do putStrLn "Hello, world!" putStrLn "What is your name, user?" name <- getLine putStrLn ( "Nice to meet you, " ++ name ++ "!" )糖衣を取り除くと、これは次のモナドパイプラインに変換されます( Haskellでは、はモナド効果のみが重要で、基となる結果を破棄できる場合のbind>>のバリアントです)。
main :: IO () main = putStrLn "Hello, world!" >> putStrLn "What is your name, user?" >> getLine >>= ( \ name -> putStrLn ( "Nice to meet you, " ++ name ++ "!" ))もう一つよくあるケースは、ログファイルを作成したり、プログラムの進行状況を報告したりすることです。プログラマーは、後でプロファイリングやデバッグを行うために、さらに詳細な技術データをログに記録したい場合があります。Writerモナドは、段階的に蓄積される補助的な出力を生成することで、これらのタスクを処理できます。
モナドパターンが主に関数型言語に限定されないことを示すために、この例ではJavaWriterでモナドを実装し、モナドを表すクラスに格納します。Writer
import java.util.ArrayList ; import java.util.List ; import java.util.function.Function ;record Writer < T > ( T value , List < String > log ) { // 内部処理はここに記述... }単位の定義も非常に簡単です。
record Writer < T > ( T value , List < String > log ) { // ...public static < T > Writer < T > unit ( T value ) { return new Writer <> ( value , new ArrayList <> ()); } }デバッグノート付きのオブジェクトを出力する単純な関数を定義するために必要なユニットは1つだけです。Writer
import java.util.List ;class Ops { private Ops () {}public static Writer < Integer > squared ( int x ) { return new Writer <> ( x * x , List . of ( String . format ( "%d は二乗されました。 " , x ))); }public static Writer < Integer > halved ( int x ) { return new Writer <> ( x / 2 , List . of ( String . format ( "%d が半分になりました。 " , x ))); } }真のモナドは依然としてbindを必要としますが、の場合Writer、この操作は現在のログと変換を適用して生成されたログを結合します。
record Writer < T > ( T value , List < String > log ) { // ...public < U > Writer < U > bind ( Function < T , Writer < U >> transform ) { Writer < U > result = transform . apply ( this . value ); List < String > newLog = new ArrayList <> ( this . log ); newLog . addAll ( result . log ()); return new Writer <> ( result . value (), newLog ); } }サンプル関数はbindを使用して連結できるようになり、これはメソッドチェーンを使用して表現できます。
Writer < Integer > result = Writer . unit ( 4 ) . bind ( Ops :: squared ) . bind ( Ops :: halved );最終的な結果として、計算の実行と、後で確認するためのログ出力の蓄積という、それぞれの関心事を明確に分離することができた。
System.out.println ( result.value ( ) ); // 8 System.out.println ( result.log ( ) ) ; // [ 4は2乗され、16は半分になった。]環境モナド(リーダーモナド、関数モナドとも呼ばれる)は、計算が共有環境の値に依存することを可能にする。モナド型コンストラクタは、型Tを型E → Tの関数にマッピングする。ここで、Eは共有環境の型である。モナド関数は以下のとおりである。
以下のモナド操作が役立ちます。
ask操作は現在のコンテキストを取得するために使用され、local は変更されたサブコンテキストで計算を実行します。状態モナドと同様に、環境モナドでの計算は、環境値を指定してそれをモナドのインスタンスに適用するだけで呼び出すことができます。
形式的には、環境モナドの値は、匿名引数が追加された関数と同等です。returnとbindは、それぞれSKIコンビネータ計算におけるKコンビネータとSコンビネータと同等です。
状態モナドを使用すると、プログラマは任意の型の状態情報を計算に付加できます。任意の値型が与えられた場合、状態モナドにおける対応する型は、状態を受け取り、新しい状態(型s)と戻り値(型t)を出力する関数です。これは環境モナドに似ていますが、新しい状態も返すため、可変環境をモデル化できます。
型State s t = s -> ( t , s )このモナドは、状態情報の型である型パラメータを受け取ることに注意してください。モナドの操作は次のように定義されます。
-- "return" は状態を変更せずに指定された値を生成します。return x = \ s -> ( x , s ) -- "bind" は m を変更して、その結果に f を適用します。m >>= f = \ r -> let ( x , s ) = m r in ( f x ) s有用な状態操作には以下が含まれます。
get = \ s -> ( s , s ) -- 計算のこの時点での状態を調べます。put s = \ _ -> ( () , s ) -- 状態を置き換えます。modify f = \ s -> ( () , f s ) -- 状態を更新します。別の操作では、状態モナドを与えられた初期状態に適用します。
runState :: State s a -> s -> ( a , s ) runState t s = t s状態モナドにおけるdoブロックは、状態データを検査および更新できる一連の操作です。
非公式には、状態型Sの状態モナドは、戻り値の型Tを型 の関数にマッピングします。ここで、Sは基底状態です。戻り値とバインド関数は次のとおりです。
圏論の観点から見ると、状態モナドは積関手と指数関手の間の随伴から導き出され、これは定義上、任意のデカルト閉圏に存在する。
戻り値の型がRである継続モナド[ p ]は、型Tを型が の関数にマッピングします。これは継続渡しスタイルをモデル化するために使用されます。戻り値とバインド関数は次のとおりです。
現在の継続を伴う呼び出し関数は、次のように定義されます。
以下のコードは擬似コードです。型が である 2 つの関数 と があると仮定しますfoo。bar
foo : int -> int bar : int -> intつまり、どちらの関数も整数を入力として受け取り、別の整数を返します。そして、以下のように関数を連続して適用することができます。
foo (バーx )結果は、に適用した結果であり、に適用したfoo結果である。barx
fooしかし、プログラムをデバッグしているときに、とにログメッセージを追加したいとしますbar。そこで、型を次のように変更します。
foo : int -> int * string bar : int -> int * stringつまり、どちらの関数もタプルを返し、整数として適用結果、文字列として適用された関数と以前に適用されたすべての関数に関する情報を含むログメッセージが格納される。
残念ながら、これは入力型と出力型が互換性がないため、とを組み合わせる ことができなくなったことを意味します。各関数の型をに変更することで再び組み合わせることが可能になりますが、そのためには各関数にタプルから整数を抽出する定型コードを追加する必要があり、そのような関数の数が増えるにつれて面倒になります。foobarintint * stringint * string -> int * string
代わりに、この定型的なコードを抽象化してくれるヘルパー関数を定義しましょう。
bind : int * string -> ( int -> int * string ) -> int * stringbindは整数と文字列のタプルを受け取り、次にfoo整数から整数と文字列のタプルにマッピングする関数(など)を受け取ります。その出力は整数と文字列のタプルであり、これは入力された整数と文字列のタプル内の整数に入力関数を適用した結果です。このようにして、タプルから整数を抽出する定型コードを で一度だけ記述すれば済みますbind。
これで、ある程度の構成可能性を取り戻しました。例えば:
bind ( bind ( x , s ) bar ) foo(x,s)は整数と文字列のタプルです。[ q ]
利点をさらに明確にするために、中置演算子を次の演算子の別名として定義してみましょうbind。
( >>= ) : int * string -> ( int -> int * string ) -> int * stringつまり、それはt >>= fと同じですbind t f。
すると、上記の例は次のようになります。
(( x , s ) >>= bar ) >>= foo(x, "")最後に、空のログメッセージを作成したいときに毎回記述することを避けるための新しい関数を定義します。ここで、""は空の文字列です。
return : int -> int * stringこれは、x上述のタプルをラップするものです。
その結果、メッセージをログに記録するためのパイプラインが構築されます。
(( return x ) >>= bar ) >>= foobarこれにより、およびfooに対する影響をより簡単に記録できますx。
int * stringは擬似コード化されたモナド値を表します。[ q ]bindおよび は、return同じ名前の対応する関数に類似しています。 int * string、、bindおよび はreturnモナドを形成します。
加法モナドは、閉じた結合的な二項演算子mplusと、mplusの下の単位元mzeroを備えたモナドです。このモナドは、 mzeroとして、 OR演算子の変形として、加法的なものと考えることができます。 も加法モナドであり、空のリストがmzeroとして、連結演算子がmplusとして機能します。MaybeNothingList[]++
直感的に言えば、mzero は基となる型から値を持たないモナド ラッパーを表しますが、bindの吸収体として機能し、モナド関数にバインドされるたびにmzero を返すため、「1」ではなく「0」とみなされます。この特性は双方向であり、bind はモナドゼロ関数に値がバインドされた場合にもmzeroを返します。
圏論的な用語では、加法モナドは、bindを持つモナド関数上のモノイドとして一度適格であり (すべてのモナドがそうであるように)、 mplusを介してモナド値上のモノイドとしてもう一度適格である。[ 32 ] [ r ]
モナドの一般的な概要は役立つ場合もありますが、特定のモナドを推奨する単純なパターンは存在しません。ここで自由モナドが登場します。自由モナドはモナドの圏における自由オブジェクトとして、モナド法則自体以外の特定の制約なしにモナド構造を表現できます。自由モノイドが評価なしで要素を連結するのと同様に、自由モナドは型システムを満たすためにマーカーを用いた計算の連鎖を可能にしますが、それ以外に深い意味論を課すことはありません。
例えば、JustとNothingマーカーを完全に扱うことで、このMaybeモナドは自由モナドになります。一方、このモナドは、リストに関する追加の具体的な事実( のように、を追加するList)を定義に取り込んでいるため、自由モナドではありません。最後に、抽象的な自由モナドの例を挙げます。
データFree f a = Pure a | Free ( f ( Free f a ))unit :: a -> Free f a unit x = Pure xbind :: Functor f => Free f a -> ( a -> Free f b ) -> Free f b bind ( Pure x ) f = f x bind ( Free x ) f = Free ( fmap ( \ y -> bind y f ) x )しかし、自由モナドは、この例のようにリンクされたリストに限定されず、ツリーなどの他の構造を中心に構築することもできます。
自由モナドを意図的に使用することは、最初は非実用的であるように思えるかもしれないが、その形式的な性質は構文の問題に特に適している。自由モナドは、意味論を後回しにして構文と型を追跡するために使用でき、その結果、パーサーやインタプリタで使用されている。 [ 33 ]また、言語内で反復処理を 提供するなど、より動的で操作的な問題にも適用されている。 [ 34 ]
追加のプロパティを持つモナドを生成するだけでなく、任意のモナドに対してコモナドを定義することもできます。概念的には、モナドが基となる値から構築された計算を表すとすれば、コモナドは値への還元と見なすことができます。モナドコードは、ある意味で完全に「展開」することはできません。値がモナド内にラップされると、副作用とともにそこに隔離されたままになります(純粋関数型プログラミングでは良いことです)。ただし、問題となるのは、コモナドで明示的にモデル化できるコンテキストデータの処理である場合が多いです。
厳密に言えば、コモナドはモナドの圏論的双対であり、大まかに言えば、必要な構成要素は同じだが、型シグネチャの方向が逆になっていることを意味します。バインド中心のモナドの定義から始めると、コモナドは以下で構成されます。
counit(wa) : WT → T
=>>)は、還元関数の連鎖を拡張する。(wa =>> f) : (WU, WU → T) → WT [ s ]
extendとcounitは、モナド法則の双対も満たさなければならない。
カウント単位 ∘ ( (wa =>> f) → wb ) ↔ f(wa) → b wa =>> counit ↔ wa wa ( (=>> f(wx = wa)) → wb (=>> g(wy = wb)) → wc ) ↔ ( wa (=>> f(wx = wa)) → wb ) (=>> g(wy = wb)) → wc
モナドと同様に、コモナドもjoinの双対を使用してファンクターから導出できます。
duplicate(wa) : WT → W (WT)
ただし、 extendのような操作は反転されますが、コモナドは作用する関数を反転させないため、コモナドは依然としてmapを持つファンクターであり、コファンクターではありません。duplicate 、counit、およびmapを用いた別の定義も、独自のコモナド法則を遵守する必要があります。
((マップ重複) ∘ 重複) wa ↔ (重複 ∘ 重複) wa ↔ wwwa ((マップ個数) ∘ 重複) わ ↔ (個数 ∘ 重複) わ ↔ わ ((map map φ) ∘ duplicate) wa ↔ (duplicate ∘ (map φ)) wa ↔ wwb
モナドの場合と同様に、この2つの形式は自動的に変換できます。
(マップ φ) ワ ↔ ワ =>> (φ ∘ カウント) wx wa を複製 ↔ wa =>> wx
wa =>> f(wx) ↔ ((map f) ∘ duplicate) wa
簡単な例として、入力値と共有環境データに基づいて値を出力するProduct コモナドProductがあります。このコモナドはモナドの双対でありWriter、実質的にはモナドと同じですReader(どちらも後述します)。 ProductとReaderの違いは、受け入れる関数シグネチャと、値をラップまたはアンラップすることによってそれらの関数をどのように補完するかという点だけです。
より単純な例としては、Stream コモナドがあり、これはデータ ストリームを表し、 extendを使用して入力信号にフィルタを付加するために使用できます。モナドほど一般的ではありませんが、研究者たちはコモナドがストリーム処理やデータフロー プログラミングのモデリングに特に役立つことを発見しました。[ 35 ] [ 36 ]
しかし、厳密な定義のため、モナドとコモナドの間でオブジェクトを単純に行き来させることはできません。さらに高い抽象化として、矢印は両方の構造を包含できますが、モナドコードとコモナドコードを組み合わせるよりきめ細かい方法を見つけることは、活発な研究分野です。[ 37 ] [ 38 ]
モデリング計算の代替案:
関連するデザインコンセプト:
モナドの一般化:
bindある型aと失敗する可能性のあるマッピングa → bが与えられたときに、失敗する可能性のある結果bを生成する抽象化 a を提唱している。(ハットン、2016) [ 7 ]liftIdentityから生じるものとも考えることができるbindられています。つまり、プログラマーは付加項を構築しました。これは、上記の擬似コード§で示されるタプルです。stringinteger(x,s)int * stringHaskellWikiの参照元:
チュートリアル:
Probabilityのモナドを含む、より詳細な説明と例を解説する第2章。興味深い事例: