モナド (プログラミング)
関数型プログラミングにおいて、モナド(monad)は計算を表現する構造であり、計算ステップの列からなる。つまり、型がモナド構造をもつというのは、命令を繋げるやり方、言い換えるとその型をもつ関数をネストさせる規則が定まっていることをいう。これはプログラマがパイプラインを作ることを可能にする。パイプラインでは入力データを１ステップずつ処理するが、モナドは各アクションに追加の処理規則を上乗せすることができる。このことから、モナドは「プログラム可能なセミコロン」と記述することもできる。セミコロンは多くの命令型プログラミング言語で個々の文を繋ぐ演算子であり、この例えは、パイプライン中の文の間に追加でコードを実行できることを示唆している。さらにモナドは組み立てラインというメタファーで説明することもできる。ベルトコンベアが機能ユニットの間のデータ転送を行い、各ユニットが一度にひとつずつそれを処理するのである。他にも、ジェネリック型を構築するための関数型デザインパターンと見ることもできる。
純粋関数型プログラムでは構造化プログラミングに見られるような、逐次実行する命令列を含む手続きの構成にモナドを利用することができる。モナド構造によって、プログラミングにおける多くの共通概念を記述することが可能である。これには入出力といった副作用、変数への代入、例外処理、構文解析、非決定計算、並行計算、継続がある。言語の意味を大きく拡張することなく、純粋性を保ったままこれらの概念が利用できるようになる。Haskellのような言語ではモナドを標準ライブラリで提供しており、プログラマはこれらの抽象的な定義を再利用することで、多くの異なったライブラリにおいても同じやりかたで機能を合成することが可能になっている。
形式的には、モナドは型構築子 "M" とふたつの演算 "bind" と "return" から構成される("return"を"unit"と呼ぶこともある)。
この演算はモナド的な関数(引数や返り値がモナドである関数)を正しく合成できるように、いくつかの性質を満たさなければならない。プログラムの中に命令を追加するという意味で、モナドはアスペクト指向プログラミングとしての側面もある。ドメインロジックはアプリケーションプログラマが定めるのに対して、必要なブックキーピングは前もってモナドとして定義することができる。
名称と概念は圏論にちなむもので、モナドは一種の関手であり、圏の間の写像である。しかし、関数型プログラミングの文脈における"モナド"は通常は圏論における強モナドを指すことが多い。
プログラミングにおけるモナドの概念は1980年代のプログラム言語に見られるが、このときは「コマンド」と呼ばれ形式化はされなかった。
1991年にEugenio Moggiはプログラムの構造化でモナドを初めて汎用的に使用した。この成果を元に、Philip WadlerやSimon Peyton Jonesといったプログラム言語の研究者(二人ともHaskellの言語仕様に取り組んでいた)により実装が行われた。初期のバージョンのHaskellは問題の多い「遅延リスト」を用いた入出力を使っていたが、Haskell 1.3からは遅延評価により柔軟に合成可能なモナドによる入出力が導入された。
入出力に加えて、プログラム言語研究者とHaskellのライブラリ設計者は構文解析器やインタープリタにモナドを適用することに成功した。Haskellのdo記法に現れる性質から、モナドの概念はapplicative functorとarrowへと一般化された。
長い間、Haskellとその派生だけがプログラミングにおけるモナドの主な利用者であった。最近では、F#がコンピュテーション式または「ワークフロー」と呼ばれる機能を導入した。これは、命令型プログラムしか経験のないプログラマにもなじみやすい構文を使ったモナド的構成を提供しようという試みである。
プログラム言語Haskellはモナドを多用する関数型の言語であり、簡単にモナドの合成ができるような構文糖衣を持っている。特に指定がない限り、この記事のコード例はHaskellで書かれている。
モナドの紹介として一般的なふたつの例 "Maybe" モナドと "I/O" モナドを取り上げる。もちろん、モナドはHaskellに限られたものではないので、JavaScriptでの "Writer" モナドの例もその次に扱う。
オプション型 "Maybe a" を考えよう。これは型 "a" の値をひとつ持つか、全く持たないかのふたつの状態を表現する型である。これらを区別するために、二種類の代数的データ型構成子を持つ。codice_1はひとつの値codice_2を保持し、codice_3は値を何も保持しない。
この型を使って検査例外の簡単なものを作ってみたいとする。計算の途中で失敗した場合は、残りの計算は飛ばして結果codice_3を返し、すべての計算が成功した場合は何か値codice_5を保持したcodice_6を返すことにする。
以下の例では、codice_7は"Maybe Int"型のふたつの引数を受け取って、同じ型の結果を返す。codice_8とcodice_9がともにcodice_10である場合は、これらの値の和をcodice_10に入れて返したい。もし、codice_8とcodice_9のどちらかがcodice_14だった場合は、codice_3を返したい。こういった振る舞いをする関数を単純に実装しようとしたら、「if codice_3 then codice_3 else codice_6のcodice_5に何かする」の形の場合わけを複数ネストすることになり、すぐやっかいなものになる。
これを楽にするための、各計算を繋げる演算子を定義することができる。二項演算子 "bind" (codice_20) は失敗する可能性のある計算の結果を、もうひとつの失敗する可能性のある計算へ連鎖する。最初の引数が codice_3 だった場合は、二番目の引数(関数引数)は無視されて、単に計算全体が失敗する。もし、最初の引数が codice_6 だった場合は、この値 codice_5 は二番目の関数引数に渡されて、この関数の結果として新しい "Maybe" の値が返される。これは codice_10 な値か失敗かのどちらかになる。
計算の状態に影響を与えずに値を投入する構築子は、codice_10 がすでにそうなっている。
これらを使うことで、上の例は次のように書ける。
さらにdo記法という構文糖衣を使うことで、次のように書くことができる。
この種の操作は頻出なので、Haskellの標準関数(codice_26)が用意されている。これはふたつのモナド的な値(ここでは、ふたつのMaybe)と、その中身(ふたつの数値)を組み合わせる関数(加算)を受け取る関数であり、上の例は次のように書ける。
Haskellのような純粋関数型言語では関数が外部から観測可能な作用をもつことは、関数の意味から外れることになるため許されない。しかし、関数が直接副作用を起こすのではなく、求める副作用を"記述する"値を構築して返し、呼び出した側が適当な時に実行することは可能である。Haskellの記法では、型 "IO a" の値がこのようなアクションを表現していて、実行されたときには、型 "a" の何らかの値を生成する。
codice_27型の値は「現在の世界の状態を引数に取り、関数の返り値として変化した状態を持つ新しい世界を返す」という意味でアクションとみなすことができる。例えば、関数 codice_28 と codice_29 は次の型を持つ関数である。
codice_29 は関数としては、codice_31 を引数にとり、ある codice_27アクションを返す。このアクションが"実行される"と世界を、この場合はファイルシステムを codice_31 という名前のファイルの存在しないものに変更する。この場合は、codice_27 の持っている値は codice_35 型の値なので、なにも結果は生成されず、呼び出し側はこれを気にする必要はない。一方で、codice_28 の場合は、関数の返す codice_27アクションはブール型の値、codice_38 または codice_39を包んでいて、概念的には、アクションが実行されたときのファイルシステムに codice_31 という名前のファイルが存在したかどうかを呼び出し側が知っているという世界の状態を表現している。アクションからアクションへと世界の状態を渡して管理するこの方法により、決まった順序で状態を変化させていくアクションの列を定めることが可能となる。この過程は時相論理において宣言的な命題のみで時間の経過を表現する手法と似ている。以下の例ではプログラム内のアクションを連鎖する方法の詳細を再び Haskell を使って明らかにする。
基本的な種類の入出力操作、標準出力に文字列を書く、標準入力から文字列を読む、ファイルの読み書き、ネットワーク越しのデータ送信等をすべて記述できるようにしたい。さらに、これらのプリミティブを組み合わせて大きなプログラムを作れる必要もある。例えば次のように書けることが望ましい。
この直観的な記法はどのように形式化できるだろうか？このためには各入出力アクションに対して、いくつかの基本的な操作を実行できる必要がある。
これらの3つの演算子をプリミティブな入出力操作として「いかなるプログラムでも」書けるかというのは、データ変換(ラムダ式使う)やif式やループを含めたとしても自明であるとは言いがたい。上の例は長い式を使って次のように書ける。
"bind" 演算子の持つパイプライン構造により、"getLine" と "putStrLn" の命令はそれぞれ一度だけ、書いた順序で評価され、入力ストリームから文字列を取り出して、出力ストリームに出力する副作用は関数型パイプラインの中でも正しく実行される。これは言語が関数をアウト・オブ・オーダー実行や遅延評価する場合でも成立する。
明らかに入出力の定義とMaybeの定義は共通の構造を持っているが、異なっている部分も多い。上述した構造や他のよくある似たような多くの構造の抽象化である。モナドの一般化された概念は、計算の一部が副作用を発生させるにも関わらず、純粋関数的な計算として実行したくなるようなあらゆる状況を包含している。
モナドは、元となる型システムが与えられたとき、その内部に対応する型システム("モナド的型システム"と呼ぶ)を埋め込む構成である(つまり、各モナド的型は元のシステムの型でもある)。このモナド的型システムは元の型システムの重要な点は全て保存するが、モナドに固有の特徴を追加する。
プログラミングにおけるモナドの普通の定式化はクライスリトリプルであり、以下の要素からなる。
型構築子 "M" が与えられたとき、ほとんどの文脈では、型 "M a" の値は型 "a" を返すアクションだと考えることができる。"return" 演算子は素の型 "a" の値を受け取り、モナド的なコンテナ型 "M a" に入れる。"bind" 演算子は型 "M a" のモナド的な値と型 "a → M b" を持つ関数を連鎖させて、型 "M b" のモナド的な値を作成する。
モナドが正しく振舞うためには、いくつかのモナド則と呼ばれる公理にしたがって定義されていなければならない。
以下、記号 ≡ はHaskellの式が等価であることを示す。公理を do 記法で表現することもできる。
また、モナド合成演算子 を使うと
Haskellではモナドを "return" と "bind" 関数を用いて定義するが、"return" にふたつの演算子 "join" と "fmap" を加えることでも定義可能である。この定式化は圏論における元のモナドの定義により近くなる。"fmap" 演算子は型 ("t"→"u") → M "t"→M "u" を持ち、ふたつの型間の関数を受け取って、モナドの中の値に「同じこと」をする関数を返す。"join" 演算子は型 M (M "t")→M "t" を持ち、二層になったモナド的な情報を平らにしてひとつにする。
ふたつの定式化は以下の関係を持つ。
ここで、m は M "t"、n は M (M "r")、f は "t" → "u"、g は t → M "v" の型を持つ。"t"、"r"、"u"、"v" は元の型である。
モナドでなくても、型と関数からなる圏では fmap 関数は任意の函手に対して定義できる。函手は次の法則を満たす。
同じ圏で、"return" はを特徴付ける。これは値を函手の中に「持ち上げる」ことによる。満たすべき法則は次の通りである。
さらに、"join" によってモナドが特徴付けられる。
加法的モナドは、モノイド則を満たす二項演算子 "mplus"とその単位元としてモナド的な値 "mzero" を備えたモナドである。演算子 "mplus" の型は "M t" → "M t" → "M t" であり(ここで、"M" はモナドの構築子であり "t" は元の型である)、結合律と左右からの単位元律を満たす。
このことから、加法的モナドはモノイドでもある。codice_20に対しては、mzeroはヌル要素として振舞う。ゼロを掛けるとゼロになるように、"mzero" と関数を bind すると結果はゼロとなる。
同様に、モナド的な値 "m" とつねにゼロを返す関数を bind すると、結果はゼロである。
直観的には、モナド的値としてのゼロはモナドに関連した構造だけを持ち元の型の値を持たない。Maybe モナドでは、「Nothing」がゼロである。リストモナドでは、空リストがゼロである。
"bind" 演算子 codice_20 を直接使ってプログラムを書くのがよい場合もあるが、典型的には"do記法" (OCamlでは "perform記法"、F#では"コンピュテーション式")を使用し、命令型言語のような見た目にできる。コンパイラはdo記法の式を codice_20 を使ったものに変換する。例えば
を変換した結果は以下となる。
リストモナドの実装を見てみよう。リストにマップを行い結合を行う(平らにする)この関数は concatMap とも呼ばれる。
よって、以下のような変形が行われて、それぞれの式はすべて等価である。
リスト[1..2]は使われないことに注意しよう。左矢印をつけないことで、連鎖した関数は引数を無視するように変換されて、値ではなくモナド的な構造だけが問題になることを示すことができる。例えば、状態モナドで値を生成せずに状態だけを変化させるようなときに使われる。do記法は codice_20 の単なる構文糖衣であるので、どんなモナドに対しても使うことができる。
Maybeモナドの値を安全に割り算する以下の定義も等価である。
同様にF#ではコンピュテーション式を使うことができる。
このmaybeブロックは構文糖衣であり、以下の式に変換される。
安全な割り算の結果は、値が Nothing であるかを素直に確認せずに(つまり割り算の結果があるかを確認せずに)そのまま計算に使えたほうが便利なことがある。これは「」関数を使うことで可能である。この関数は Maybe に限定されず、任意のモナドに対して定義される。Haskellでは LiftM2 と呼ばれる。
関数型を示す矢印が右結合であることから、liftM2は2引数の関数を引数に取って、さらなる2引数の関数を返す。型シグネチャは m がモナドのとき、任意の2引数関数を「持ち上げ」ることができることを示している。例えば、
は演算子 (.*.) を定義し、それは引数がどちらも Nothing でないときに、掛け算した結果を返す(どちらかが Nothing の場合は Nothing を返す)。この方法の利点はモナドの実装に深入りする必要がないことである。他の同じような関数やモナドを使う場合でも、liftM2 を使うことで、やりたいことが鮮明になる(コードの再利用を見よ)。
数学的には、liftM2 は次のように定義される。
もっとも単純なモナドは恒等モナドであり、値になんの情報も付加しない。
このモナドのdoブロックは変数の代入を実行する。例えば、do {x 恒等函手の間の随伴から得られる。
リストや集合や多重集合といった、おなじみのコレクション型にもモナドであるものがある。リストの場合の定義は次のようになる。
はリストモナドに固有のもので、例えば、リスト内包 [ 2*x | x 内包記法に似ているが、集合はモナドにならない。なぜなら、計算の型は等しいかどうかを比較可能でなければならない — モナドは計算の型についてこれを課していないように見えるにも関わらず — という制限があるからである。実際、集合は制限されたモナド(restricted monad)である。コレクションによるモナドは非決定計算を自然に表現する。リスト(または他のコレクション)は非決定的な計算の異なった実行パスが今の時点で取りうる結果の全体を表す。例えば、x 遅延評価を行う言語では、リストはその要素が必要になって初めて評価される。例えばリストの最初の要素を要求した場合は、最初の要素だけが計算される。非決定計算にリストモナドを使用した場合には、全ての計算からなる結果は非決定的に遅延リストとして生成され、最初の要素を要求すると、必要最小限の計算のみが行われて、最初の結果を返す。この過程はバックトラックに対応している。ある実行パスにいて計算に失敗した(mzeroに評価された)場合、最後に行った分岐までバックトラックを行い、そこから次のパスを選んで実行を続ける。二番目の要素が要求された場合、再びその答を得るのに必要な最小限の計算が行われる。よって、遅延評価の言語ではリストモナドを使って簡単にバックトラックを実装することができる。
圏論的な見方をすると、コレクションモナドは集合の圏とモノイドの圏の間のと忘却函手の随伴から作られる。
状態モナドは状態を付加した計算の型である。好きな型に対して、その型を状態として持つ状態モナドを作ることができる。これは状態を引数に取り、通常の返り値(型 t の値)と新しい状態(型 s の値)を返す関数である。
今までに見たモナドと違い、状態の型という型引数を取ることに注意しよう。モナドの演算は次のように定義される。
状態を操作する関数を挙げる。
初期状態を指定して状態モナドを実行する関数。
状態モナドのdoブロック内の計算は状態の確認や更新を行うことができる。
非形式的に書くと、状態の型 "S" を持つ状態モナドは返り値の型 "T" を関数型 formula_3 に変換する。"return" は単に
であり、"bind" は
である。
圏論的な見方では、状態モナドはデカルト閉圏の定義に現れる積函手と指数函手の随伴から作られる。
環境モナド("Readerモナド"や"関数モナド"とも呼ばれる)は環境を共有した計算を可能にする。型構築子は型 "T" を 関数型 "E" → "T" にうつす。ここで、"E" は共有したい環境の型である。モナド演算子は
で定義される。
環境を操作する関数として
がある。"ask" は現在の環境を取得するのに使う。"local"は指定した計算を一時的に変更した環境で行う。状態モナドと同様に、環境モナドによる計算は単に環境の値にモナドの値を適用することで実行できる。
Writerモナドは本来の計算を行いながら、様々な種類の追加の出力を作成することができる計算である。ログ取りやプロファイリングに有用である。元の型 "T" に対して、Writerモナドは型 "W" × "T" の値を持つ。ここで、"W" はモノイド則を満たす演算を持った型である。すなわち、"W" は二項演算子 "(a,b) → a*b" と単位元 "ε" を持っており、これは結合則 "(a*b)*c = a*(b*c)" を満たし、任意の "x" について、"x*ε = ε*x = x" を満たす。
モナド演算は
で定義される。ここで、εと * はモノイド W の単位元と二項演算子である。
JavaScriptではWriterモナドの値として2要素の配列を使うことができる。最初の要素は任意の値であり、二番目の要素は出力を貯めておくための配列である。
角括弧はこのモナドの値コンストラクタとして使っていて、より単純な要素(配列の0番目は値、1番目にはログ配列)からWriterモナドの値を作っている。
"unit" ("return" はJavaScriptの予約語なのでこちらを使う)は元の値に空のログ配列をつけるだけである。
"bind" は最初のWriterモナドの中の値に関数を適用し、それぞれのログ配列を結合して新しいモナドの値として返す。
"pipeline"は補助関数で関数の配列を順番に "bind" する。
モナドの値を返す関数の例を挙げる。
最後に数学関数のパイプラインにデバッグ情報(デバッグ情報の配列は結合されて、値と一緒に返される)を追加する例を挙げる。
結果の型 formula_13 の継続モナドは型 formula_14 を関数型 formula_15 にうつす。これは継続渡しスタイルのモデルとして使われる。return と bind 関数の定義は
である。
関数は次で定義できる。
研究者により発見されたモナドになる概念を挙げる。自由モナドは自由モノイドと似ていて、直観的には、使う型に依存せずにモナド則(またはモノイド則)を満たすようにできる汎用の構造である。
型 t に対して、codice_2 の自由モノイドは codice_56 であり、結合的な二項演算子として codice_57、単位元として codice_58 を選ぶ。Haskellでは以下のように書ける。
具体的なモノイドとでは、値 codice_59 を二項演算で足すことができるが、codice_58 では単に、結合 codice_61 になるだけであり、単に「一緒に集めた」だけである。ここで、「一緒に集めた」結果がどうなるべきかは定めない。
自由モナドも同じ考えに基づいて定義される。上の定義 codice_62 に函手を挿入することで、次の定義を得る。
codice_63 が値のリストを保存するのと違って、codice_64 はドメインの値を決めた函手のリストを保存する。codice_64 の codice_66 と codice_67 インスタンスの定義によると、受け取った関数を codice_68 でリストに落としているだけしかしていないことが分かる。
コモナドはモナドの圏論的な双対である。型構築子 W "T" と2つの演算 "extract" と "extend" を持つ。"extract" の型は W "T" → "T" であり、"extend" の型は (W "T" → "T' ") → W "T" → W "T' " であり、以下のコモナド則を満たすとする。
他の方法として、"fmap" と "extract" と "duplicate" を使った定義もできる。"fmap" と "extract" により W は余基点付き函手となる。"duplicate" がコモナドを特徴付ける。"duplicate" の型は W "T" → W (W "T") であり、次の規則を満たす。
ふたつの定式化の関係は
で与えられる。
モナドが副作用を表現すると言われているのに対して、コモナド "W" は一種の「文脈」を表している。"extract" は文脈から値を取り出す関数である。"extend" は型 W "A" → "B" を持つ「文脈依存の関数」を合成してパイプラインを作る。
恒等コモナドはもっとも単純なコモナドであり、型 "T" をそれ自身にうつす。"extract"は恒等関数であり、"extend"は関数適用である。
積コモナドは型 formula_14 を積の型 formula_31 にうつす。ここで formula_32 はコモナドの文脈の型である。コモナド演算は
で定める。
関数コモナドは型 formula_14 を関数型 formula_38 にうつす。ここで、formula_39 はモノイド構造を持つ型である。コモナド演算は
で定める。εと * はモノイド formula_39 の単位元と二項演算子である。
余状態コモナドは型 formula_14 を型 formula_46 にうつす。ここで S はストアの型である。コモナド演算は
で定義される。