プログラミングで「モナド」という言葉に出会うと、入出力やエラー処理を包む特別な箱として説明されることがあります。圏論では、モナドは自己関手T、単位η、乗法μからなる構造です。二つの説明は無関係ではありませんが、「箱」だけでは合成の規則が見えません。
モナドの核心は、通常の射A → Bではなく、A → T(B)という効果付きの射を順番につなぐ方法を与えることです。候補が複数ある計算、失敗する計算、状態を変える計算などを、同じ入力・出力の構造で合成できます。圏論上の定義からプログラム上のbindまでを、飛躍なくつないでみましょう。
自己関手Tが対象へ計算の文脈を加える
圏C上のモナドは、自己関手T: C → Cから始まります。Tは対象AをT(A)へ、射f: A → BをT(f): T(A) → T(B)へ送ります。Setなら、T(A)をAの値だけでなく追加の文脈を持つ集合として考えられるでしょう。
リストモナドではT(A)はAの有限列全体、MaybeモナドではAの値または失敗、状態モナドでは状態を受け取って値と新状態を返す計算です。ただし具体例ごとにTの形は同じではありません。共通するのは、Tが関手として通常の関数を文脈付きの世界へ持ち上げる点にあります。
単位ηが普通の値を文脈へ入れる
モナドには自然変換η: IdC ⇒ Tがあります。各対象AについてηA: A → T(A)を与え、普通の値を最小限の文脈へ入れます。
リストなら要素aを一要素リスト[a]へ、Maybeならaを成功値Just aへ送ります。ηは任意の初期化関数ではありません。射f: A → Bとの自然性により、先にfを適用してから文脈へ入れる道と、文脈へ入れてからT(f)で移す道が一致します。
乗法μが二重の文脈を一段へ平らにする
もう一つの自然変換がμ: T² ⇒ Tです。対象AごとにμA: T(T(A)) → T(A)を与え、二重になった文脈を一段へまとめます。
リストならリストのリスト[[a,b],[c]]を[a,b,c]へ連結します。MaybeならJust (Just a)をJust aへ、外側か内側が失敗なら失敗へまとめます。μがなければ、効果付き計算を重ねるたびにTが増え、同じ形の計算として続けられません。
二つの単位律が余分な包装を消す
単位を外側に加えてからμで平らにする操作μ∘ηTと、内側の各値へ単位を加えてから平らにする操作μ∘Tηは、どちらもT上の恒等自然変換にならなければなりません。
リスト[a,b]を一つのリストとして[[a,b]]へ包んでから連結しても、各要素を[[a],[b]]へ変えてから連結しても、[a,b]へ戻ります。単位は計算へ余計な効果を持ち込まず、合成の始点と終点で中立に働きます。
結合律が三段の文脈を同じ結果へまとめる
T(T(T(A)))をT(A)へ平らにする道は二つあります。外側から二段をまとめてから残りをまとめる道と、内側を先にまとめてから外側をまとめる道です。モナドの結合律がなければ、この二つの一致は保証されません。
リストのリストのリストなら、どの段から連結しても要素の順序が同じ一つのリストになります。効果付き計算を三つ以上つないだとき、括弧の位置で意味が変わらないための法則です。
Kleisli射が効果付き関数を表す
モナドTに対するKleisli射A → Bは、元の圏Cでは射A → T(B)です。普通の入力Aから、文脈付きの出力T(B)を返します。失敗しうる関数ならA → Maybe(B)、複数の候補を返す関数ならA → List(B)という形になるでしょう。
二つのKleisli射f: A → T(B)とg: B → T(C)は、そのままでは通常の合成ができません。gの定義域はBなのに、fの出力がT(B)だからです。モナドはgをT(g): T(B) → T(T(C))へ持ち上げ、μでT(C)へ平らにすることで合成を作ります。
bindが持ち上げと平坦化を一つにする
プログラミングでは、T(A)の値mと関数k: A → T(B)からT(B)を作る操作をbindと呼び、m >>= kなどと書きます。圏論的にはμB∘T(k)をmへ適用する操作です。
Maybeならmが失敗のとき失敗をそのまま伝え、成功値aならk(a)へ進みます。Listならmの各要素へkを適用して得たリストのリストを連結します。効果の具体的な伝播規則がTとμに入らなければ、同じbindの形で計算をつなげられません。
Maybeモナドが失敗の伝播を合成する
文字列を整数へ変換するparse: String → Maybe(Int)と、ゼロでない整数の逆数を返すreciprocal: Int → Maybe(Float)を考えます。通常の関数合成ではparseの出力Maybe(Int)をreciprocalへ渡せません。
Kleisli合成なら、parseが失敗すればそこで失敗、整数が得られればreciprocalへ進みます。空文字、数値でない文字列、ゼロという異なる失敗箇所を、例外的な分岐を毎回手書きせず一つの合成規則へまとめられます。ただしエラー理由を保持したいなら、Maybeでは足りません。Eitherなど別のTを選ぶ必要があります。
リストモナドが非決定的な分岐をつなぐ
関数neighbors: Place → List(Place)が一地点から一手で行ける複数地点を返すとします。地点Aの隣接先すべてについてさらにneighborsを適用すると、二手で行ける地点のリストのリストができます。
bindはこれを一つのリストへ連結します。各段階で複数の可能性が生じる探索を、Kleisli合成として扱えるわけです。重複除去や最短距離はリストモナドの法則から自動的には得られません。必要なら集合、重み付き関係、探索戦略など追加の構造を選びます。
状態モナドが状態の受け渡しを隠さず管理する
状態Sを伴う計算の一つのモデルはT(A)=S → (A×S)です。計算は状態を受け取り、値Aと更新後の状態を返します。二つの計算をbindでつなぐと、最初の新状態が次の入力状態へ渡されるでしょう。
表面上は値だけを順に扱いながら、背後では状態が明示的な関数引数として流れます。「副作用を消す」のではなく、状態変化を合成可能な値としてモデル化しているのです。実装の評価順や並行実行の問題まで自動解決するわけではありません。
随伴から生まれるモナド
随伴F ⊣ Gがあると、C上の自己関手T=GFにモナド構造が入ります。単位は随伴の単位η: IdC ⇒ GF、乗法はGεF: GFGF ⇒ GFで、εは余単位です。
自由モノイド関手Fと忘却関手Gの随伴から得られるGFは、集合を有限列の集合へ送るリストモナドです。二重の自由モノイドを平らにする乗法は、列の列を連結します。モナドがプログラミングだけの技巧ではなく、随伴の往復から自然に生まれる構造だと分かるでしょう。
Eilenberg–Moore代数が文脈を解釈する
T-代数は対象Aと射a: T(A) → Aからなり、単位と乗法に整合する条件を満たします。Tが加えた形式的な文脈を、Aの中で実際の操作として評価する仕組みです。
リストモナドの代数はモノイドに対応します。有限列を一つの要素へ畳み込む写像は、空列の値として単位元を、二要素列の畳み込みとして二項演算を与えます。モナドが「自由に形式を作る側」なら、その代数は形式を意味ある構造へ解釈する側といえるでしょう。
モナドは副作用そのものではない
モナドを「副作用を扱う箱」とだけ定義すると、恒等モナド、リストモナド、確率モナドなどの共通点が見えません。圏論上のモナドは、自己関手と二つの自然変換が法則を満たす構造です。
プログラミングでは、この構造が計算効果の意味論やプログラム構成に有効だったため広まりました。すべての副作用が一種類のモナドで自然に組み合わさるわけではなく、複数の効果を合成する問題も残ります。モナド変換子や代数的効果など、目的に応じた別の道具があります。
「モナドは自己関手の圏のモノイド」の意味
圏C上の自己関手を対象、自然変換を射とする関手圏[C,C]を考えます。関手の合成をモノイダル積、恒等関手を単位対象とすると、モナド(T,η,μ)はこのモノイダル圏におけるモノイド対象です。
この有名な説明は正確ですが、最初の定義としては情報を圧縮しすぎています。モノイドの単位元がη、乗法がμに対応し、結合律と単位律がそのままモナド則になります。モノイダル圏を理解する前に、この一文だけで全体をつかむのは難しいでしょう。
計算をつなぐ規則としてのモナド
モナドの価値は難しい計算を自動化することではなく、文脈付きの計算をつなぐ規則を明示することです。単位は普通の値を計算へ入れ、乗法またはbindは重なった計算を一つへ戻し、法則は括弧や余分な包装に意味が左右されないことを保証します。
どのTを選ぶかによって、失敗、分岐、状態、入出力などの意味は変わります。圏論が統一するのは効果の内容ではなく、その合成形式に限られます。「箱」の比喩を覚えるだけでは、モナドを理解したとはいえません。何を文脈として加え、どう平らにし、どの法則の下で連結するかを見分ける必要があります。
圏論からプログラミングへ渡った歴史
モナドは1950年代末から60年代に、随伴や代数的構造を研究する圏論の中で整えられました。計算機科学へ大きく接続したのは、Eugenio Moggiが1989年以降、計算効果の表示にモナドを用いた研究です。Philip Wadlerらがその考えを関数型プログラミングの構成へ展開し、Haskellの入出力にも採用されました。
したがって、プログラマが後から圏論用語を比喩的に借りたわけではありません。Kleisli圏や単位・乗法という数学的構造が、効果付き計算を合成する必要と合致しました。一方、言語実装で「Monad」と呼ばれる型クラスは、圏論上のデータをプログラムで使いやすい形へ表したものです。両者の水準は区別しなければなりません。
Kleisli圏とEilenberg–Moore圏の二つの見方
同じモナドTから、Kleisli圏とEilenberg–Moore圏という二つの圏を作れます。Kleisli圏は効果付き射A → T(B)の合成を前面に出し、プログラムを順に接続する側に向いています。
Eilenberg–Moore圏はT-代数と、それを保つ射を集めます。形式的な効果や自由構造を、具体的な演算で解釈する側です。二つは競合する説明ではありません。モナドが可能にする計算と、モナドを受け入れる構造を別方向から捉えています。