こんにちは,エムスリーエンジニアリングGの榎田(@niflh)です.最近は SEKIRO をトロコンしたり,確率論の勉強をしたりの日々です.
この記事は前半に引き続く,関数型言語で登場するモナドに関する記事の後半です.前半は数学成分控えめにモナドの解説を試みました.
後半では数学のことばをあまり控えずに著します.モナドを勉強するとあらわれる圏論のことば(のうち,「関手(かんしゅ)」と「自然変換」)について,プログラムに即した理解を試みるのが本稿後半の目的です.或いは,「関数型を勉強するとたまに圏論のことばが出てくるけれども,あのことばたちはプログラムの何に対応するの?」という疑問に(私に可能な範囲で)答える,と言ってもいいでしょう.
本稿は,「圏論のことばに置き換えて新しい知見を得る」ところまでは届きません.それでも,「関数型を勉強していたら突然圏論が出てきて困った」という状況に対するひとつの処方箋として,本稿の役立つところがあるのではないかと信じて書きます.
前半の復習
前半では,Monad
が value のみならず computation を扱うための概念であること,および以下のパーツから構成できることを述べました.
- 「どんな計算 computation がしたいのか」を示す型
T
- value to value の関数を computation to computation の関数に変換する
map
T
で包んで value から computation への橋渡しを担うpure
T
の入れ子を剥ぐflatten
それぞれのメソッドの型を明示するため,擬似コードを置いておきます.map
の引数の順番を前半とはわざと入れ替えています.
trait Monad[T[_]] { def map[A, B](f: A => B)(ta: T[A]): T[B] def pure[A](a: A): T[A] def flatten[A](tta: T[T[A]]): T[A] }
以下では,T
およびmap
が併せて「関手(かんしゅ)」と呼ばれる概念であること,pure
およびflatten
が「自然変換」と呼ばれる概念であることを見ていきます.
型クラス Functor と関手
本稿ではあまりmap
の性質について議論してきませんでしたが,map
は以下の性質を充たすことが普通です.
- 「なにもしない」関数を
map
すると,引数の型は変わるが,実態は「なにもしない」関数のまま.A
上の恒等関数をmap
するとT[A]
上の恒等関数にうつる,と言っても良い.map(a => a) == (ta => ta)
ということ.- 本稿前半の
map
の引数の順番に即して書くと,(ta => map(ta)(a => a)) == (ta => ta)
です *1 .
map
してから合成しても,合成してからmap
しても結果は同じ.ta: T[A]
,f: A => B
,g: B => C
に対して,map(a => g(f(a)))(ta) == map(g)(map(f)(ta))
ということ.
このようなmap
をもつ型クラスは,Cats においてFunctor
と呼ばれます.functor は「関手(かんしゅ)」と訳される,圏論由来の概念です…というより,このmap
は(T
と併せて)関手そのものです.それを見るために,上の性質を書き換えていきます.
上に挙げたmap
は関数を変換するものですが,その実装は型T
ごとに異なります.それを明示するために,型T
のmap
を と書いてみることにします.つまり,関数 に対して, をmap
で変換した結果をと書く,ということです*2.更に色々なところを書き換えてみましょう.
- 上の恒等関数 を で変換すると 上の恒等関数 にうつる.
- ということ.
- してから合成しても,合成してから しても結果は同じ.
- および に対して, ということ.
ここでやや唐突ですが,(自己共変)関手の定義を述べます.定義していない用語が多数出てきますが,定義を理解してもらう必要はないです.文字面がなんとなく似ていることをお見せするのが目的です.
定義. を圏とする *3 .「関数」 が 上の関手 functor (正確には自己共変関手 covariant endofunctor) であるとは,次の条件を全て充たすことをいう.
- は の対象または射を入力として受け付ける.
- は の対象 を入力すると の対象 を返す.
- は の射 を入力すると の射 を返す.
- 上の恒等射 を で変換すると 上の恒等射 にうつる.すなわち ということ.
- どのような射 および に対しても, が成り立つ.
細かな点( と で記号を分けているか, と書くか と書くか,等)を見逃せば,同じことを述べていることがわかります.特に,map
は型T
と併せて関手の定義を充たします.これが,Functor
型クラスがその名を冠する所以です.
型構造の抽象としての圏
上に挙げたmap
の性質と関手の定義をより丁寧に突き合わせていくと,次のような対応たちが見いだせます.
- 「圏 の対象 」は,型
A
と対応している. - 「圏 の射 」は,(純粋)関数
f: A => B
と対応している. - とくに,「 上の恒等射 」は,
A
上の恒等関数a => a
と対応している. f: A => B
とg: B => C
の合成関数a => g(f(a))
があるように,「射の合成 」というものもある.
正確な定義は述べませんが,上に挙げた「対象と射からなる,恒等射や合成という概念も持つなにか」を圏と呼びます.プログラミングで現れる型および関数の構造は,その具体例の一つとして捉えることができそうです*4.或いは,このような型や関数の構造の抽象化のひとつが圏であると言っても良いでしょう*5.
flatten と自然変換
前半で,T
の入れ子を剥ぐメソッドとしてflatten
を紹介しました.機能としてはこれ以上紹介することはないのですが,このメソッドは「自然変換」として捉える見方もできます.
圏論の話と関係なしに,flatten
には期待したい性質があります.それは,「関数f
があるとき,(必要ならmap
でf
の型を合わせる前提で)f
してからflatten
しても,flatten
してからf
しても結果が変わらない」ことです.入れ子を剥がすタイミングを間違えると結果が変わるのでは使いづらいですから.
この要請を正確に述べます.これ以降は,map
が関手であることを意識して,型T
のことも,それに付随するmap
のことも と書きます. のことを と書きます. を関数とし,それを1回map
したものを と,2回map
したものを と書くことにします . という,「型に対しては二重でT
で包み,関数に対しては二重でmap
する」別の関手を考えていると言っても良いです.これらの記号の準備の下,先程の要請は次のような図で表すことができます.
ただし, は flatten[A]
を, は flatten[B]
をそれぞれ示します *6 .図式において「下に行ってから右に行く」のが「f
してからflatten
」「右に行って下に行く」のが「flatten
してからf
」にそれぞれ対応します.この「結果が図の中での道順に依らない」ことを,数学のことばで「図式が可換である」と言います.
再び唐突に,圏論における自然変換の定義を出します.
定義. を圏とし, および を 上の(自己共変)関手とする.「関数」 は,以下の条件を全て充たすときに から への自然変換と言われる.
- は入力として の対象を受け付ける.
- の対象 を入力したときの返り値は, から への射である.この射を と書く.
- 返り値である射たちは次の性質を充たす;どのような射 に対しても,次の図式が可換となる.
が から への自然変換であることを, のように書く.
やはり細かな点を除けば,同じことを書いていることがわかると思います.すなわちflatten
とは,関手 から関手 への自然変換にほかなりません.
全く同様にして,pure
は「恒等関手」 から関手 への自然変換 であると理解することができます.
後半まとめ:圏のことばでモナドを望む
ここまでで,モナドを構成するそれぞれの部品が,実は圏論に現れることばの具体例として位置づけられることを見てきました.或いは,プログラムに現れるモナドを数学のことばで抽象して圏論まで行き着いたとも言えます.
- モナドはひとつの関手 およびふたつの自然変換 からなる.
- 関手 は,型
T
およびそれに付随するmap
の圏論的な対応物である. - 自然変換 は,
flatten
の抽象化にあたる. - 自然変換 は,
pure
の抽象化である.
最初に述べたとおり,圏論の視点ゆえに得られる恩恵についてはここでは述べません*7.本稿では,様々な型とモナドとの関係,およびモナドに関わる概念たちと圏論のことばとの関係を述べることで,イメージが湧きにくい圏論のことばを,具体的なモナド,ないしは型に根ざして理解するひとつの方向性を描こうとしました.この試みが,読者の皆さんにとって何かしらの一助となることを願いつつ,筆を置くことにします.
この先に見えそうなもの
以下に,まだ勉強していないがぼんやり疑問に思っていることを書き並べておきます.なにか面白い話を見つけたら,どこかで記事を書くかもしれません.この記事を読んだ方でなにかご存じの方はぜひ教えて下さい.
pure
とflatten
は随伴対か?- モナド変換子は自然変換として捉えられるか?
- プログラムと圏論の関係でよく顔を出すのはデカルト閉圏であるが,本稿で扱っていない冪対象の役割は何か?
- 型と関数の圏に対して米田埋め込みをすると何が出てくるのか?
- Curry-Howard-Lambek 対応を踏まえ,型理論に有益な知見が出せるか?
We are hiring!
本稿のきっかけとなった Scala with Cats 読み会をはじめ,社内では複数の勉強会をしています.ご興味のある方はぜひ話を聞きに来てください.
*1:が,わかりづらいと思います.なので引数の順番を入れ替えています.
*2:ここから LaTeX 記法を使い始めていますが,単に記法を変えただけで,同じことを書き続けています.
*3:技術的な注:圏の大きさについては議論しません.本稿では集合論的に扱いが難しい部分に踏み込むことは目的ではないし,また私にその能力はないからです.おそらく (locally) small を仮定すれば本稿の範囲では問題は吹き出さないと思うのですが.
*4:が,具体的な型システムが圏と見做せるかどうかについては注意深い考察が必要です.例えば Haskell の型システムは Bottom 型のせいで圏を成しません.
*5:歴史的にはそうではないです.圏論ははじめ代数トポロジーにおいて発展し,それが後になって計算機科学への応用を見いだされたと聞いています.
*6:わざわざ記号を変えているのは,LaTeX の図式で “flatten” と書きたくないから,というのと,直後に出す自然変換の定義に合わせてのことです.
*7:実際のところ「これは誰もが納得する明確な恩恵だ」という具体例をいまだ知らないのが悲しいところで,ぜひ知りたいと思っています.しかしそれでも,圏論を補助輪にしてプログラミングの理解を深める窓口が開いているというだけで,私にとって間違いなく役に立っています.