モナド (プログラミング)
関数型プログラミングにおいて、モナドはプログラムを構造化するための汎用的な抽象概念である。対応したプログラム言語では、ボイラープレート的なコードでもモナドを使って除去することが可能となる。これはモナドが、特定の形をした計算を表すデータ型と、それに関する生成と合成の2つの手続きを提供することによって実現されている。生成は任意の基本型の値をモナドに包んでモナド値を生成する手続きであり、合成はモナド値を返す関数(モナド関数)たちを合成する手続きである。
広い範囲の問題をモナドを使うことで単純化できる。例えば、Maybeモナドを使えば未定義値への対処が簡単になり、Listモナドを使えばリストに入った値を柔軟に扱うことができる。複雑に組み合わさった関数は、モナドを使えば、補助データの管理や制御構造や副作用を除去した簡単なパイプライン構造に置き換えることができる。
モナドの概念や用語は圏論から来ている。圏論においては、モナドは追加の構造を持つ関手として定義されているプログラミングにおいては複数の自由変数上の関数が使われるため、正確にはここでいうモナドは圏論における強モナドである。。1980年代の後半に研究され始め、1990年代前半には、一見無関係な計算機科学上の問題がモナドによって統一的で関数的にモデル化できることが分かってきた。圏論はモナドの満たすべき形式的な条件を与え、これはモナド則と呼ばれている。モナド則はモナド的なコードの形式的検証にも使用可能である。
ある種の計算では、その計算の意味をモナドで明確に表現できることを利用して、プログラム言語の便利機能の実装にモナドを使うことができる。Haskellのように、標準ライブラリですでに汎用のモナド構造といくつかのインスタンスを定義している言語もある。
プログラミングにおけるモナドの概念は1980年代のプログラム言語Opal (プログラミング言語)に見られるが、このときは「コマンド」と呼ばれ形式化はされなかったJune 2013。
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 の値をひとつ持つか、全く持たないかのふたつの状態を表現する型である。これらを区別するために、二種類の代数的データ型構成子を持つ。Just tはひとつの値tを保持し、Nothingは値を何も保持しない。
この型を使って検査例外の簡単なものを作ってみたいとする。計算の途中で失敗した場合は、残りの計算は飛ばして結果Nothingを返し、すべての計算が成功した場合は何か値xを保持したJust xを返すことにする。
以下の例では、addはMaybe Int型のふたつの引数を受け取って、同じ型の結果を返す。mxとmyがともにJustである場合は、これらの値の和をJustに入れて返したい。もし、mxとmyのどちらかがNoneだった場合は、Nothingを返したい。こういった振る舞いをする関数を単純に実装しようとしたら、「if Nothing then Nothing else Just xのxに何かする」の形の場合わけを複数ネストすることになり、すぐやっかいなものになる。
これを楽にするための、各計算を繋げる演算子を定義することができる。二項演算子 bind (>>=) は失敗する可能性のある計算の結果を、もうひとつの失敗する可能性のある計算へ連鎖する。最初の引数が Nothing だった場合は、二番目の引数(関数引数)は無視されて、単に計算全体が失敗する。もし、最初の引数が Just x だった場合は、この値 x は二番目の関数引数に渡されて、この関数の結果として新しい Maybe の値が返される。これは Just な値か失敗かのどちらかになる。
計算の状態に影響を与えずに値を投入する構築子は、Just がすでにそうなっている。
これらを使うことで、上の例は次のように書ける。
さらにdo記法という構文糖衣を使うことで、次のように書くことができる。
この種の操作は頻出なので、Haskellの標準関数(liftM2)が用意されている。これはふたつのモナド的な値(ここでは、ふたつのMaybe)と、その中身(ふたつの数値)を組み合わせる関数(加算)を受け取る関数であり、上の例は次のように書ける。
(上記のdo記法を使ってliftM2の定義を書いてみよう)
Haskellのような純粋関数型言語では関数が外部から観測可能な作用をもつことは、関数の意味から外れることになるため許されない。しかし、関数が直接副作用を起こすのではなく、求める副作用を記述する値を構築して返し、呼び出した側が適当な時に実行することは可能である。Haskellの記法では、型 IO a の値がこのようなアクションを表現していて、実行されたときには、型 a の何らかの値を生成する。
IO型の値は「現在の世界の状態を引数に取り、関数の返り値として変化した状態を持つ新しい世界を返す」という意味でアクションとみなすことができる。例えば、関数 doesFileExist と removeFile は次の型を持つ関数である。
removeFile は関数としては、FilePath を引数にとり、ある IOアクションを返す。このアクションが実行されると世界を、この場合はファイルシステムを FilePath という名前のファイルの存在しないものに変更する。この場合は、IO の持っている値は () 型の値なので、なにも結果は生成されず、呼び出し側はこれを気にする必要はない。一方で、doesFileExist の場合は、関数の返す IOアクションはブール型の値、True または Falseを包んでいて、概念的には、アクションが実行されたときのファイルシステムに FilePath という名前のファイルが存在したかどうかを呼び出し側が知っているという世界の状態を表現している。アクションからアクションへと世界の状態を渡して管理するこの方法により、決まった順序で状態を変化させていくアクションの列を定めることが可能となる。この過程は時相論理において宣言的な命題のみで時間の経過を表現する手法と似ている。以下の例ではプログラム内のアクションを連鎖する方法の詳細を再び Haskell を使って明らかにする。
基本的な種類の入出力操作、標準出力に文字列を書く、標準入力から文字列を読む、ファイルの読み書き、ネットワーク越しのデータ送信等をすべて記述できるようにしたい。さらに、これらのプリミティブを組み合わせて大きなプログラムを作れる必要もある。例えば次のように書けることが望ましい。
この直観的な記法はどのように形式化できるだろうか?このためには各入出力アクションに対して、いくつかの基本的な操作を実行できる必要がある。
ふたつの入出力アクションをひとつにできなければならない。Haskellでは、中置演算子 >> を用いて、putStrLn "abc" >> putStrLn "def" と書き、コンソールに二行の文字列を表示するひとつのアクションである。演算子 >> の型は IO a → IO b → IO b であり、ふたつの入出力アクションを引数に取り、順番に実行するひとつのアクションにまとめて返す。
何もしないという入出力アクションも必要である。これは値を返すがなんの副作用も持たない。Haskellではこのアクションは return で構築され、この型は a → IO a である。
さらに、最初のアクションの結果に基づいて次に実行するアクションを決める必要がある場合がある。Haskellでは、演算子 >>= (bindと読む)を使い、型は IO a → (a → IO b) → IO b である。左側のオペランドは型 a の値を返す入出力アクションであり、右側のオペランドは左側のアクションの生成した値に応じて次に実行する入出力アクションを選択する関数である。結果は、最初のアクションを実行しその値を関数に渡した評価結果をアクションとして実行し、最後にこのアクションの結果を返す合成されたひとつのアクションとなる。
Haskellのこの演算子を使った例は getLine >>= putStrLn であり、これは標準入力から一行文字列を読み込み、それをそのまま標準出力に書き出す。最初の演算子 >> は単にこの演算子の特別な場合である。最初のアクションの結果は無視され、二番目のアクションは常に同じである。
これらの3つの演算子をプリミティブな入出力操作として「いかなるプログラムでも」書けるかというのは、データ変換(ラムダ式使う)やif式やループを含めたとしても自明であるとは言いがたい。上の例は長い式を使って次のように書ける。
bind 演算子の持つパイプライン構造により、getLine と putStrLn の命令はそれぞれ一度だけ、書いた順序で評価され、入力ストリームから文字列を取り出して、出力ストリームに出力する副作用は関数型パイプラインの中でも正しく実行される。これは言語が関数をアウト・オブ・オーダー実行や遅延評価する場合でも成立する。
明らかに入出力の定義とMaybeの定義は共通の構造を持っているが、異なっている部分も多い。上述した構造や他のよくある似たような多くの構造の抽象化である。モナドの一般化された概念は、計算の一部が副作用を発生させるにも関わらず、純粋関数的な計算として実行したくなるようなあらゆる状況を包含している。
モナドは、元となる型システムが与えられたとき、その内部に対応する型システム(モナド的型システムと呼ぶ)を埋め込む構成である(つまり、各モナド的型は元のシステムの型でもある)。このモナド的型システムは元の型システムの重要な点は全て保存するが、モナドに固有の特徴を追加する技術的には、モナドは元となる型を保存する必要はない。例えば、全ての演算子がただひとつの多相な値を返すような自明なモナドもモナドの公理を満たす。逆に、モナドは新しい構造を追加する必要はない。元の型を変更せずそのままとする恒等モナドはモナドの公理を満たし、モナド変換子の再帰の基点として有用である。。
モナドは複数の方法で定義できる。そのうちの1つは「モナドとは、ある法則(モナド則)を満たす三つ組みである」 で表される定義である。すなわち圏論のクライスリ圏におけるクライスリトリプルである。
三つ組は以下の要素からなる。
型構築子Mは、ある型 x から派生した型を得る方法を定める。得られる型はしばしば M x で表記される。
unit関数( return ともこの名前は後述する do 記法での使われ方から来ている。)は型 x の引数を型 M x の返り値にうつす関数である。すなわちunit 関数は型 x の値をモナド型 M x の値へうつす多相な関数である。引数値変換の有無は定義されない(一般には値を変換せずに保持する(例:入力 1 =>出力 Maybe(1))。
bind関数( >>= とも)は「モナド的値」と「値をモナド的値へうつす関数」を引数にとり、第1引数のモナド的値を第2引数関数の値域へうつす。
三つ組 {M, unit(return), bind(>>=)} をモナドと成す規則をモナド則という。以下がモナド則である。
return は <tt>>>=</tt> に対して、ほとんど単位元である。
ふたつの関数を続けて bind するのは、これらの関数から決まるひとつの関数を bind することに等しい。
公理を 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 は元の型である)、結合律と左右からの単位元律を満たす。
このことから、加法的モナドはモノイドでもある。>>=に対しては、mzeroはヌル要素として振舞う。ゼロを掛けるとゼロになるように、mzero と関数を bind すると結果はゼロとなる。
同様に、モナド的な値 m とつねにゼロを返す関数を bind すると、結果はゼロである。
直観的には、モナド的値としてのゼロはモナドに関連した構造だけを持ち元の型の値を持たない。Maybe モナドでは、「Nothing」がゼロである。リストモナドでは、空リストがゼロである。
bind 演算子 >>= を直接使ってプログラムを書くのがよい場合もあるが、典型的にはdo記法 (OCamlでは perform記法、F#ではコンピュテーション式)を使用し、命令型言語のような見た目にできる。コンパイラはdo記法の式を >>= を使ったものに変換する。例えば
を変換した結果は以下となる。
リストモナドの実装を見てみよう。リストにマップを行い結合を行う(平らにする)この関数は concatMap とも呼ばれる。
よって、以下のような変形が行われて、それぞれの式はすべて等価である。
リスト<tt>[1..2]</tt>は使われないことに注意しよう。左矢印をつけないことで、連鎖した関数は引数を無視するように変換されて、値ではなくモナド的な構造だけが問題になることを示すことができる。例えば、状態モナドで値を生成せずに状態だけを変化させるようなときに使われる。do記法は >>= の単なる構文糖衣であるので、どんなモナドに対しても使うことができる。
Maybeモナドの値を安全に割り算する以下の定義も等価である。
同様にF#ではコンピュテーション式を使うことができる。
このmaybeブロックは構文糖衣であり、以下の式に変換される。
安全な割り算の結果は、値が <tt>Nothing</tt> であるかを素直に確認せずに(つまり割り算の結果があるかを確認せずに)そのまま計算に使えたほうが便利なことがある。これは「持ち上げ」関数を使うことで可能である。この関数は <tt>Maybe</tt> に限定されず、任意のモナドに対して定義される。Haskellでは <tt>LiftM2</tt> と呼ばれる。
関数型を示す矢印が右結合であることから、<tt>liftM2</tt>は2引数の関数を引数に取って、さらなる2引数の関数を返す。型シグネチャは <tt>m</tt> がモナドのとき、任意の2引数関数を「持ち上げ」ることができることを示している。例えば、
は演算子 <tt>(.*.)</tt> を定義し、それは引数がどちらも <tt>Nothing</tt> でないときに、掛け算した結果を返す(どちらかが <tt>Nothing</tt> の場合は <tt>Nothing</tt> を返す)。この方法の利点はモナドの実装に深入りする必要がないことである。他の同じような関数やモナドを使う場合でも、<tt>liftM2</tt> を使うことで、やりたいことが鮮明になる(コードの再利用を見よ)。
数学的には、liftM2 は次のように定義される。
\text{liftM2} \colon \forall M \colon \text{monad}, \; (A_1 \to A_2 \to R) \to M \, A_1 \to M \, A_2 \to M \, R =  op \mapsto m_1 \mapsto m_2 \mapsto \text{bind} \; m_1 \; (a_1 \mapsto \text{bind} \; m_2 \; (a_2 \mapsto \text{return} \; (op \, a_1 \, a_2)))
もっとも単純なモナドは恒等モナドであり、値になんの情報も付加しない。
このモナドのdoブロックは変数の代入を実行する。例えば、<tt>do {x <- 2; return (3*x)}</tt> の結果は6である。
圏論的な見方をすると、恒等モナドは恒等函手の間の随伴から得られる。
リストや集合や多重集合といった、おなじみのコレクション型にもモナドであるものがある。リストの場合の定義は次のようになる。
リスト内包記法はリストモナドに固有のもので、例えば、リスト内包 <tt>[ 2*x | x <- [1..n], isOkay x]</tt> はリストモナドでの計算 <tt>do {x <- [1..n]; if isOkay x then return (2*x) else mzero;}</tt> に対応している。
リスト内包記法は集合の内包記法に似ているが、集合はモナドにならない。なぜなら、計算の型は等しいかどうかを比較可能でなければならない — モナドは計算の型についてこれを課していないように見えるにも関わらず — という制限があるからである。実際、集合は制限されたモナド(restricted monad)である。コレクションによるモナドは非決定計算を自然に表現する。リスト(または他のコレクション)は非決定的な計算の異なった実行パスが今の時点で取りうる結果の全体を表す。例えば、<tt>x <- [1,2,3,4,5]</tt> を実行したとすると、変数 <tt>x</tt> はこのリストから非決定的に選ばれた値であるとみなしている。<tt>x</tt>を返すことは、各実行パスの結果を値のリストにまとめることを意味する。さらに、bind 演算子では今の可能な結果に対して関数 <tt>f</tt> を実行し、それぞれの結果をひとつのリストに結合する。
<tt>if condition x y then return () else mzero</tt>の形の文はよく現れる。条件が真である場合は何もしない計算を非決定的に選択し、意味のある値は返さない。一方で、条件が偽の場合は、選択肢の存在しない非決定な値 <tt>mzero = []</tt> を返す。つまり、この実行パスを終了し、他の実行パスは走り続ける。これは条件を満たす特定の実行パスだけを通す「ガード」の役割を果たす。よって、コレクションモナドは論理パズルや数独などの問題を解く場合に役に立つ。
Haskellのように遅延評価を行う言語では、リストはその要素が必要になって初めて評価される。例えばリストの最初の要素を要求した場合は、最初の要素だけが計算される。非決定計算にリストモナドを使用した場合には、全ての計算からなる結果は非決定的に遅延リストとして生成され、最初の要素を要求すると、必要最小限の計算のみが行われて、最初の結果を返す。この過程はバックトラックに対応している。ある実行パスにいて計算に失敗した(<tt>mzero</tt>に評価された)場合、最後に行った分岐までバックトラックを行い、そこから次のパスを選んで実行を続ける。二番目の要素が要求された場合、再びその答を得るのに必要な最小限の計算が行われる。よって、遅延評価の言語ではリストモナドを使って簡単にバックトラックを実装することができる。
圏論的な見方をすると、コレクションモナドは集合の圏とモノイドの圏の間の自由函手と忘却函手の随伴から作られる。
状態モナドは状態を付加した計算の型である。好きな型に対して、その型を状態として持つ状態モナドを作ることができる。これは状態を引数に取り、通常の返り値(型 <tt>t</tt> の値)と新しい状態(型 <tt>s</tt> の値)を返す関数である。
今までに見たモナドと違い、状態の型という型引数を取ることに注意しよう。モナドの演算は次のように定義される。
状態を操作する関数を挙げる。
初期状態を指定して状態モナドを実行する関数。
状態モナドのdoブロック内の計算は状態の確認や更新を行うことができる。
非形式的に書くと、状態の型 S を持つ状態モナドは返り値の型 T を関数型  S \rarr T \times S に変換する。return は単に
\text{return} \colon T \rarr S \rarr T \times S = t \mapsto s \mapsto (t, s)
であり、bind は
\text{bind} \colon (S \rarr T \times S) \rarr (T \rarr S \rarr T' \times S) \rarr S \rarr T' \times S \ = m \mapsto k \mapsto s \mapsto (k \ t \ s') \quad \text{where} \; (t, s') = m \, s
である。
圏論的な見方では、状態モナドはデカルト閉圏の定義に現れる積函手と指数函手の随伴から作られる。
環境モナド(Readerモナドや関数モナドとも呼ばれる)は環境を共有した計算を可能にする。型構築子は型 T を 関数型 E → T にうつす。ここで、E は共有したい環境の型である。モナド演算子は
\text{return} \colon T \rarr E \rarr T = t \mapsto e \mapsto t
\text{bind} \colon (E \rarr T) \rarr (T \rarr E \rarr T') \rarr E \rarr T' = r \mapsto f \mapsto e \mapsto f \, (r \, e) \, e
で定義される。
\text{ask} \colon E \rarr E = \text{id}_E
\text{local} \colon (E \rarr E) \rarr (E \rarr T) \rarr E \rarr T = f \mapsto c \mapsto e \mapsto c \, (f \, e)
がある。ask は現在の環境を取得するのに使う。localは指定した計算を一時的に変更した環境で行う。状態モナドと同様に、環境モナドによる計算は単に環境の値にモナドの値を適用することで実行できる。
Writerモナドは本来の計算を行いながら、様々な種類の追加の出力を作成することができる計算である。ログ取りやプロファイリングに有用である。元の型 T に対して、Writerモナドは型 W × T の値を持つ。ここで、W はモノイド則を満たす演算を持った型である。すなわち、W は二項演算子 (a,b) → a*b と単位元 ε を持っており、これは結合則 (a*b)*c = a*(b*c) を満たし、任意の x について、x*ε = ε*x = x を満たす。
\text{return} \colon T \rarr W \times T = t \mapsto (\epsilon, t)
\text{bind} \colon (W \times T) \rarr (T \rarr W \times T') \rarr W \times T' = (w, t) \mapsto f \mapsto (w * w',\, t') \quad \text{where} \; (w', t') = f \, t
で定義される。ここで、εと * はモノイド W の単位元と二項演算子である。
JavaScriptではWriterモナドの値として2要素の配列を使うことができる。最初の要素は任意の値であり、二番目の要素は出力を貯めておくための配列である。
角括弧はこのモナドの値コンストラクタとして使っていて、より単純な要素(配列の0番目は値、1番目にはログ配列)からWriterモナドの値を作っている。
unit (return はJavaScriptの予約語なのでこちらを使う)は元の値に空のログ配列をつけるだけである。
bind は最初のWriterモナドの中の値に関数を適用し、それぞれのログ配列を結合して新しいモナドの値として返す。
pipelineは補助関数で関数の配列を順番に bind する。
モナドの値を返す関数の例を挙げる。
最後に数学関数のパイプラインにデバッグ情報(デバッグ情報の配列は結合されて、値と一緒に返される)を追加する例を挙げる。
結果の型 R の継続モナドは型 T を関数型 \left( T \rarr R \right) \rarr R にうつす。これは継続渡しスタイルのモデルとして使われる。return と bind 関数の定義は
\text{return} \colon T \rarr \left( T \rarr R \right) \rarr R = t \mapsto f \mapsto f \, t
\text{bind} \colon \left( \left( T \rarr R \right) \rarr R \right) \rarr \left( T \rarr \left( T' \rarr R \right) \rarr R \right) \rarr \left( T' \rarr R \right) \rarr R= c \mapsto f \mapsto k \mapsto c \, \left( t \mapsto f \, t \, k \right)
である。
call-with-current-continuation関数は次で定義できる。