モナドの架け橋:プログラミング言語と圏論の繋がりを俯瞰図で解説
プログラミング言語における『モナド』という概念は、特に関数型プログラミングを学ぶ人々にとってとりわけ興味深いテーマだと思います。
このプログラミング言語における『モナド』ですが、よく知られているように圏論という数学の一分野と深いつながりを持っています。
当記事では、これらプログラミング言語のモナドと圏論の諸概念について、次の俯瞰図を用いてそれぞれの橋渡しを試みます。
私と一緒にモナドの解像度を高めていきましょう!
前置き
少し前置きをさせてください。
前提知識について
プログラミング言語の知識について
プログラミング言語のモナドに定義されているpure
や>>=
などの関数は知っている前提で進めます。
圏論の知識について
『圏』は知っている前提で進めます。
『関手』『自然変換』などについては、今回の図に登場するので説明します。
これらについて私の記事でよければ冒頭に載せたここにも説明がありますので、よかったら必要に応じて参照してみてください。
具体的なコードについて
具体的なコードはPureScriptで書きますが、PureScriptが書けなくても知らなくても内容は理解できます。
なぜならこの記事で登場するコードはほぼすべてシンプルなものだからです。
PureScriptを知らない方向けに関数定義の構文を書いておきます。
これだけ覚えておいていただければあとはノリで理解できるので大丈夫です。
-- 【意味】
-- 関数名 :: 引数1 -> 引数2 -> 戻り値
function :: String -> String -> Int
function = 関数の実装
関数のシグニチャの定義は::
の左側が関数名です。
右側が->
で区切られた引数と戻り値の型になっており、区切られた最後の型が戻り値の型です。
プログラミング言語と圏論を繋ぐ俯瞰図
俯瞰図の説明をします。
図の左側がプログラミング言語の世界、右側が圏論の世界で、同じ世界の中で関係しているものは実線、世界をまたいで関係しているものは破線で結ばれています。
大きな図ですが、次の順番で少しずつ説明していきます。
-
型構築子や
map
関数と自己関手の対応
まずは、型構築子やmap
関数と自己関手の関係を説明します。
これは割とモナドの前提となるような話です。
-
次にプログラミング言語のモナドと、圏論のモナドの対応を説明します。
-
最後にプログラミング言語のモナドに定義される
join
関数と>>=
関数のつながりには理論的な背景があるということを圏論の世界を通して説明します。
map
と自己関手の対応
型構築子/まずは図のこの部分のお話をします。
型構築子
型構築子とは、新たな「型」を作り出すための関数のようなもの※です。
※Haskell/PureScriptにおける一般的な関数ではないが、型を別の型に写すという振る舞いは関数に近い
具体例としてMaybe
を挙げてみます。
Maybe
は次のように定義されています。
data Maybe a = Nothing | Just a
Maybe
は型引数a
をとり具体的な型を生成します。
Maybe String
とかMaybe Int
とかですね。
map
map
関数は、PureScriptやHaskellでは型クラスFunctor
に定義されている関数です。
(Haskellではfmap
という名前になっています)
class Functor f where
map :: forall a b. (a -> b) -> f a -> f b
他の言語でも色々な型に定義されていますね。
この関数が何をするかの説明は不要でしょう。
関手
圏
-
対象関数:
の各対象{\footnotesize \mathbf{C}} を{\footnotesize X} の各対象{\footnotesize \mathbf{D}} に対応させる{\footnotesize \mathrm{F}(X)} -
射関数:
における射{\footnotesize \mathbf{C}} を{\footnotesize f:X\rightarrow Y} における射{\footnotesize \mathbf{D}} に対応させる{\footnotesize \mathrm{F}(f):\mathrm{F}(X)\rightarrow \mathrm{F}(Y)}
からなるものであり以下の性質を満たす。
-
恒等射を保存する: 各対象
に対して{\footnotesize X\in \mathbf{C}} {\footnotesize \mathrm{F}(\mathrm{id}_X) = \mathrm{id}_{\mathrm{F}(X)}} -
射の合成を保存する: 任意の射
および{\footnotesize f:X\rightarrow Y} に対して{\footnotesize g:Y\rightarrow Z} {\footnotesize \mathrm{F}(g\circ f) = \mathrm{F}(g)\circ \mathrm{F}(f)}
視覚化するとこのようなイメージでしょうか。
対象関数と射関数のイメージ
恒等射/合成射を保存するイメージ
(Wikipediaに書かれていた定義がカジュアルで良さそうだったので拝借させてもらいました(ちょっと改変してますが))
自己関手
同じ圏
自己関手のイメージ
関手がわかっていればこれは簡単ですね。
対応
ここで、プログラミング言語側の『型構築子』『map
』の関係性と圏論側の『自己関手』『対象関数』『射関数』の関係性を並べてみてみましょう。
プログラミング言語側の方は具体的な型を当てはめてみました。
両者に対応関係があることがわかると思います。
最初お見せした図の意味がおわかりいただけたでしょうか。
ちなみに、関手の『恒等射/合成射を保存する』という条件は、プログラミング言語においてはFunctor則と呼ばれるものに対応しています。
次はPureScriptで表現したFunctor則です。
map identity a == identity a
map (f <<< g) a == (map f <<< map g) a
関手の条件と比べてみるとこれらが対応関係にあるのがわかると思います。
{\footnotesize \mathrm{F}(\mathrm{id}_X) = \mathrm{id}_{\mathrm{F}(X)}} {\footnotesize \mathrm{F}(g\circ f) = \mathrm{F}(g)\circ \mathrm{F}(f)}
プログラミング言語のモナドと圏論のモナドの関係
まず図のこの部分を見てください。
圏論のモナド側から見ると、
自然変換 pure
が対応しており、
自然変換 join
が対応しています。
(アルファベットは異なりますが、形が一致していることがわかるはずです)
プログラミング言語のモナドに馴染みがある方は「おや?」と思われなかったでしょうか?
「>>=
はどこにいったんだ?」
と。
そう、プログラミング言語側のモナドでpure
の他に一般的に定義される関数は>>=
なのですが、>>=
と圏論のモナドは直接の対応がありません。
対応しているのはjoin
です。
更に圏論のモナドの定義には「可換図式」という条件があり、その条件に対応するのがプログラミング言語側のモナドにおける「pure
とjoin
によるモナド則」なのです。
pure
とjoin
によるモナド則
みなさんがよく見かけるモナド則は次のような形をしていると思いますが、このモナド則は>>=
ではなくjoin
を使って書くこともできます。
-
左単位律:
pure a >>= f == f a
-
右単位律:
m >>= pure == m
-
結合律:
(m >>= g) >>= h == m >>= (\x -> g x >>= h)
join
を使って書くとこうなります。
(単位律のm
はモナドの値、結合律のm
はネストされたモナドの値です)
-
左単位律:
join (map pure m) == m
-
右単位律:
join (pure m) == m
-
結合律:
join (join m) == join (map join m)
このあたりの対応を詳しくみていきたいのですが、それにはまず圏論のモナドの定義を知る必要があります。
モナド
圏
自然変換とか可換について一応補足しておきます。
補足:自然変換
自然変換
また
ざっくりいうと、自然変換とは、関手から関手への射みたいなものですね。
視覚化したものが以下です。
自然変換のイメージ
補足:可換
図式が可換であるとは、異なる経路を辿っても結果が同じになることをいいます。
可換である図式を可換図式といいます。
モナドの図式は可換なのですが、これは次が成り立つことを意味しています。
- 左の図式:
{\footnotesize \mu\circ \mathrm{T}\mu = \footnotesize \mu\circ \mu\mathrm{T}} - 右の図式:
{\footnotesize \mu\circ \mathrm{T}\eta = \footnotesize \mu\circ \eta\mathrm{T}}
対応
関数と自然変換の対応
プログラミング言語側のモナドの関数と圏論側のモナドの自然変換の対応を見てみましょう。
そのために自然変換
{\footnotesize \eta_{A}:A\rightarrow \mathrm{T}(A)} {\footnotesize \mu_{A}:\mathrm{T}(\mathrm{T}(A)) \rightarrow \mathrm{T}(A)}
これを用いて比較したものが次になります。
プログラミング言語側 | 圏論側 |
---|---|
pure :: a -> m a |
|
join :: m (m a) -> m a |
どうでしょうか。形が一致していますね。
これで図の赤枠のところがわかりました。
モナド則と可換図式の対応
あらためて可換図式を見てみましょう。
まず可換図式に出てくる
自然変換 |
|
意味 | プログラミング言語で表したもの |
---|---|---|---|
|
map pure m |
||
|
pure m |
||
|
map join m |
||
|
join m |
上記の表をベースに、図式の経路と、その経路をプログラミング言語で表したものの表を作ってみます。
経路 | プログラミング言語で表したもの |
---|---|
join (map pure m) |
|
join (pure m) |
|
join (map join m) |
|
join (join m) |
図式が可換であるとは、異なる経路を辿っても結果が同じになることでしたので、条件の形にしてみましょう。
図式の条件 | プログラミング言語で表したもの |
---|---|
join (map pure m) == join (pure m) |
|
join (map join m) == join (join m) |
すると、これはモナド則と一致します。
- 左単位律:
join (map pure m) == m
- 右単位律:
join (pure m) == m
- 結合律:
join (join m) == join (map join m)
可換図式とモナド則の対応がわかりました。
これで次の図の意味がおわかりいただけたかと思います。
join
と>>=
クライスリ圏を通してつながる最後はこの図のこの部分の説明をします。
クライスリ圏とクライスリトリプル
クライスリ圏とは、クライスリトリプルと呼ばれる3つ組
上記の3つ組を見ると、ほとんどモナドと共通の要素をもっていることがわかるかと思います。
違いといえば
- モナドには自然変換
があるがクライスリトリプルにはない。{\footnotesize \mu} - クライスリトリプルには拡張演算子
があるがモナドにはない。{\footnotesize -^{*}}
ということです。
そして、
つまりモナドからクライスリトリプルを構成することができ、クライスリトリプルからモナドを構成することができるわけです。
どういうことか見ていきましょう。
拡張演算子
まずはこの意味を理解しましょう。
拡張演算子は、いってみれば射をもとに別の射をつくる射(関数から別の関数を作る関数みたいなやつ)です。
クライスリ圏の射は
という射になります。
図にするまでもないかもしれませんが、こんなイメージです。
=<<
の対応
拡張演算子とこのような拡張演算子を、プログラミング言語で表すとしたらa -> t b
という関数をt a -> t b
という関数に変換する関数になるので、このようになるのではないでしょうか。
拡張演算子 :: (a -> t b) -> (t a -> t b)
というかこれは=<<
そのものです。
PureScriptでは次のように定義されています。
bindFlipped :: forall m a b. Bind m => (a -> m b) -> m a -> m b
bindFlipped = flip bind
infixr 1 bindFlipped as =<<
=<<
は>>=
の引数の順序を入れ替えたものなので、実質同じ関数だと考えてよいでしょう。
ここまでで、図の次の部分のつながりがわかりました。
続いて赤枠の部分を見ていきます。
拡張演算子を使ってμを定義する
まず
拡張演算子にこの恒等射を適用した
どこかで見た形をしていますね?
そう、
μを使って拡張演算子を定義する
射
この
これは図にしてみると意味がわかると思います。
ここまでで
=<<
を使ってjoin
を定義することの対応
拡張演算子を使ってμを定義することと、拡張演算子を使って
そして拡張演算子はプログラミング言語のモナドにおいては=<<
なのでした。
恒等射がプログラミング言語のモナドにおいてidentity
だとすると、identity
を=<<
に適用すればよさそうです。
実際、PureScriptのjoin
は次のように定義されています。=<<
ではなく引数の順序を入れ替えた>>=
を使っていますが、もうそのまんまといってもいいでしょう。
join m = m >>= identity
join
を使って>>=
を定義することの対応
μを使って拡張演算子を定義することと、
まずこの場合のmap
です。
join
に対応していますね。
ということは、map
してjoin
すればよさそうです。
実際このように実装することができます。
bind m f = join (f <$> m) -- <$>はmapの中置演算子
infixl 1 bind as >>=
まとめ
>>=
とjoin
がお互いを使って定義可能であるということは、モナドとクライスリトリプルはお互いに構成可能という理論的背景でもってつながっていました。
このような背景をコードレベルでも確認できるのが面白いところです。
ということで図の次の部分の説明が終わりました。
>=>
の対応
クライスリ射の合成とここまでで正直私が書きたかったことはもう書けてしまっているのですが、クライスリ射の合成についても触れておきます。
図のこの部分です。
クライスリ射の合成
クライスリ圏の射とは
ここで
>=>
との対応
拡張演算子はプログラミング言語のモナドにおいては=<<
で引数の順序を入れ替えれば>>=
なので、
クライスリ射に対応するa -> m b
という関数と>>=
を組み合わせれば、
実際PureScriptでは>=>
は次のように定義されています。これもそのまんま一緒ですね。
composeKleisli :: forall a b c m. Bind m => (a -> m b) -> (b -> m c) -> a -> m c
composeKleisli f g a = f a >>= g
infixr 1 composeKleisli as >=>
おわりに
以上で冒頭にお見せした馬鹿でかい図のすべての説明が終わりました。
あらためて俯瞰してみていかがでしょうか。
自分は全体像を最初に見たいタイプなので、こういうものはまさに自分がほしかったものなのですが、みなさんはいかがでしたでしょうか。
あとがき (なぜ書いたか)
私は最初『圏論のモナドとPureScriptのモナドの関係性を知ろう』という記事を書いたとき、クライスリ圏のことはよく知らなかったんです。
それゆえ、そのときはjoin
の対応については書いたものの、join
と>>=
のつながりについての説明が、「一方を使ってもう一方を実装することができます」程度のものになってしまっていました。
一方で『クライスリ圏とプログラムの関係』という記事では、圏論側と>>=
の対応については言及したものの、記事のテーマ的にjoin
とのつながりには言及しませんでした。
ということで「こんな感じにつながっているのか」という図はずっと頭の中にあったのですが、「書かなくてもええか」とアウトプットはしてきませんでした。
が、喉に何かが詰まった感じではあったため、「やっぱ書くか」と11月くらいからちょとずつ書いてきて年末にようやく書き終わりました。
一日のうち自由に使える時間が非常に少ないため、終わるまでやけに時間が掛かってしまいました。
あー、自由時間がほしい。
Discussion
たまたま通りすがりです。興味ある内容をわかりやすく解説いただきありがとうございます。自分の考えの整理にもなります。
プログラミングのモナドと圏論のモナドを対比ということですが、プログラミングのモナドと直接対応するのがクライスリ・トリプルであるなら、圏論のモナドの性質とかjoin演算子とかの難しい話に入りこまないシンプルにした話も読んでみたいと思いました。
個人的な意見として、プログラミングのモナドを理解したいならモナドじゃなくてクライスリ圏にウェイト置いた理解するのが正しい理解の仕方だと思うからです。
個人的な意見として、圏論のモナドの可換図式といくらにらめっこしてもプログラミングのモナドはわからないと思います。なぜかと言うと、記事の内容を自分なりに要約すると、プログラミングのモナドというのは、単にプログラムが合成できて矛盾もしませんよ、ということを難しく表現している機構でしかないと思うので。圏論的な図式とか全然関係ない。
記事を誤解していたら申し訳ないです。