序文

しばらく前から、プログラマーを対象とした圏論に関する本を書こうと考えていた。計算機科学者ではなくプログラマー、科学者ではなくエンジニア向けだということに注目してほしい。正気の沙汰ではないし、本当に恐ろしい。科学と工学の間に大きなギャップがあるのは否定できないと思う。自分自身がその分断の両側で仕事をしてきたからだ。それでも、物事を説明したいという強い衝動をいつも感じていた。簡潔な説明の達人だったリチャード・ファインマン1を心から尊敬している。自分がファインマンではないことは分かっているが、最善を尽くしたい。まずは、この序文――読者に圏論を学ぶ気を起こさせることを想定したもの――を公開することから始めようと思う。それによって議論を開始しフィードバックを募れることを願っている2

ここからの数段落をかけて、この本はあなたのために書かれたものであり、数学のうちでも特に抽象的な分野を学ぶために「あり余る自由時間」を費やすことへのどんな反対意見も全く根も葉もないことを確信してもらえるように試みたい。

私の楽観論はいくつかの観察に基づいている。第一に、圏論は極めて有用なプログラミングのアイデアの宝庫だ。Haskellプログラマーたちは長い間この資源を利用していて、得られたアイデアは他の言語にゆっくりと浸透してきているが、進行が遅すぎる。もっとスピードを上げる必要がある。

第二に、いろいろな種類の数学があり、それぞれ興味を惹く人も異なる。微積分や代数にアレルギーがあったとしても、圏論を楽しめないとは限らない。私としては、圏論はプログラマーのマインドに特に合った数学であるとさえ主張したい。圏論では、個々の物事を扱うのではなく、構造を扱うからだ。圏論はプログラムを合成可能にするような構造を扱う。

合成は圏論の最も根本であり、圏そのものの定義の一部だ。そして私は、合成こそプログラミングの本質であると強く主張したい。我々は、偉大なエンジニアがサブルーチンのアイデアを思いつく遥か前から、ずっとものを合成してきた。かつて構造化プログラミングはプログラミングに革命をもたらした。コードのブロックを合成可能にしたからだ。続いてオブジェクト指向プログラミングが登場した。これはオブジェクトを合成することこそすべてだ。関数プログラミングは、関数や代数的データ構造を合成するだけでなく、並行性をも合成可能にする。これは他のプログラミングパラダイムでは事実上不可能だ。

第三に、私には秘密兵器の「肉切り包丁」がある。それを使って数学を捌いて、よりプログラマーの口に合うものにするつもりだ。プロの数学者としては、すべての仮定を確かめ、すべての命題を適切に述べ、すべての証明を厳密に構成するために細心の注意を払う必要がある。そのせいで数学の論文や書籍は門外漢には非常に読みにくくなっている。だが、私は物理学者としての教育を受けており、物理学は形式的でない推論を用いて驚くべき進歩を遂げた学問だ。かつて数学者たちはディラックのデルタ関数を嗤った。デルタ関数は偉大な物理学者であるP.A.M. ディラックが微分方程式を解く過程で作ったものだ。数学者たちは、ディラックの洞察を形式化した微積分学の全く新しい分野として超関数理論 (distribuiton theory) を発見したとき、嗤うのをやめた。

当然ながら、身振り手振りで議論するときには明らかに間違ったことを言ってしまう危険があるので、本書では非公式な議論の背景にしっかりした数学的理論があるように気を付けたい。私のベッドサイドには、読み古したソーンダーズ・マックレーンの『圏論の基礎』が置かれている。

この本はプログラマーのための圏論なので、すべての主要な概念を説明するのにコンピューターコードを使うことにしたい。お気付きかもしれないが、より普及している命令型言語よりも関数型言語の方が数学に近い。また、より強力な抽象化能力を提供する。そのため、自然な誘惑として「圏論の恩恵に浴するにはHaskellを学ばなければならない」と言いたくなるかもしれない。しかし、それは圏論が関数プログラミング以外の用途を持たないことを意味しており、全く誤っている。そこで、C++での例を数多く載せようと思う。確かに、いくつかの醜い構文を克服しなければならず、雑然とした背景に紛れてパターンが目立たなくなったり、高度な抽象化の代わりにコピー&ペーストに頼らなければならない場面もあるだろう。だが、それがC++プログラマーというものだ。

しかし、Haskellに関しては逃れられない。Haskellプログラマーになる必要はないが、C++で実装しようとしているアイデアをスケッチしたり文書化したりするための言語としてHaskellが必要になる。私もそうやってHaskellを始めた。そして、簡潔な構文と強力な型システムが、C++のテンプレート、データ構造、アルゴリズムを理解し実装する上で大きな助けになると気付いた。もっとも、読者がすでにHaskellを知っているとは期待できないので、ゆっくり紹介しながら進行に応じてすべて説明していくつもりだ。

プログラマーとしての経験が豊富なら、次のように自問するかもしれない:長い間ずっと圏論や関数型の手法を気にすることなくコーディングしてきたが、何が変わったのだろう? 確かにそう思わずにいられないだろうが、関数型の新機能が続々と登場し、命令型言語に侵入していることに気付いてほしい。オブジェクト指向プログラミングの牙城であるJavaでさえ、ラムダを導入した。最近のC++は数年ごとの新しい標準という激動のペースで進化し、変化する世界に追いつこうとしている。これらはすべて、破壊的な変化、すなわち物理学者である我々が相転移と呼ぶものに備えるための動きだ。お湯を温め続けると、やがて沸騰しはじめる。我々はいま、その中のカエルの立場にいる。カエルはどんどん熱くなるお湯の中で泳ぎ続けるべきなのか、それとも何か別のものを探し始めるべきなのか、決めなければならない。

大きな変化を引き起こしている力のひとつがマルチコア革命だ。広く普及しているプログラミングパラダイムであるオブジェクト指向プログラミングは、並行・並列処理の領域では何のメリットもなく、その代わりに危険でバグを生じやすい設計を奨励している。オブジェクト指向の基本的前提であるデータ隠蔽は、データの共有や改変と組み合わされると、データ競合のレシピになる。ミューテックス3 とそれが保護するデータを組み合わせるというアイデアは素晴らしい。しかし、残念ながらロックは合成できないし、ロックを隠すことでデッドロックが発生しやすくなり、デバッグが難しくなる。

さらに、並行性が存在しないとしても、ソフトウェアシステムの複雑さが増すにつれて、命令型パラダイムのスケーラビリティは限界が試されている。簡単に言うと、副作用が手に負えなくなってきている。確かに、副作用のある関数は便利だし簡単に書ける。それらの作用は、原理的には、名前やコメントに示しておける。SetPasswordやWriteFileなどと命名された関数は、明らかに何らかの状態を変化させ副作用を発生させるが、我々はそれに対処するのには慣れている。副作用のある関数に副作用のある別の関数を合成し始めたときに初めて、物事は複雑になり始める。副作用が本質的に悪いわけではなく、隠れて見えないせいで大規模な管理が不可能になっているのだ。副作用はスケールせず、そして命令型プログラミングでは副作用こそすべてだ。

ハードウェアの変化とソフトウェアの複雑さが増すことで、プログラミングの基礎を再考する必要に迫られている。ヨーロッパの偉大なゴシック大聖堂の建設者と同じように、我々は材料と構造の限界まで技術を磨き続けてきた。フランスには未完成のゴシック建築のボーヴェ大聖堂4 があり、限界との深く人間的なこの闘いの証拠となっている。それまでの高さと軽さの記録をすべて破ることを目論んでいたが、相次ぐ崩壊に見舞われた。鉄筋や木製の支柱などの応急処置で崩壊を防いでいるが、明らかに多くのことがうまくいかなかった。現代の視点から見ると、材料科学、コンピューターモデリング、有限要素解析、そして汎用的な数学と物理学の助けなしに、これほど多くのゴシック構造が成功裏に完成したことは奇跡だ。将来の世代が、複雑なオペレーティングシステム、Webサーバー、インターネット基盤を構築する際に我々が示してきたプログラミングのスキルを賞賛するようになるのを願っている。率直に言って、彼らはそうすべきだ。我々はそれらすべてを非常に貧弱な理論的基盤に基づいて行ってきたのだから。前進するためには、これらの基盤を修復しなければならない。

ボーヴェ大聖堂の崩壊を阻止するための応急処置

1 圏:合成の本質

圏は、戸惑ってしまうほど単純な概念だ。圏 (category) は複数の対象 (object) とそれらをつなぐ (arrow, morphism) で構成される5。そのため、圏は図で簡単に表せる。対象は円または点として、射は矢印として描ける。(変化を付けるために、私は対象を子豚、射をロケット花火として描くことがある。) しかし、圏の本質は合成 (composition) にある。あるいは、お好みなら、合成の本質は圏だと言っても構わない。射は合成できるので、対象AAから対象BBへの射があって、さらに対象BBから対象CCへの別の射があるなら、それらを合成したAAからCCへの射が必ずある。

圏では、AからBへの射とBからCへの射があるなら、それらを合成したAからCへ直接向かう射が必ずある。この図には恒等射(後述)がないため、完全な圏ではない。

1.1 関数としての射

すでに抽象的ナンセンスでいっぱいだろうか6? 絶望しないでほしい。具体的な話をしよう。射を関数として考えよう。関数ffが型AAの引数を取ってBBを返すとする。また、別の関数ggBBを取ってCCを返すとする。ffの結果をggに渡せばそれらを合成できる。つまりAAを取ってCCを返す新たな関数を定義したことになる。

数学では、このような合成を関数同士の間に小さな丸を書いてgfg \circ fのように表す。合成の順序が右から左であることに注意してほしい。これが紛らわしいと感じる人もいるだろう。読者の中には、Unixのパイプ記法:

lsof | grep Chrome

や、F#の前方合成演算子>>を見慣れている人もいるかもしれない。どちらも左から右の向きだ。ところが、数学やHaskellの関数は右から左に合成する7gfg \circ fを「ggffの後に(“g after f”)」合成する、と読めば理解しやすくなる。

もっとはっきりさせるために、C言語のコードを少々書こう。型Aの引数を取って型Bを返す関数f

B f(A a);

と、別の関数:

C g(B b);

の合成は次のとおりだ:

C g_after_f(A a)
{
    return g(f(a));
}

ここで再び、右から左への合成g(f(a))が、今回はC言語で現れた。

C++の標準ライブラリには2つの関数を取って合成関数を返すテンプレートがある、と言えたら良かったのだが、そんなものはない。そこで、気分を変えるためにHaskellを少し試してみよう。ここにAからBへの関数の宣言がある:

f :: A -> B

同様に:

g :: B -> C

これらの合成は次のとおりだ:

g . f

Haskellの簡潔さを知ると、C++で単純明快な関数の概念を表現できないのには少し当惑させられる。実際、HaskellではUnicode文字を使えるので、合成を次のようにも書ける:

g ∘ f

Unicodeの二重コロンと矢印さえ使える8

f ∷ AB

ここで、第1回目のHaskellのレッスンだ:二重コロンは「……という型を持つ」を意味する。関数型 (function type) は2つの型の間に矢印を挿入することで作成される。2つの関数を合成するには、間にピリオド(あるいはUnicodeの丸)を置く。

1.2 合成の性質

どんな圏においても合成が満たすべき非常に重要な性質が2つある。

  1. 合成は結合的 (associative) である。3つの射ff, gg, hhがあり、それらが合成できる(つまり端同士の対象が一致している)なら、合成するときに括弧は要らない。このことは数学の記法では次のように表される: h(gf)=(hg)f=hgfh \circ (g \circ f) = (h \circ g) \circ f = h \circ g \circ f (擬似的な)Haskellでは次のように書ける:

    f :: A -> B
    g :: B -> C
    h :: C -> D
    h . (g . f) == (h . g) . f == h . g . f

    (ここで「擬似的」と呼んだのは、関数に等しさが定義されていないからだ。)

    関数を扱うなら結合性は全く自明だが、その他の圏では自明ではないこともある。

  2. どんな対象AAにも、合成の単位元 (unit) となる射が1つずつ存在する。その射は対象から対象自身へとループを描く。合成の単位元となるというのは、AAから始まるかAAで終わるどんな射と合成しても、もとと同じ射になるという意味だ。対象Aについて単位元となる射は𝐢𝐝A% \mathbf{id}_{A}% AA上の恒等射、identity)と呼ばれる。数学の表記法では、ffAAからBBへ向かうなら

    f𝐢𝐝A=ff \circ % \mathbf{id}_{A}% = f かつ 𝐢𝐝Bf=f% \mathbf{id}_{B}% \circ f = f となる。

関数を扱うとき、恒等射は引数をそのまま返す恒等関数として実装される。この実装はどの型でも同じであり、この関数は普遍的に多相 (universally polymorphic) であることを意味する。これはC++ではテンプレートとして定義できる:

template<class T> T id(T x) { return x; }

もちろん、C++ではそれほど単純ではない。何を渡すかだけでなく、どのように渡すか(値渡し・参照渡し・const参照渡し・ムーブなど)も考慮する必要があるからだ。

Haskellの恒等関数は、(Preludeと呼ばれる)標準ライブラリの一部だ。宣言と定義は以下のとおりだ:

id :: a -> a
id x = x

ご覧のとおり、Haskellの多相関数は朝飯前だ。宣言において、型を型変数に置き換えるだけでよい。トリックは次のとおりだ:具体的な型の名前は常に大文字で始まり、型変数の名前は小文字で始まる。ここでaはすべての型を表している。

Haskellの関数定義は、関数の名前とそれに続く仮引数 (formal parameter) ――ここではxただひとつ――で構成される。関数の本体は等号の後に続く。この簡潔さは、多くの初心者には衝撃的だが、すぐに完全に理にかなっていることが分かるだろう。関数定義と関数呼び出しは関数プログラミングの必需品なので、構文は最小限に抑えられている。引数リストを括弧で囲まないだけでなく、引数間のコンマさえない(これについては後ほど複数の引数の関数を定義するときに説明する)。

関数の本体は常に式 (expression) であり、関数内に文 (statement) はない。関数の結果はその式だ――ここでは単にxだ。

これでHaskellのレッスンの第2回は終了だ。

恒等条件は、(再び疑似Haskellで)次のように書ける。

f . id == f
id . f == f

誰が恒等関数――何もしない関数――をわざわざ気にするのか、と疑問に思うかもしれない。では、なぜ0という数をわざわざ気にするのだろうか? 0は無の象徴だ。古代ローマ人は0のない数値体系を使っていたが、優れた道路や水路を建設でき、その一部は今日まで残っている。

0や𝐢𝐝\mathbf{id}のような中立の値は、記号変数を扱うときに非常に便利だ。これこそが、ローマ人は代数があまり得意ではなく、0の概念に精通していたアラビア人やペルシア人は得意だった理由だ。そのため、恒等関数は、高階関数 (higher-order function) の引数あるいは戻り値として非常に便利になる。高階関数は関数の記号的操作を可能にする。それらは関数の代数だ。

要約すると、圏は対象と射で構成されている。射は合成でき、その合成は結合性を持つ。すべての対象には、合成の単位元として機能する恒等射がある。

1.3 合成はプログラミングの本質

関数プログラマーは、問題に独特の方法でアプローチする。彼らはまるで禅のような問いから始める。たとえば、対話型プログラムを設計するときは「対話とは何か?」と問うだろう。コンウェイのライフゲームを実行するときには、生命の意味について思索するだろう。そのような精神で「プログラミングとは何か?」と問いかけたい。最も基本的なレベルでは、プログラミングとはコンピューターに何をすべきかを指示することだ。「メモリーアドレスxの内容を取り、レジスタEAXの内容に加えよ」のように。しかし、アセンブリー言語でプログラムを作成する場合でも、コンピューターに与える命令はもっと意味のあるものを表現している。解こうとしているのは自明な問題ではないのだ(自明ならコンピューターの助けは不要だろう)。どうすれば問題を解けるだろうか? 大きな問題を小さな問題に分解すればよい。小さくした問題がまだ大きすぎる場合は、それらをさらに分解する。最後に、小さな問題すべてについて、解決するコードを書く。そうしてプログラミングの本質が現れる。すなわち、それらのコードを合成し、より大きな問題に対する解決策を創造する。分解は、断片をもとの状態に戻せなければ意味がない。

この階層的な分解と再合成のプロセスは、コンピューターによって強制されているわけではない。それは人間の精神の限界を反映しているのだ。脳は一度に少しの概念しか扱えない。心理学で最も引用された論文の1つ、The Magical Number Seven, Plus or Minus Two9は、我々は7±27 \pm 2の「チャンク」の情報しか保持できないと仮定した。人間の短期記憶に関する我々の理解の詳細は変化しているかもしれないが、限界があるのは確実に分かっている。要するに、我々はオブジェクトのスープやコードのスパゲッティを扱えないということだ。構造が必要なのは、よく構造化されたプログラムが見やすいからではなく、そうでなければ脳が効率的に処理できないからだ。あるコード断片について、エレガントだ、あるいは美しいと形容することがよくある。だが、本当に意味しているのは、人間の限界ある精神で処理するのが簡単だということだ。エレガントなコードは、ちょうど適切なサイズのチャンクを作成し、精神の消化器系がそれらを消化するのにちょうど適切な数だけ生成する。

では、プログラムの合成にとって適切なチャンクとは何だろうか。チャンクの表面積は体積よりも必ずゆっくりと増加する。(私がこのたとえを気に入っているのは、幾何学的な対象の表面積はその大きさの2乗に比例して増加する――体積が大きさの3乗に比例して増加するのよりも遅い――という直観による。)表面積は、チャンクを合成するために必要な情報だ。体積は、それらを実装するために必要な情報だ。そのこころは、ひとたびチャンクが実装されると、その実装の詳細を忘れて他のチャンクとの相互作用に集中できる、ということだ。オブジェクト指向プログラミングでは、表面はオブジェクトのクラス宣言、あるいはその抽象インターフェースだ。関数プログラミングでは、それは関数の宣言だ10。(ここでは少し単純化しているが、要点はこれだ。)

圏論は、対象の中を見ることを積極的に思いとどまらせるという意味で極端だ。圏論における対象は抽象的で漠然とした存在だ。対象について知り得るのは、他の対象たちとどのように関連しているか、つまり、どのように射で接続しているかだけだ。これは、インターネット検索エンジンが流入リンクと流出リンクを分析してウェブサイトを順位付けするやり方だ(不正行為がある場合は除く)。オブジェクト指向プログラミングでは、理想化されたオブジェクトを見られるのは抽象インターフェース(純粋な表面なので体積なし)を通してだけで、メソッドが射の役割を果たす。他のオブジェクトと合成する方法を理解するためにオブジェクトの実装を掘り下げなければならなくなった瞬間、このプログラミングパラダイムの利点は失われてしまう。

1.4 課題

  1. 恒等関数を、好きな言語で(それがたまたまHaskellなら2番目に好きな言語で)できるだけうまく実装せよ。

  2. 合成関数を好きな言語で実装せよ。このメソッドは2つの関数を引数として受け取り、その合成である関数を返す。

  3. 合成関数が恒等関数と整合しているかテストするプログラムを作成せよ。

  4. ワールドワイドウェブは、何らかの意味で圏だろうか? リンクは射だろうか?

  5. Facebookは人を対象とし友達関係を射とする圏だろうか?

  6. 有向グラフが圏になるのはどのような場合だろうか?

2 型と関数

型と関数の圏はプログラミングにおいて重要な役割を果たす。そこで、型とは何か、なぜ型が必要なのかについて説明しよう。

2.1 型を必要とするのは誰か?

静的型付けと動的型付け、および強い型付けと弱い型付けの利点については、議論があるようだ。これらの選択肢を思考実験で説明しよう。コンピューターのキーボードを操作する何百万匹もの猿が喜んでランダムにキーを打ち、プログラムを作成したり、コンパイルしたり、実行したりする様子を想像してみてほしい。

機械語では、猿が生成するバイトの組み合わせはどれでも受け入れられて実行される。しかし、より高級な言語ではコンパイラーが語彙や文法上の誤りを検出できるという事実を我々は理解している。多くの猿はバナナなしで去るだろうが、残されたプログラムは役に立つ可能性が高いだろう。型検査も、無意味なプログラムに対するもう1つの防御壁となる。さらに、型の不一致は動的型付け言語では実行時に発見されるのに対し、強く型付けされ静的に型検査される言語ではコンパイル時に発見されるので、多くの不正なプログラムが実行される機会を得る前に排除される。

そこで、問題は次のようになる。猿を幸せにしたいのか、それとも正しいプログラムを作りたいのか?

タイピング猿の思考実験における通常の目標はシェークスピア全集を作ることだ。スペルチェッカーと文法チェッカーをループに含めれば、勝算は大幅に上昇するだろう。型検査器に類するものを含めれば、さらなる前進が見込める。ロミオは人間である、と宣言されていれば、彼は決して葉を発芽したり自身の強力な重力場に光子を閉じ込めたりはしない。

2.2 型は合成に関する

圏論は射を合成することに関する。しかし、2本の射なら何でも合成できるわけではない。ある射の終点 (target) となる対象は、次の射の始点 (source) となる対象と同じでなくてはならない。プログラミングでは、ある関数の結果を別の関数に渡す。後段の関数が前段の関数によって生成されたデータを正しく解釈できない場合、プログラムは機能しない。合成が機能するためには両端が適合しなければならない。言語の型システムが強力であればあるほど、この一致はよりよく記述され、機械的に検証される。

強力な静的型検査に対して私が耳にする唯一の重要な反対意見は、意味的に正しいプログラムを排除する可能性がある、というものだ。実際には、そうなることは極めてまれで、いずれにしても、どの言語にも、本当に必要な場合に型システムを迂回するための何らかのバックドアが用意されている。HaskellにさえunsafeCoerceがある。しかし、このような装備は思慮深く使うべきだ。フランツ・カフカの小説の主人公グレゴール・ザムザが、巨大なバグに変身したとき型システムを破壊し、どんな結末を迎えたかは誰もが知っている。

私がよく耳にするもう1つの意見は、型を扱うのはプログラマーにとって負担が大きすぎる、というものだ。私もC++でイテレーターの宣言をいくつか自分で書かなければならなかったら共感するだろう。もっとも、型推論 (type inference) と呼ばれる技術があり、コンパイラーはほとんどの型を文脈から推論できるようになっている。C++では、変数をautoで宣言してコンパイラーにその型を発見させられるようになった。

Haskellでは、稀な場合を除いて、型注釈は純粋にオプションだ。プログラマーはどのみち型注釈を使う傾向がある。なぜなら、コードの意味について多くを伝えられ、コンパイルエラーを理解しやすくできるからだ。Haskellでは、型を設計することからプロジェクトを始めるのが一般的な慣習だ。後々、型注釈は実装を駆動し、コンパイラーによって強制されるコメントになる。

強力な静的型付けはコードをテストしない言い訳としてよく使われる。Haskellのプログラマーが「コンパイルが通るなら正しいはずだ」と言っているのを耳にすることがあるだろう。しかし、型が正しいプログラムなら正しい出力を生成する、などという保証は当然ない。そのような無頓着な態度の結果、いくつかの研究では、Haskellのコード品質は予想ほど群を抜いて高くはなかった11。商用の環境では、バグを修正する圧力はある品質レベルまでしか働かないようだ。そのレベルは、ソフトウェア開発の経済的状況とエンドユーザーの許容度に深く関係し、プログラミング言語や方法論にはほとんど関係しないのだろう。より良い基準は、スケジュールより遅れているプロジェクトや、大幅に機能が削減されたプロジェクトの数を調べることだろう。

単体テストによって強い型付けを置き換えられる、という意見に関しては、強く型付けされた言語で一般的に行われているリファクタリング手法として、関数の引数の型の変更について考えてみてほしい。強く型付けされた言語では、その関数の宣言を変更してから、すべてのビルドブレークを修正すれば十分だ。弱く型付けされた言語では、関数が異なるデータを要求するようになったという事実は呼び出し側に伝わらない。単体テストはミスマッチのいくつかを捉えるかもしれないが、テストはほとんどすべての場合において、確率論的なプロセスにすぎず、決定論的なプロセスではない。テストは証明の代わりにはならないのだ。

2.3 型とは何か?

型とは、最も単純な直観としては、値の集合だ。型BoolTrueFalseの二元集合だ(Haskellでは具体的な型は大文字で始まることを思い出してほしい)。Char型はaąのようなUnicode文字すべてからなる集合だ。

集合は有限の場合も無限の場合もあり得る。String型は、Charのリストの同義語で、無限集合の例だ。

以下のようにxIntegerとして宣言すること:

x :: Integer

は、xが整数の集合の要素だと言っていることになる。HaskellのIntegerは無限集合であり、任意精度の演算が可能だ。また、C++のintと同様の、マシンネイティブの型に対応する有限集合Intもある。

いくつか微妙な点があるせいで、こうした型と集合の同一視はトリッキーなものになっている。多相関数には循環定義の問題があり、すべての集合の集合が存在しないことも問題だ。だが、約束したとおり、私は数学にこだわるつもりはない。ありがたいことに、集合の圏が存在する。𝐒𝐞𝐭\mathbf{Set}と呼ばれるその圏をここでは扱う。𝐒𝐞𝐭\mathbf{Set}では、対象は集合であり、射は関数だ。

𝐒𝐞𝐭\mathbf{Set}は非常に特別な圏だ。対象の内部を実際に見られ、そうすることで多くの直観が得られるからだ。たとえば、空集合には要素がないと分かっている。単元集合という特別な集合たちが存在するのも分かっている。関数が1つの集合の要素を別の集合の要素に写すのも分かっている。関数は、2つの要素を1つに写すことはできるが、1つの要素を2つに写すことはできない。恒等関数が集合の各要素を自身に写すことなども分かっている。予定としては、これらすべての情報を徐々に忘れ、すべての概念を純粋に圏論の言葉、つまり対象と射によって表していく。

理想的な世界では、Haskellの型は集合であり、Haskellの関数は集合間の数学的関数であると言えば済んだだろう。だが、1つだけ小さな問題がある。数学関数はコードを実行せず、単に解を知っているだけなのだ。Haskellの関数は解を計算する必要がある。有限のステップ数で解が得られるなら、何ステップかかっても問題はない。ところが、計算のなかには再帰を伴うものもあり、ずっと停止しないことがあり得る。Haskellで停止しない関数をただ単に禁止はできない。なぜなら、停止する関数と停止しない関数の区別は決定不能だからだ。これは停止性問題 (halting problem) として有名だ。そのため計算機科学者たちは素晴らしいアイデアを考案した。それは捉え方によっては大きなハッキングとも言えるだろう。そのアイデアとは、ボトム (bottom) と呼ばれる、記号_|_またはUnicodeの\bot12表される特別な値を用いてすべての型を拡張する、というものだ。この「値」は停止しない計算に対応する。したがって、次のように宣言される関数:

f :: Bool -> Bool

TrueFalse_|_を返し、ボトムの場合は決して停止しないことを意味する。

興味深いことに、ひとたび型システムの一部としてボトムを受け入れたなら、すべての実行時エラーをボトムとして扱い、さらには関数からボトムを明示的に返せるようにするのが便利になる。後者は通常、undefinedという式を使って行われる:

f :: Bool -> Bool
f x = undefined

この定義が型検査を通るのは、undefinedが評価されるとボトムになるからだ。ボトムはBoolも含むすべての型のメンバーだ。さらに:

f :: Bool -> Bool
f = undefined

のように(xなしで)書くことさえできる。ボトムがBool -> Bool型のメンバーでもあるためだ。

取りうるすべての引数に対して有効な結果を返す関数が全域関数 (total function) と呼ばれるのに対し、ボトムを返す可能性のある関数は部分関数 (partial function) と呼ばれる。

ボトムがあるため、Haskellの型と関数の圏は、𝐒𝐞𝐭\mathbf{Set}ではなく𝐇𝐚𝐬𝐤\mathbf{Hask}と呼ばれる。理論的な観点から見ると、これは果てしない複雑さの原因となる。だから、この時点で一連の推論を肉切り包丁で捌いて終わらせよう。実用的な観点からは、停止しない関数とボトムを無視し、𝐇𝐚𝐬𝐤\mathbf{Hask}を正真正銘の𝐒𝐞𝐭\mathbf{Set}として扱うことは問題ない13

2.4 なぜ数学モデルが必要なのか?

プログラマーであるあなたは、自分が使っているプログラミング言語の構文と文法に精通している。言語のそれらの側面は、通常、言語仕様の冒頭で形式的な表記法によって記述される。一方で、言語の意味、すなわちセマンティクスを記述するのははるかに困難だ。より多くのページを必要とし、十分に形式的であることはほとんどなく、完全であることもほとんどない。それゆえ、言語法律家たちの間では終わりのない議論が交わされ、言語標準の細かい解釈を目的とした書籍が家内工業的に出版されている。

言語のセマンティクスを記述するための形式手法ツールは存在するが、複雑なため、ほとんどの場合は簡略化された学術言語で使われ、実用される巨大なプログラミング言語ではあまり使われない14。そのようなツールのうち操作的意味論 (operational semantics) と呼ばれるものは、プログラム実行の仕方を記述する。それは形式化され理想化されたインタープリターを定義する。C++のような産業用言語のセマンティクスは通常、操作的挙動に関する非形式的な議論により「抽象機械」として述べられることが多い。

問題は、操作的意味論を使ってプログラムに関することを証明するのが非常に難しいことだ。プログラムの性質を示すには、基本的には理想化されたインタープリターを通して「実行」しなければならない15

プログラマーが正しさを形式的に証明しないことは問題ではない。我々はいつも正しいプログラムを書いていると「思っている」。キーボードの前に座って、「さて、コードを数行打ち込んで、何が起こるか見てみよう」と言う人はいない。我々は、作成するコードが望ましい結果を生み出す特定のアクションを実行すると考えている。そうならない場合、たいていかなり驚くことになる。つまり、我々は自分が書いたプログラムについて推論していて、通常は頭の中でインタープリターを走らせることでそうしている。すべての変数を追跡するのは極めて難しい。コンピューターはプログラムを実行するのが得意だが、人間は不得意だ! もし得意だったら、コンピューターは必要ないだろう。

しかし、別の選択肢もある。それは表示的意味論 (denotational semantics) と呼ばれ、数学に基づいている。表示的意味論では、すべてのプログラム要素に数学的解釈が与えられる。それを使えば、プログラムの性質を証明したいときは数学的定理を証明するだけでよい16。定理を証明するのは難しいと思うかもしれないが、実際には、人類は数千年にわたって数学的手法を構築してきたので、利用できる知識が豊富に蓄積されている。また、プロの数学者が証明する定理と比べると、プログラミングで遭遇する問題は、自明ではないにせよ、通常は極めて単純なものだ。

表示的意味論ととても相性が良い言語であるHaskellで階乗関数の定義を考えてみよう:

fact n = product [1..n]

[1..n]は、1からnまでの整数のリストだ。関数productは、リストのすべての要素を乗算する。これは数学の教科書に載っている階乗の定義と同じだ。これをCと比較してほしい:

int fact(int n) {
    int i;
    int result = 1;
    for (i = 2; i <= n; ++i)
        result *= i;
    return result;
}

これ以上言う必要があるだろうか?

確かに、不当な批判だったのは真っ先に認めよう! そもそも階乗関数には自明な数学的解釈がある。鋭い読者なら「キーボードから文字を読み取ったり、ネットワークを介してパケットを送信したりするための数学的モデルは何か?」と尋ねるだろう。長きに渡って、それはかなり複雑な説明につながる面倒な質問だった。有用なプログラムを書くために不可欠な多くの重要なタスクには、表示的意味論は最適でないように思われたが、操作的意味論では容易に解決できた。突破口は圏論からもたらされた。エウジニオ・モッジ (Eugenio Moggi) によって、計算作用をモナドに写せることが発見された。これは表示的意味論に新たな生命を与え、純粋関数プログラムをより使いやすくするだけでなく、従来のプログラミングに新たな光を当てる重要な観察となった。モナドについては後ほど、より多くの圏論的な道具立てを説明するときに述べる。

プログラミングに数学的モデルがあることの重要な利点の1つは、ソフトウェアの正しさを形式的に証明できることだ17。消費者向けのソフトウェアを書く際にはそれほど重要でないように思えるだろうが、失敗の代償が法外なものになったり人命が危険にさらされたりするようなプログラミングの領域もある。もっとも、医療システム用のWebアプリケーションを作成する場合でさえ、Haskell標準ライブラリの関数やアルゴリズムが正しさの証明を伴うというアイデアに価値を見出すだろう。

2.5 純粋関数と非純粋関数

C++やその他の命令型言語で関数と呼ぶものは、数学者が関数と呼ぶものとは異なる。数学関数は値から値への写像にすぎない。

数学関数はプログラミング言語で実装できる。そのような関数は、入力値が与えられると、出力値を計算する。数の2乗を生成する関数は、入力値をそれ自身で乗算するはずだ。この関数は呼び出すたびに同じことを行い、同じ入力で呼び出されるたびに同じ出力を生成することが保証されている。数の2乗は月の満ち欠けによって変化しない。

また、数の2乗を計算することで犬においしい餌を出すという副作用があってはならない。それを行う「関数」は数学関数として簡単にモデル化できない。

プログラミング言語では、同じ入力に対して常に同じ結果を生成し副作用のない関数は純粋関数 (pure function) と呼ばれる。Haskellのような純粋関数型言語では、すべての関数が純粋だ18。そのため、これらの言語に表示的意味論を与え、圏論でモデル化することが容易になる。他の言語の場合は、純粋なサブセットだけを使うように制限したり、副作用を切り分けて扱ったりすることは常に可能だ。モナドによって、純粋関数のみを使ってあらゆる種類の作用をモデル化する方法については、後ほど説明する。数学的関数だけという制約を課しても何も失われないのだ。

2.6 型の例

型が集合であることを理解すれば、ややエキゾチックな型を考えられる。たとえば、空集合に対応するのはどんな型だろう? それは決してC++のvoidではないが、HaskellではVoidと呼ばれている。その型には値が存在しない。Voidを引数に取る関数は、定義はできるが呼び出せない。呼び出すにはVoid型の値を提供する必要があるが、それは存在しないからだ。この関数が返す内容に関しては、何ら制限はない。それは任意の型を返せる(ただし、呼び出せないため、返すことはない)。言い換えると、戻り値の型が多相な関数だ。Haskell使いたちはこう呼ぶ:

absurd :: Void -> a

aは任意の型を表せる型変数なのを覚えておいてほしい。)この名前は偶然ではない19。型と関数を論理の言葉でより深く解釈したカリー・ハワード同型と呼ばれるものが存在する。型Voidは矛盾を表し、関数absurdの型は「矛盾からは何でも導ける」というラテン語の格言 “ex falso sequitur quodlibet” に対応している20

次は、単元集合に対応する型だ。これが持てる値は1つしかない。その値は「唯一存在する」。すぐには分からないかもしれないが、これがC++のvoidだ。この型を引数に取る関数と、この型を返す関数を考えてみてほしい。voidを取る関数は常に呼び出せる。それが純粋関数なら、常に同じ結果を返す。そのような関数の例を示そう:

int f44() { return 44; }

この関数は引数に取るものが「何もない」のだと思うかもしれないが、先ほど見たように、「何もない」を取る関数ならば決して呼び出せない。「何もない」を表す値がないからだ。この関数は何を取るのだろうか? 概念的には、インスタンスが1つしか存在しないダミー値を取る。そのため、その値に明示的に言及する必要はない。しかしHaskellでは、その値を表す記号として、空の括弧のペア()がある。こうして、奇妙な偶然(これは偶然なのか?)によって、voidに対する関数の呼び出しはC++とHaskellで同じように見える。また、Haskellは簡潔さを好むので、同じシンボル()が型、コンストラクター、そして単元集合に対応する唯一の値に使われる。この関数をHaskellで書くとこうなる:

f44 :: () -> Integer
f44 () = 44

最初の行は、f44が “unit” と発音される()型をInteger型に写すことを宣言している。2番目の行はf44を、unitの唯一のコンストラクター()に対してパターンマッチングを行い、44という数を返すことによって定義している。この関数を呼び出すにはunitの値()を与える:

f44 ()

unitのどの関数も、結果の型から1つの要素を選択するのと等価であることに注意してほしい(ここではIntegerである44を選択する)。実際、f44は数44の別の表現と見なせる。これは、集合の要素への明示的な言及の代わりに関数(射)についての議論に置き換える方法の例だ。unitからどんな型AAへのどんな関数も、その集合AAの要素と1対1で対応している。

void型を返す関数や、Haskellでunit型を返す関数はどうだろうか? C++ではそのような関数が副作用を目的として使われるものの、数学的な意味での本当の関数ではないのは分かっている。unitを返す純粋関数は何もせず、引数を破棄する。

数学的には、集合AAから単元集合への関数はAAのすべての要素をその単元集合の単一の要素に写す。AAごとに、そのような関数が1つだけ存在する。Integerに対するこの関数は次のとおりだ:

fInt :: Integer -> ()
fInt x = ()

任意の整数を与えると、unitが返される。簡潔さの精神で、Haskellでは、破棄する引数をワイルドカードパターンであるアンダースコアで示せる。この方法なら引数に名前を付ける必要はない。よって、上記は次のように書き直せる:

fInt :: Integer -> ()
fInt _ = ()

この関数の実装は、渡された値に依存しないだけでなく、引数の型にも依存しないことに注目してほしい。

どの型に対しても同じ式で実装できる関数は、パラメトリック多相関数 (parametrically polymorphic function) と呼ばれる。そのような関数の族 (family) はすべて、具体的な型の代わりに型パラメーターを使う1つの等式によって実装できる。任意の型からunit型への多相関数を何と呼ぶべきだろう? もちろんunitと呼ぶ:

unit :: a -> ()
unit _ = ()

C++では、この関数を次のように記述する:

template<class T>
void unit(T) {}

型の類型学における次のものは二元集合だ。C++ではboolと呼ばれ、Haskellでは予想どおりBoolと呼ばれる。違いは、C++のboolは組み込みの型であるのに対して、Haskellでは次のように定義できることだ:

data Bool = True | False

(この定義の読み方はBool is True or Falseだ。) 原理的には、C++でもBoolean型を列挙型として定義できるはずだ:

enum bool {
    true,
    false
};

しかし、C++のenumは密かに整数だ21。C++11の “enum class” を代わりに使うこともできたが、その場合は、bool::truebool::falseのように、クラス名で値を修飾する必要がある。そして、言うまでもなく、それを使うすべてのファイルに適切なヘッダを含める必要がある。

Boolを取る純粋関数は、結果の型から2つの値を選択するだけだ。1つはTrueに対応し、もう1つはFalseに対応する。

Boolを返す関数は述語 (predicate) と呼ばれる。たとえば、HaskellライブラリData.CharisAlphaisDigitのような述語でいっぱいだ。C++にはisalphaisdigitなどを定義する同様のライブラリがあるが、これらはブール値ではなくintを返す。実際の述語はstd::ctypeで定義され、ctype::is(alpha, c)ctype::is(digit, c)などの形式がある。

2.7 課題

  1. 好きな言語で高階関数(または関数オブジェクト)memoizeを定義せよ。この関数は純粋関数fを引数として受け取り、次の点を除いてfと同じ動作をする関数を返す。すなわち、momoizeの結果として返される関数は、もとの関数を引数ごとに1回だけ呼び出し、結果を内部に格納し、その後は同じ引数で呼び出されるたびに格納済みの結果を返す。メモ化 (memoize) された関数ともとの関数は、パフォーマンスを見れば区別できる。たとえば、評価に時間のかかる関数のメモ化を試みること。最初に呼び出したときは結果を待つ必要があるが、同じ引数を使って次に呼び出したときは結果をすぐ得られるだろう。

  2. 乱数を生成するためにあなたが普段使う標準ライブラリ関数をメモ化してみよ。うまくいくか?

  3. ほとんどの乱数発生器はシードで初期化できる。シードを受け取り、そのシードで乱数発生器を呼び出し、結果を返す関数を実装せよ。その関数をメモ化せよ。うまくいくか?

  4. 以下のC++関数のうち、純粋なのはどれか? これらをメモ化してみて、何度も呼び出したときに何が起こるかを、メモ化した場合とそうでない場合について観察せよ。

    1. 本文中で例示した階乗関数。

    2. std::getchar()
    3. bool f() {
          std::cout << "Hello!" << std::endl;
          return true;
      }
    4. int f(int x) {
          static int y = 0;
          y += x;
          return y;
      }
  5. Boolを取りBoolを返す関数は何種類あるか? それらすべてを自分で実装できるか?

  6. Void型、() (unit) 型、Bool型だけを対象とする圏の絵を描け。ただし、射についてはこれらの型の間のすべての可能な関数に対応するようにせよ。射には関数名のラベルを付けよ。

3 圏のさまざま

様々な例を調べれば圏の真価が理解できる。圏にはさまざまな形やサイズがあり、予期しない場所によく現れる。ごくシンプルなものから始めよう。

3.1 対象がない場合

最も自明な圏は、対象が0個で、したがって射が0本のものだ。それ自体は非常に哀しい圏だが、他の圏との関連、たとえば、すべての圏の圏(そう、そういうものが存在する)において重要になるだろう。空集合に意味があると思うなら、空圏 (empty category) が無意味だとは思わないだろう?

3.2 有向グラフ

対象を射で接続するだけで圏を作成できる。任意の有向グラフから始めて、単に射を追加するだけで圏になるのは想像できるだろう。最初に、各ノードに恒等射を追加する。次に、一方の終点が他方の始点と一致するような2つの射(つまり、2つの合成可能な射)に対して、それらの合成として機能する新しい射を追加する。新しい射を追加するたびに、(恒等射を除く)他の射との合成も考慮する必要がある。たいていは射が無限に多くなるが、問題ない。

このプロセスを別の方法で見ると、グラフ内の各ノードを対象とし、合成可能なグラフの辺からなるすべてのチェイン22を射とする圏を作成していることになる。(恒等射はチェインの長さが0の特殊な場合とも見なせる。)

このような圏は、与えられたグラフによって生成される自由圏 (free category) と呼ばれる。これは自由構成 (free construction) の例であり、任意の構造を、その規則(ここでは圏の規則)を満たせる最少の項目で拡張して完成させるプロセスだ。今後さらに多くの例について見ていく。

3.3 順序

さて、全く別のものを見てみよう! 射が対象間の関係、具体的には小なりイコール(\leqslant)であるような圏だ。これが本当に圏かどうか調べてみよう。恒等射はあるだろうか? すべての対象はそれ自身以下だろうか:良し! 合成はあるだろうか? aba \leqslant bかつbcb \leqslant cならばaca \leqslant c:良し! 合成は結合的か? 良し! このような関係は前順序 (preorder) と呼ばれる。前順序は確かに圏だ。

aba \leqslant bかつbab \leqslant aならばaabbと等しくなければならないという追加の条件も満たす、より強い関係も考えられる。これを半順序 (partial order) と呼ぶ。

最後に、任意の2つの対象が\leqslantまたはその逆で関係しているという条件も課せる。そうすると、線形順序 (linear order) または全順序 (total order) と呼ばれる関係が得られる。

これらの関係を満たす順序集合を圏として特徴づけよう。前順序は、任意の対象aaから任意の対象bbに向かう射が高々1つ存在する圏となる。そのような圏は別名「細い圏」(thin category) と呼ばれる。前順序圏は細い圏だ。

𝐂\mathbf{C}における対象aaから対象bbへの射の集合はhom集合と呼ばれ23𝐂(a,b)\mathbf{C}(a, b) と書かれる(𝐇𝐨𝐦𝐂(a,b)\mathbf{Hom}_{\mathbf{C}}(a, b) とも書かれる)。したがって、前順序のhom集合はどれも空集合か単元集合になる。これはhom集合𝐂(a,a)\mathbf{C}(a, a)、つまりaaからaaへの射の集合にも当てはまる。その場合はどの前順序においても必ず単元集合になり、恒等射だけを含む。ただし、前順序では循環が起こりうる。半順序では循環は禁止されている。

整列(ソーティング)においては前順序・半順序・全順序を区別できることが非常に重要だ。クイックソート・バブルソート・マージソートなどの整列アルゴリズムは全順序に対してのみ正しく機能する。半順序にはトポロジカルソートが使える。

3.4 集合としてのモノイド

モノイドは非常にシンプルにもかかわらず驚くほど強力な概念だ。それは基礎的な計算の背景にある概念であり、加算と乗算は両方ともモノイドをなす。モノイドはプログラミングの世界では至るところにある。それは、文字列、リスト、畳み込み可能なデータ構造、並行プログラミングのfuture、関数型リアクティブプログラミングのイベントなどとして現れる。

伝統的に、モノイドは二項演算を持つ集合として定義される。この演算に要求されるのは、結合律を満たすことと、単位元のように振る舞う特別な要素が1つあることだけだ。

たとえば、0を含む自然数は加算についてモノイドをなす。結合律は次のことを意味する: (a+b)+c=a+(b+c)(a + b) + c = a + (b + c) (言い換えると、数を加算するときは括弧を無視できる。)

中立元24は0だ。なぜなら: 0+a=a0 + a = a かつ a+0=aa + 0 = a だからだ。2つ目の等式は冗長だ。加算は可換 (a+b=b+a)(a + b = b + a) だからだ。ただし、可換律はモノイドの定義の一部ではない。たとえば、文字列連接は可換ではないが、モノイドをなす。ちなみに、文字列連接の中立元は空文字列であり、文字列を変更せずに文字列の両側に付加できる。

Haskellではモノイドに対して型クラスを定義できる。その型クラスに属する型は、memptyと呼ばれる中立元とmappendと呼ばれる二項演算を持つ。

class Monoid m where
    mempty  :: m
    mappend :: m -> m -> m

この2引数関数の型シグネチャーでのm -> m -> mという型は、最初は奇妙に見えるかもしれないが、カリー化を知った後には完全に理にかなったものだと思えるようになるだろう。複数の矢印を含むシグネチャーには、2つの基本的な解釈がある。複数の引数を取る関数とみなして右端の型を戻り値の型とする解釈と、1引数(左端の引数)の関数とみなして関数を返すという解釈だ。後者の解釈は、m -> (m -> m)のように括弧(矢印が右結合であるため冗長)を追加することによって強調できる。この解釈については後で説明する。

Haskellでは、memptymappendのモノイド性(monoidal properties、すなわち、memptyは中立で、mappendは結合律を満たすという事実)を表現する方法がないことに注意してほしい。それらを満たすことを確認するのはプログラマーの責任だ。

HaskellのクラスはC++のクラスほど押し付けがましくはない。新しい型を定義するときに事前にクラスを指定する必要はない。先延ばしして、与えられた型を後からあるクラスのインスタンスであると宣言してよい。例として、memptymappendの実装を提供することでStringをモノイドとして宣言しよう(実際には、これは標準のPreludeですでにやってくれている):

instance Monoid String where
    mempty = ""
    mappend = (++)

ここで、Stringの値は単に文字のリストなので、リスト連接演算子(++)を再利用した。

Haskellの構文に関する注:中置演算子は括弧で囲うことで2つの引数を取る関数に変換できる。与えられた2つの文字列を連接するには、それらの間に++を挟んでもよい:

"Hello " ++ "world!"

あるいは、括弧付きの(++)に2つの文字列を引数として渡してもよい:

(++) "Hello " "world!"

関数の引数がコンマで区切られたり括弧で囲まれたりしていないことに注意してほしい。(これはおそらく、Haskellを学ぶときに慣れるのが一番難しい部分だろう。)

Haskellでは関数の等しさを次のように表現できることは、強調しておく価値がある:

mappend = (++)

概念的には、これは関数によって生成される値の等しさを次のように表現するのとは異なる:

mappend s1 s2 = (++) s1 s2

前者は、𝐇𝐚𝐬𝐤\mathbf{Hask}圏(または、終わりのない計算を指すボトムを無視するなら、𝐒𝐞𝐭\mathbf{Set})の射の等しさに変換される。このような等式はより簡潔であるだけでなく、しばしば他の圏にも一般化できる。後者は外延的等価性 (extensional equivalence) と呼ばれ、どんな2つの入力文字列に対してもmappend(++)の出力は同じであることを述べている。引数の値は (point) と呼ばれることが(「点xxにおけるffの値」という言い回しのように)あるため、これは点ごとの等しさ (point-wise equality) と呼ばれる。引数を指定しない関数の等しさはポイントフリー (point-free) と表現される。(ちなみに、ポイントフリーの式は関数合成を含むことが多く、これは点記号.で表されるため、初心者は少し混乱するかもしれない。)

C++でモノイドを宣言するのに最も近い方法は、C++20標準のコンセプト機能を使うことだ。

template<class T>
struct mempty;

template<class T>
  T mappend(T, T) = delete;

template<class M>
  concept Monoid = requires (M m) {
    { mempty<M>::value() } -> std::same_as<M>;
    { mappend(m, m); } -> std::same_as<M>;
  };

最初の定義は、各特殊化で中立元を保持するための構造だ。

キーワードdeleteは、デフォルト値が定義されていないことを意味する。これはケースバイケースで指定する必要がある。同様に、mappendにもデフォルトはない。

Monoidというコンセプトは、与えられた型Mに対してmemptymappendの適切な定義が存在するかをテストする。

このMonoidコンセプトのインスタンス化は、適切な特殊化とオーバーロードを提供することで実現できる:

template<>
struct mempty<std::string> {
    static std::string value() { return ""; }
};

template<>
std::string mappend(std::string s1, std::string s2) {
    return s1 + s2;
}

3.5 圏としてのモノイド

ここまではモノイドの「おなじみの」定義として、集合の要素に基づくものを見た。しかし、ご存知のように、圏論では集合とその要素から逃れようとし、代わりに対象と射について述べる。そこで、少し視点を変えて、二項演算子を適用すると集合の周りで何かを「移動」したり「シフト」したりすると考えてみよう。

たとえば、各自然数に5を加算する演算を考える。これは0を5、1を6 、2を7のように写す。これは関数で、自然数の集合上で定義されている。良い感じだ。関数と集合がある。一般に、任意の数nnについて、nnを加算する関数が存在する。これはnnの「加算器」だ。

加算器はどのように合成すればよいだろう? 5を加算する関数と7を加算する関数の合成は、12を加算する関数だ。これにより、加算器の合成を加算ルールと等価にできる。これまた良い感じだ。加算を関数合成に置き換えられる。

ちょっと待った。それだけではない。中立元0の加算器もある。0を加算しても何も写されないので、これは自然数の集合における恒等関数だ。

従来の加算の規則を与える代わりに、情報を失うことなく加算器を構成する規則を与えることもできる。加算器の合成は結合律を満たすことに注目してほしい。これは、関数の合成が結合律を満たし、恒等関数に対応する0加算器があるからだ。

鋭い読者なら、整数から加算器への写像がmappendの型シグネチャーをm -> (m -> m)と解釈した結果であることに気付いただろう。これはmappendがモノイド集合の要素を、その集合に作用する関数に写すことを表している。

さて、自然数の集合を扱っていることを忘れて、たくさんの射――加算器たちをひとかたまりにした、単一の対象だと考えてほしい。モノイドは単一対象の圏だ。実際、monoidという名前は、ギリシャ語で単一を意味するmonoに由来する。すべてのモノイドは、適切な合成規則に従う射の集合を持つ単一対象の圏として表せる。

文字列の連接は興味深いケースだ。なぜなら、右連接器(right appender)と左連接器(left appender、あるいはお好みなら前置器prepender)25を定義する選択肢があるからだ。2つのモデルの合成表は互いに鏡像反転している。「foo」に「bar」を後置するのと「bar」に「foo」を前置するのが同じなのは簡単に納得できるだろう。

圏論的モノイド――単一対象の圏――はどれも二項演算を伴う集合としてのモノイドを一意に定義するのか、という疑問を持つかもしれない。単一対象の圏からは常に集合を抽出できることが分かる。その集合は射――この例では加算器――の集合だ。言い換えれば、圏𝐌\mathbf{M}内の単一対象mmについてhom集合𝐌(m,m)\mathbf{M}(m, m) が得られるということだ。この集合における二項演算は簡単に定義できる。2つの集合要素のモノイド的な積は、それらに対応する射を合成したものに対応する要素だ。つまり、𝐌(m,m)\mathbf{M}(m, m) の2つの要素が与えられ、それらに対応する射がffggだとすると、それらの積は合成fgf \circ gに対応する要素となる。この合成は常に存在する。なぜなら、射の始点と終点が同じ対象だからだ。また、圏の規則より、結合律も満たす。恒等射はこの積の中立元だ。このように、圏論的モノイドからは常に集合論的モノイドを復元できる。どこからどう見てもそれらは同一だ。

射あるいは集合内の点として見たモノイドhom集合

数学者が補足すべき箇所は、射は必ずしも集合を形成しないということだけだ。圏の世界には集合よりも大きなものがある。任意の2つの対象間の射が集合を形成する圏は、局所的に小さい、と呼ばれる。約束どおり、私はそのような些細なことはほとんど無視するが、念のため言及すべきだと考えた。

hom集合の要素は、合成律に従う射とも、ある集合内の点とも見なせる。圏論における多くの興味深い現象はこの事実に根ざしている。ここで、𝐌\mathbf{M}の射の合成は、集合𝐌(m,m)\mathbf{M}(m, m) でのモノイド的な積に変換される。

3.6 課題

  1. 以下から自由圏を生成せよ:

    1. 1つのノードを持ち、辺のないグラフ
    2. 1つのノードと1つの(有向)辺を持つグラフ(ヒント:この辺は自身と合成できる)
    3. 2つのノードと、それらの間の(有向)辺を1つ持つグラフ
    4. 1つのノードと、アルファベットa, b, c \ldots zでマークされた26個の(有向)辺を持つグラフ
  2. 以下はどんな順序だろうか?

    1. 複数の集合を要素とする集合と、その上の包含関係:なお、AABBに包含されるとは、AAの全要素がBBの要素でもあることを指す。
    2. C++の型の集合とその上の部分型関係:なお、T1T2の部分型であるとは、T2へのポインターを期待する関数にT1をコンパイルエラーを発生させずに渡せることを指す。
  3. Boolが2つの値TrueFalseの集合であることを踏まえて、それが2つの演算子&& (AND) と|| (OR) のそれぞれについて(集合論的)モノイドをなすことを示せ。

  4. AND演算子を伴うBoolモノイドを圏として表せ。射と合成の規則を列挙せよ。

  5. モジュロ3加算26をモノイドの圏として表せ。

4 クライスリ圏

型と純粋関数を圏としてモデル化する方法についてはすでに見た。また、圏論には副作用、つまり純粋でない関数をモデル化する方法があることにも触れた。その例として、実行をロギングやトレースする関数を見てみよう。命令型言語では、次のように、何らかのグローバルな状態を変更することによって実装される場合が多い:

string logger;

bool negate(bool b) {
     logger += "Not so! ";
     return !b;
}

これは純粋関数ではない。メモ化版ではログを生成できないからだ。この関数には副作用がある。

現代のプログラミングでは、グローバルな可変状態をできる限り避ける。並行性を複雑にするというだけでも理由として十分だ。それに、こんなコードをライブラリに加えたくはないだろう。

我々にとって幸いなことに、この関数は純粋にできる。ログを明示的に受け渡しするだけでよい。文字列引数を追加し、通常の出力に更新されたログを含む文字列を付け加えたペアを返すようにすることで、これを実現しよう:

pair<bool, string> negate(bool b, string logger) {
     return make_pair(!b, logger + "Not so! ");
}

この関数は純粋で、副作用はなく、同じ引数で呼び出されるたびに同じペアを返し、必要ならメモ化できる。ただし、ログには累積的な性質があるので、特定の呼び出しにつながる可能性のあるすべての履歴をメモ化する必要があるだろう。別々のメモエントリーになるのは:

negate(true, "It was the best of times. ");

negate(true, "It was the worst of times. ");

などだ。

これもライブラリ関数のインターフェースとしてはあまり適していない。戻り値が文字列を含むことについては、呼び出し元は無視してよいので、大きな負担ではない。一方で、入力として文字列を渡す必要があるのについては、不便かもしれない。

同じことをもっと面倒なしにやる方法はないのだろうか? 関心事を分離する方法はあるだろうか? この単純な例では、関数negateの主な目的は、あるブール値を別のブール値に変換することだ。ログ生成は二次的なものだ。確かに、ログに記録されるメッセージは関数固有だが、それらのメッセージを1つの連続したログに集約する作業は別の関心事だ。この関数には文字列を生成してほしいが、ログの生成とは分離したい。妥協案はこうだ:

pair<bool, string> negate(bool b) {
     return make_pair(!b, "Not so! ");
}

このアイデアは、ログを関数呼び出しの間で集約する、というものだ。

どうすればこうできるか確認するために、もう少し現実的な例に切り替えよう。小文字を大文字に変換するような、文字列を取って文字列を返す関数があるとする:

string toUpper(string s) {
    string result;
    int (*toupperp)(int) = &toupper; // toupperはオーバーロードされている
    transform(begin(s), end(s), back_inserter(result), toupperp);
    return result;
}

さらに、文字列を空白区切りで分割して文字列のvectorにする別の関数があるとする。

vector<string> toWords(string s) {
    return words(s);
}

実際の作業は補助関数wordsで行われる。

vector<string> words(string s) {
    vector<string> result{""};
    for (auto i = begin(s); i != end(s); ++i)
    {
        if (isspace(*i))
            result.push_back("");
        else
            result.back() += *i;
    }
    return result;
}

関数toUppertoWordsを修正して、通常の戻り値の上にメッセージ文字列を背負わせるようにしたい。

これらの関数の戻り値を「装飾」していこう。総称的な方法で、テンプレートWriterを定義しよう。このテンプレートは、最初の成分が任意の型Aの値で、2番目の成分が文字列であるペアをカプセル化する:

template<class A>
using Writer = pair<A, string>;

装飾された関数は次のとおりだ:

Writer<string> toUpper(string s) {
    string result;
    int (*toupperp)(int) = &toupper;
    transform(begin(s), end(s), back_inserter(result), toupperp);
    return make_pair(result, "toUpper ");
}

Writer<vector<string>> toWords(string s) {
    return make_pair(words(s), "toWords ");
}

この2つの関数を合成することで、文字列を大文字にして単語に分割し、また同時にそれらのログを生成するという、別の装飾された関数にしたい。その方法は次のとおりだ。

Writer<vector<string>> process(string s) {
    auto p1 = toUpper(s);
    auto p2 = toWords(p1.first);
    return make_pair(p2.first, p1.second + p2.second);
}

目標は達成された。ログの集約はもはや個々の関数の関心事ではなくなった。それら個々の関数は独自のメッセージを生成し、それらのメッセージが外部で連接されてより大きなログになる。

このスタイルで書かれたプログラム全体を想像してみてほしい。重複が多くエラーが発生しやすいコードという悪夢だ。しかし、我々はプログラマーだ。重複の多いコードの扱い方は知っている――それを抽象化することだ! ただし、ありきたりの抽象化ではだめだ――関数合成自体を抽象化する必要がある。しかし、合成は圏論の本質なので、さらにコードを書く前に、圏論の観点から問題を分析してみよう。

4.1 Writer圏

いくつかの追加機能を背負わせるために関数群の戻り値の型を装飾するというアイデアは、非常に実りの多いものだと分かった。さらに多くの例をこれから見ることになるだろう。その出発点は、いつもの型と関数の圏だ。対象は型のままにしておくが、射は装飾された関数になるように再定義する。

たとえば、intからboolへの関数isEvenを装飾したいとする。それを装飾された関数で表される射に変換する。重要なのは、装飾された関数は次のようなペアを返すにもかかわらず、この射は相変わらず対象intboolの間の射と見なされる、ということだ:

pair<bool, string> isEven(int n) {
     return make_pair(n % 2 == 0, "isEven ");
}

圏の規則によれば、この射は対象boolから何かに向かう別の射と合成できるはずだ。具体的には、先ほどのnegateと合成できるはずだ:

pair<bool, string> negate(bool b) {
     return make_pair(!b, "Not so! ");
}

明らかに、入力と出力の不一致のせいで、これら2つの射は通常の関数合成と同じようには合成できない。それらの合成はもっとこんな風になるはずだ:

pair<bool, string> isOdd(int n) {
    pair<bool, string> p1 = isEven(n);
    pair<bool, string> p2 = negate(p1.first);
    return make_pair(p2.first, p1.second + p2.second);
}

こうして、我々が構築している新しい圏における2つの射の合成のレシピができる:

  1. 最初の射に対応する装飾された関数を実行する。

  2. その結果のペアから第一成分を取り出し、2番目の射に対応する装飾された関数に渡す。

  3. 最初の結果の第二成分(文字列)と2番目の結果の第二成分(文字列)を連接する。

  4. 2番目の結果の第一成分と連接された文字列を組み合わせた新しいペアを返す。

この合成をC++の高階関数として抽象化したい場合、いま考えている圏での3つの対象に対応する3つの型でパラメーター化されたテンプレートを使わなければならない。そのテンプレートでは、ルール上合成可能な2つの装飾された関数を受け取り、3番目の装飾された関数を返す必要がある:

template<class A, class B, class C>
function<Writer<C>(A)> compose(function<Writer<B>(A)> m1,
                               function<Writer<C>(B)> m2)
{
    return [m1, m2](A x) {
        auto p1 = m1(x);
        auto p2 = m2(p1.first);
        return make_pair(p2.first, p1.second + p2.second);
    };
}

ここで、前の例に戻り、この新しいテンプレートを使ってtoUppertoWordsの合成を実装できる:

Writer<vector<string>> process(string s) {
   return compose<string, string, vector<string>>(toUpper, toWords)(s);
}

composeテンプレートへの型の受け渡しにはまだ多くのノイズがある。これは、戻り値型推論を持つ総称ラムダ関数をサポートするC++14準拠のコンパイラーがあれば回避できる。(このコードはEric Nieblerによる。)

auto const compose = [](auto m1, auto m2) {
    return [m1, m2](auto x) {
        auto p1 = m1(x);
        auto p2 = m2(p1.first);
        return make_pair(p2.first, p1.second + p2.second);
    };
};

この新しい定義では、processの実装は次のように簡略化される:

Writer<vector<string>> process(string s){
   return compose(toUpper, toWords)(s);
}

しかし、まだ完成ではない。この新しい圏における合成を定義したが、恒等射は何だろう? それらは通常の恒等関数ではない! それらは、型Aから型Aに戻る射でなければならず、つまり、それらは次の形の装飾された関数であることを意味する:

Writer<A> identity(A);

それらは合成に関する単位元として振る舞う必要がある。合成の定義を見ると、恒等射は引数を変更せずに渡し、ログには空文字列だけを与える必要があるのが分かる:

template<class A>
Writer<A> identity(A x) {
    return make_pair(x, "");
}

ここで定義した圏が本当に正当な圏であることは簡単に納得できる。特に、ここでの合成が結合性を持つのは自明だ。各ペアの最初の成分で何が起こっているかを見ると、それは単なる通常の関数合成であり、結合的だ。2番目の成分は連接されており、連接も結合的だ。

鋭い読者なら、この構成を文字列モノイドだけでなく、どんなモノイドにも容易に一般化できると気付くだろう。composeの中ではmappendを、identityの中ではmemptyを(+""の代わりに)使おう。ログ生成を文字列だけに限定する理由はない。優れたライブラリ作成者は、ライブラリを機能させられる最小限の制約を見抜けなくてはならない。ここで、ログ生成ライブラリの唯一の要件は、ログがモノイド性を持つことだ。

4.2 HaskellにおけるWriter

Haskellでは同じことを少し簡潔にやれて、コンパイラーからも多くの支援を受けられる。Writer型を定義することから始めよう:

type Writer a = (a, String)

ここでは単にC++のtypedef(またはusing)に相当する型エイリアスを定義している。型Writerは型変数aによってパラメーター化され、aStringのペアと等価だ。ペアの構文は最小限だ。2つの項目を括弧で囲み、コンマで区切るだけだ。

射は任意の型からWriter型への関数だ:

a -> Writer b

合成を中置演算子として宣言する。この風変わりな演算子は「魚」と呼ばれることもある:

(>=>) :: (a -> Writer b) -> (b -> Writer c) -> (a -> Writer c)

この関数は、それら自体も関数である2つの引数を取り、1つの関数を返す。最初の引数の型は(a -> Writer b)、2番目の引数は(b -> Writer c)、結果は(a -> Writer c)となる。

この中置演算子の定義は次のとおりだ27。2つの引数m1m2は魚型シンボルの両側に現れる:

m1 >=> m2 = \x ->
    let (y, s1) = m1 x
        (z, s2) = m2 y
    in (z, s1 ++ s2)

結果は、1つの引数xを取るラムダ関数だ。ラムダはバックスラッシュで書かれる――ギリシャ文字のλ\lambdaを片脚にしたものと考えてほしい。

let式では補助変数を宣言できる。ここでm1を呼び出した結果は変数のペア(y, s1)にパターンマッチングされ、最初のパターンからの引数ym2を呼び出した結果は(z, s2)にマッチングされる。

Haskellでは、C++でアクセッサーが使われるのとは違って、ペアをパターンマッチするのが一般的だ。それ以外は、2つの実装はほぼ直接的に対応している。

let式全体の値はin節で指定される。ここでは、最初の要素がzで、2番目の要素が2つの文字列の連接s1 ++ s2であるペアだ。

この圏の恒等射も定義し、returnと呼ぶことにする28。そう呼ぶ理由は後で明らかになる。

return :: a -> Writer a
return x = (x, "")

完全を期すために、装飾された関数upCasetoWordsのHaskell版を用意しよう:

upCase :: String -> Writer String
upCase s = (map toUpper s, "upCase ")

toWords :: String -> Writer [String]
toWords s = (words s, "toWords ")

関数mapはC++のtransformに対応する。式map toUpperは文字関数toUpperを文字列sに適用する。補助関数wordsは標準のPreludeライブラリで定義されている。

最後に、2つの関数の合成はfish演算子の助けを借りて達成される:

process :: String -> Writer [String]
process = upCase >=> toWords

4.3 クライスリ圏

この圏は私がこの場で発明したのではないことに、もう気付いているかもしれない。これはいわゆるクライスリ圏の一例で、モナドに基づく圏だ。モナドについてはまだ議論する準備ができていないが、モナドで何ができるのかを少しあなたに伝えたかったのだ。我々の限定された目的に関しては、クライスリ圏は、背後にあるプログラム言語の型を対象として持っている29。型AAから型BBへの射は、AAを取る関数であり、特定の装飾によってBBから派生した型を返す。個々のクライスリ圏は、そのような射を合成する個別の方法や、その合成に関する恒等射を定義している。(不正確な用語である「装飾」は、圏論における自己関手という概念に対応していることが後で分かるだろう。)

この章でクライスリ圏の基礎として使ったモナドはwriterモナドと呼ばれ、関数の実行をログに記録したりトレースしたりするために使われる。また、純粋な計算に副作用を埋め込むための、より汎用的なメカニズムの例でもある。これまで見てきたように、プログラミング言語の型と関数は、(いつもどおりボトムは無視して)集合の圏でモデル化できる。ここでは、そのモデルをわずかに異なる圏へと拡張した。すなわち、装飾された関数によって射が表現され、それらを合成した関数がひとつの関数の出力を別の関数の入力に渡す以上のことを行う圏だ。このクライスリ圏ではもうひとつ使える自由度がある。合成そのものだ。それはまさに、命令型言語においては副作用を用いて従来は実装されてきたようなプログラムに、簡潔な表示的意味を与えられるようにする自由度だと分かる。

4.4 課題

引数が取り得る値のすべてに対して定義されているわけではない関数は、部分関数と呼ばれる。これは実際には数学的な意味での関数ではないので、標準的な圏の枠組みには合わない。しかし、装飾された型optionalを返す関数でなら表せる:

template<class A> class optional {
    bool _isValid;
    A    _value;
public:
    optional()    : _isValid(false) {}
    optional(A v) : _isValid(true), _value(v) {}
    bool isValid() const { return _isValid; }
    A value() const { return _value; }
};

たとえば、以下は装飾された関数safe_rootの実装だ:

optional<double> safe_root(double x) {
    if (x >= 0) return optional<double>{sqrt(x)};
    else return optional<double>{};
}

課題は以下のとおりだ:

  1. 部分関数についてクライスリ圏を構築せよ(合成と恒等射を定義せよ)。

  2. 引数が0でない場合にその逆数を返す装飾された関数safe_reciprocalを実装せよ。

  3. 関数safe_rootsafe_reciprocalを合成して、可能なすべての場合にsqrt(1/x)を計算するsafe_root_reciprocalを実装せよ。

5 積と余積

古代ギリシアの劇作家のエウリーピデースは「人間は、喜んで交際している仲間たちと異なるところがない」と言った30。我々は人間関係によって定義される。これほど圏論に当てはまる言葉はない。圏の中の特定の対象を選び出すには、他の対象(およびそれ自身)との関係性のパターンを記述するしかない。それらの関係性は射によって定義される。

圏論では、普遍的構成 (universal construction) と呼ばれる、対象をその関係性によって定義するための構成法がよく現れる。そのための方法の1つとしては、対象と射から構成された特定の形のパターンを選び、圏に出現するそのパターンをすべて探すことが挙げられる。そのパターンが十分に一般的で、圏が大きい場合、該当するものが山ほど出てくるだろう。秘訣は、該当したものに対するある種の順位付けを確立し、最適と考えられるものを選択することだ。

このプロセスはウェブ検索のやり方を思い起こさせる。クエリはパターンのようなものだ。非常に一般的なクエリなら、再現率 (recall) が高く、すなわちヒットする数が多い。関連性があるものもあれば、そうでないものもあるだろう。無関係なヒットを削除するには、クエリを絞り込む。これにより精度 (precision) が向上する。最終的に、検索エンジンは検索結果を順位付けして、うまくいけば、あなたが興味のある結果がリストの一番上に表示される。

5.1 始対象

最も単純な形は単一の対象だ。明らかに、この形の実例は、特定の圏にある対象と同じ数だけ存在する。それでは候補が多すぎる。ある種の順位付けを確立し、この階層のトップにある対象を見つける必要がある。我々が自由に使える唯一の手段は射だ。射を矢印として捉えるなら、矢印の全体的な総フローが、圏の一方の端から他方の端へと存在しうる。これは半順序などの順序付けられた圏に当てはまる。aaからbbへ向かう矢印(射)が存在するなら、対象aaは対象bbよりも「始め」だとして、対象の優先順位の概念を一般化できる。次に、唯一の始対象を、他のすべての対象に向かう射を持つものとして定義する。もちろん、そのような対象が存在する保証はない。だが、それについては大丈夫だ。より大きな問題は、そのような対象が多すぎるかもしれないことだ。再現率は高いが、精度を欠いている。解決策は、順序圏からヒントを得ることだ。それらの圏では、任意の2つの対象の間に高々1つの射しかない。そのため、別の対象以下となる方法は1つしかない。これは始対象の次のような定義につながる:

始対象 (the initial object) とは、圏内の任意の対象に対し、そこへ向かう射をちょうど1つだけ持つ対象である。

それでも、始対象が(存在するにしても)一意だとは保証されない。しかし、それは次善のものを保証する。同型を除いて一意 (uniqueness up to isomorphism) という性質だ。同型は圏論では非常に重要なので、すぐに説明する。差し当たっては、同型を除いて一意という性質が始対象の定義における “the” の使用を正当化するのを認めることにしよう。

以下にいくつかの例を示す。半順序集合(partially ordered set、しばしばposetとも)では最小元が始対象となる。始対象を持たない半順序集合もある。たとえば、すべての整数の集合(正の数も負の数も含む)で、小なりイコール関係を射とするものだ。

集合と関数の圏では、始対象は空集合だ。空集合はHaskellの型Voidに対応し(C++には対応する型は存在しない)、Voidから他の型への一意な多相関数はabsurdと呼ばれていたのを思い出してほしい。

absurd :: Void -> a

Voidを型の圏における始対象にしているのは、この射の族なのだ。

5.2 終対象

引き続き単一対象パターンを扱うが、対象の順位付け方法を変更しよう。bbからaaへの射がある場合、対象aaは対象bbよりも「終わりの側」と言える(方向が逆になっていることに注意してほしい)。探したいのは、圏のどの対象よりも終わりの側となる対象だ。再び、一意性を主張することになる:

終対象 (the terminal object) とは、圏内のどの対象からもちょうど1つの射しか来ない対象である。

繰り返しになるが、終対象は同型を除いて一意だ。これについてはすぐ後で説明する。まずは、いくつかの例を見てみよう。半順序集合では、終対象があれば、それが最大元だ。集合の圏では、終対象は単元集合だ。単元集合についてはすでに説明した。単元集合は、C++ではvoid型に対応し、Haskellではunit型()に対応する。この型が持つ唯一の値は、C++では暗黙的だが、Haskellでは明示的に()で表される。また、任意の型からunit型へのちょうど1つの純粋関数が存在することも確認した:

unit :: a -> ()
unit _ = ()

これで、終対象のすべての条件が満たされた。

この例では、射の一意性という条件が決定的に重要であることに注意してほしい。なぜなら、すべての集合から入ってくる射を持つ他の集合(実際には、空集合を除くすべての集合)が存在するからだ。たとえば、すべての型に対して定義されたブール値関数(述語)がある:

yes :: a -> Bool
yes _ = True

しかし、Boolは終対象ではない。すべての型に対して、少なくとももう1つのBool値関数がある(Voidに対してはどちらの関数もabsurdと等しくなるので除く):

no :: a -> Bool
no _ = False

一意性を主張することで、終対象の定義を1つの型だけに絞り込むのにちょうど良い精度が得られる。

5.3 双対性

始対象と終対象の定義の対称性には注目せずにいられないだろう。両者の唯一の違いは、射の方向だった。どの圏𝐂\mathbf{C}に対しても、すべての射を逆にするだけで反対圏 (opposite category) 𝐂𝑜𝑝\mathbf{C}^\mathit{op}を定義できると分かる31。 反対圏は、同時に合成を再定義しさえすれば、圏としての要件をすべて自動的に満たす。もとの射fabf \Colon a \to bgbcg \Colon b \to ch=gfh=g \circ fによってhach \Colon a \to cへと合成される場合、逆の射f𝑜𝑝baf^\mathit{op} \Colon b \to ag𝑜𝑝cbg^\mathit{op} \Colon c \to bh𝑜𝑝=f𝑜𝑝g𝑜𝑝h^\mathit{op} = f^\mathit{op} \circ g^\mathit{op}によってh𝑜𝑝cah^\mathit{op} \Colon c \to aへと合成される32。 そして、恒等射を逆にすることは、(駄洒落に注意!)no-opだ。

双対性は圏の非常に重要な特性だ。圏論を扱うすべての数学者の生産性を倍増させるからだ。思いつくすべての構成にはその反対があり、そして、証明するすべての定理について無料でもう1つ付いてくる。反対圏の構成にはしばしば「余」(co) が前置され、積と余積、モナドとコモナド、錐と余錐、極限と余極限などがある。ただし、射を2回反転させればもとの状態に戻るので、ココモナドはない。

そのため、終対象は反対圏の始対象だと言える。

5.4 同型

プログラマーである我々は、等しさを定義することが簡単な作業ではないことをよく知っている。2つのオブジェクトが等しいとはどういう意味だろう? メモリー内の同じ場所を占有する必要があるだろうか(ポインターの等しさ)。あるいは、すべての要素の値が同じであれば十分だろうか? 2つの複素数の一方を実部と虚部で表し、もう一方を絶対値と偏角で表すしたとき、それらは等しいだろうか? 数学者たちが等しさの意味を解明済みだろう、と思うかもしれないが、そうではない。数学においても、等しさには複数の競合する定義があるという、同じ問題がある。命題として表される等しさ (propositional equality)、内包的な 等しさ (intensional equality)、外延的な等しさ (extensional equality)、ホモトピー型理論における道 (path) としての等しさがある。そして同型 (isomorphism) のより弱い概念、さらには等価性 (equivalence) のより弱い概念もある。

直観としては、同型の対象は同じように見える。つまり、同じ形をしている。これは、1対1写像により、ある対象のどの一部も別の対象のどこか一部に対応することを意味する。我々の道具で調べうる限り、2つの対象はお互いの完全なコピーだ。数学的には、対象aaから対象bbへの写像があり、対象bbから対象aaへの写像があり、それらが互いの逆であることを意味する。圏論では、写像を射に置き換える。同型射は可逆な射、つまり一方が他方の逆になっているような射のペアだ。

逆であるということは合成と恒等射によって理解できる。射ggが射ffの逆であるのは、それらの合成が恒等射となる場合だ。それら2つの射は、合成する方法が2つあるので、実際には2つの等式で表される:

f . g = id
g . f = id

始(終)対象が同型を除いて一意だと言ったとき、2つの始(終)対象は同型だということを意味していた。これは簡単に理解できる。2つの始対象i1i_{1}i2i_{2}があるとしよう。i1i_{1}が始対象であるため、i1i_{1}からi2i_{2}への一意な射ffが存在する。同様に、i2i_{2}が始対象であるため、i2i_{2}からi1i_{1}への一意な射ggが存在する。これらの2つの射を合成すると何になるだろう?

この図中のすべての射は一意だ

合成gfg \circ fは、i1i_{1}からi1i_{1}への射でなければならない。しかし、i1i_{1}は始対象なので、i1i_{1}からi1i_{1}へ向かう射は1つだけだ。圏の中なので、i1i_{1}からi1i_{1}への恒等射があるのは分かっている。候補は1つだけなので、gfg \circ fはそれでなければならない。したがって、gfg \circ fは恒等射と等しくなる。同様に、fgf \circ gは恒等射と等しくなければならない。i2i_{2}からi2i_{2}に戻る射は1つしかないからだ。これは、ffggが互いの逆でなければならないことを証明している。したがって、任意の2つの始対象は同型だ。

この証明では、始対象からそれ自体への射の一意性を用いたことに注意してほしい。そうしなければ「同型を除いて」の部分は証明できない。しかし、なぜffggの一意性が必要なのだろうか。始対象は同型を除いて一意なだけでなく、一意な同型を除いて一意だからだ。原則として、2つの対象間には複数の同型が存在する可能性があるが、ここではそうではない。この「一意な同型を除いて一意という性質」は、すべての普遍的構成の重要な特性だ。

5.5

次の普遍的構成は積 (product) に関するものだ。2つの集合のデカルト積 (Cartesian product) が何なのかは知っている。ペアからなる集合だ。しかし、集合の積とそれを構成する集合を結びつけるパターンは何だろう? それが分かれば、他の圏にも一般化できるはずだ。

唯一言えるのは、積から各構成要素への射影 (projection) という関数が2つある、ということだ。Haskellでは、これら2つの関数はfstsndと呼ばれ、それぞれペアの第一要素と第二要素を抜き出す:

fst :: (a, b) -> a
fst (x, y) = x
snd :: (a, b) -> b
snd (x, y) = y

ここで、関数は引数に対するパターンマッチングによって定義されている。パターン(x, y)は任意のペアにマッチし、要素を変数xyに抽出する。

これらの定義はワイルドカードを使ってさらに単純化できる:

fst (x, _) = x
snd (_, y) = y

C++では、たとえば次のようなテンプレート関数が使われるだろう:

template<class A, class B>
A fst(pair<A, B> const & p) {
    return p.first;
}

この非常に限られているように一見思える知識をもって、2つの集合(aabb)の積の構成につながる、集合の圏における対象と射のパターンを定義してみよう。このパターンは対象ccと、それぞれaabbに接続される2つの射ppqqとで構成される:

p :: c -> a
q :: c -> b

このパターンに一致するすべてのccが積の候補となる。それは大量にあるかもしれない。

たとえば、構成要素として、2つのHaskellの型IntBoolを選択してみて、それらの積の候補を挙げてみよう。

1番目の候補はIntだ。IntIntBoolの積の候補となるだろうか? そう、候補となる――その射影はこうなる:

p :: Int -> Int
p x = x

q :: Int -> Bool
q _ = True

これはかなり酷いが、基準を満たしている。

2番目の候補は(Int, Int, Bool)だ。要素が3つの組、すなわち3つ組だ。これを正当な候補にする2つの射を以下に示す(ここでは3つ組に対してパターンマッチングを使っている):

p :: (Int, Int, Bool) -> Int
p (x, _, _) = x

q :: (Int, Int, Bool) -> Bool
q (_, _, b) = b

1番目の候補は狭すぎ、積のIntの次元だけをカバーしている。一方で、2番目の候補は大きすぎ、Intの次元が重複してしまっている。

しかし、この普遍的構成の別の部分である順位付けについてはまだ調べていない。そこで、パターンの2つの例を比較できるようにしたい。つまり、対象の1つの候補ccとその2つの射影ppおよびqqを、別の対象の候補cc'とその2つの射影pp'およびqq'と比較したい。cc'からccへの射mmがある場合に、cccc'よりも「優れている」と言いたいのだが、それではあまりにも弱い。それに加えて、ccの射影たちがcc'の射影たちよりも「優れている」、すなわち「より普遍的」であってほしい。つまり、射影pp'qq'は、ppqqからmmを使って再構成できるということだ:

p' = p . m
q' = q . m

別の観点でこれらの等式を見ると、mmpp'qq'分解 (factorize) している。これらの等式が自然数について成り立ち、ドットが乗算であると仮定すると、mmpp'qq'に共通な因数だ。

ある種の直観を築くために、ペア型(Int, Bool)およびその正統な射影fstsndが、前に提示した2つの候補より本当に優れていることを示そう。

1番目の候補に対する写像mは次のようになる:

m :: Int -> (Int, Bool)
m x = (x, True)

実際、2つの射影pqは次のように再構成できる:

p x = fst (m x) = x
q x = snd (m x) = True

2番目の例のmも同様に一意に定まる:

m (x, _, b) = (x, b)

(Int, Bool)が2つの候補のどちらよりも優れていることを示せた。その逆がなぜ真ではないのかを見てみよう。pqからfstsndを再構築するのに役立つm'を見つけられるだろうか?

fst = p . m'
snd = q . m'

1番目の例では、qは常にTrueを返す。しかし、第2要素がFalseであるペアが存在するのは分かっている。したがって、qからはsndを再構築できない。

2番目の例は別物だ。pまたはqを経た後でも十分な情報が保持される。しかし、fstsndを分解する方法が複数ある。pqはどちらも3つ組の第2要素を無視するので、m'はそこに何でも入れられる。たとえば:

m' (x, b) = (x, x, b)

あるいは:

m' (x, b) = (x, 42, b)

などを定義できる。

以上すべてをまとめると、2つの射影pqを持つ任意の型cについて、それらの射影を分解する一意なmcからデカルト積(a, b)へと存在する。実際には、pqを組み合わせてペアにしているだけだ:

m :: c -> (a, b)
m x = (p x, q x)

これによってデカルト積(a, b)がベストマッチとなり、すなわち、この普遍的構成が集合の圏で機能することを意味する。この構成は任意の2つの集合の積を選ぶ。

さて、集合のことは忘れて、同じ普遍的構成を使って任意の圏にある2つの対象の積を定義しよう。そのような積が必ず存在するわけではないが、存在する場合は、一意な同型を除いて一意だ。

2つの対象aabbとは、2つの射影を伴う対象ccであり、別の任意の対象cc'が伴う2つの射影について、それらを分解するcc'からccへの一意な射mmが存在するものを言う。

2つの候補から分解関数mを生成する(高階)関数は、factorizerと呼ばれることもある。この例では、次の関数になる:

factorizer :: (c -> a) -> (c -> b) -> (c -> (a, b))
factorizer p q = \x -> (p x, q x)

5.6 余積

圏論のすべての構成と同じく、積にも双対があり、余積 (coproduct) と呼ばれる。積のパターンの射を逆にすると、2つの入射 (injection) ijを伴う対象cになる33。すなわち、aabbからccへの射だ。

i :: a -> c
j :: b -> c

順位付けも逆転している:対象ccは、もしccからcc'への射mmが単射を分解するなら、ii'jj'の単射を伴う対象cc'よりも「優れて」いる。

i' = m . i
j' = m . j

「最も優れた」対象は、そこから他のどのパターンへも一意な射を持つもので、余積と呼ばれ、存在する場合は、一意な同型を除いて一意だ。

2つの対象aabb余積とは、2つの単射を伴う対象ccであり、別の任意の対象cc'が伴う2つの単射について、それらを分解するccからcc'への一意な射mmが存在するものを言う。

集合の圏では、余積は2つの集合の非交和 (disjoint union) だ。aabbの非交和の要素は、aaの要素かbbの要素のどちらかだ。2つの集合が重なる場合、非交和には共通部分のコピーが2つ含まれる。非交和の要素は起源を示す識別子でタグ付けされていると見なせる。

プログラマーにとっては、型の観点から余積を理解する方が簡単だ。それは2つの型からなるタグ付き和 (tagged union) だ。C++がサポートしている共用体 (union) はタグ付けされていない。つまり、プログラム内では共用体のどのメンバーが有効であるかを何らかの方法で追跡しなければならないということだ。タグ付き共用体を作成するには、タグ――列挙型――を定義して共用体と結びつける必要がある。たとえば、intchar const*のタグ付き共用体は次のように実装できる:

struct Contact {
    enum { isPhone, isEmail } tag;
    union { int phoneNum; char const * emailAddr; };
};

これに対する2つの入射は、コンストラクターとしても、関数としても実装できる。たとえば、最初の単射を関数PhoneNumとして実装するとこうなる:

Contact PhoneNum(int n) {
    Contact c;
    c.tag = isPhone;
    c.phoneNum = n;
    return c;
}

これはContactに整数を注入 (inject) する。

タグ付き共用体はvariantとも呼ばれ、boostライブラリに非常に汎用的なboost::variantという実装がある34

Haskellでは、データ構成子を縦棒で区切ることで、任意のデータ型をタグ付き和にまとめられる。Contactの例だと次のような宣言になる:

data Contact = PhoneNum Int | EmailAddr String

ここで、PhoneNumEmailAddrは、構成子(入射)としても、パターンマッチングのタグとしても機能する(これについては後で詳しく説明する)。たとえば、電話番号を使って連絡先を構成する方法はこうなる:

helpdesk :: Contact
helpdesk = PhoneNum 2222222

正統な実装では、積がプリミティブなペアとしてHaskellに組み込まれているのに対し、余積はEitherと呼ばれるデータ型であり、標準のPreludeで次のように定義されている:

data Either a b = Left a | Right b

これはabの2つの型によってパラメーター化され、2つの構成子を持つ。すなわち、型aの値を取るLeftと、型bの値を取るRightだ。

積についてfactorizerを定義したのと同様に、余積についても定義できる。余積の型の候補cと2つの入射の候補ijについて、Eitherに対するfactorizerは次の分解関数を生成する:

factorizer :: (a -> c) -> (b -> c) -> Either a b -> c
factorizer i j (Left a)  = i a
factorizer i j (Right b) = j b

5.7 非対称性

これまでに2組の双対の定義を見てきた。終対象の定義は、始対象の定義から射の方向を逆にすることで得られ、余積の定義は積の定義から得られる。しかし、集合の圏では、始対象と終対象は大きく異なり、余積と積は大きく異なる。後述するように、積は乗算のように振る舞い、終対象は1の役割を果たし、余積は和のように振る舞い、始対象は0の役割を果たす。特に、有限集合の場合、積のサイズは個々の集合のサイズの積であり、余積のサイズはサイズの合計だ。

これは集合の圏が射の反転に関して対称でないことを示している。

空集合については、どの集合に対しても一意な射(absurd関数)がある一方で、戻ってくる射はないことに注意してほしい[訳注:ただし、空集合自身から戻ってくる恒等射(これもabsurd関数)は存在する。]。単元集合では、どの集合からも一意な射が来るうえに、(空集合を除く)すべての集合へ向かう外向きの射ある。これまで見てきたように、終対象から発するこれらの射[訳注:終対象の定義では終対象へ向かう射についてしか述べていないので、終対象から他の対象へ向かう射が存在するのは何の問題もない。]は、他の集合の要素を選択するのに非常に重要な役割を果たしている(空集合には要素がないので、選択するものは何もない)。

単元集合と積の関係は、余積とは全く違う。unit型()で表される単元集合を、積パターンのもう1つの――非常に劣った――候補として使うことを考えてみてほしい。それを2つの射影pq、すなわち単元集合から各構成要素の集合への関数として実装してみよう。それらは具体的な要素をそれぞれの集合から選択する。積は普遍的なので、ここでの候補の単元集合から積への(一意な)射mも存在する。この射は積の集合から要素を選択する。つまり、具体的なペアを選択する。さらに、次の2つの射影を分解する:

p = fst . m
q = snd . m

単元集合の唯一の要素である値()に作用させると、これら2つの式は次のようになる:

p () = fst (m ())
q () = snd (m ())

m ()mによって選択された積の要素なので、これらの式は、第1の集合からpによって選択された要素p ()が、mによって選択されたペアの第1要素であることを示す。同様に、q ()は第2要素に等しい。これは、積の要素は構成要素の集合からの要素のペアであるという理解と完全に一致している。

余積にはそのような単純な解釈はない。単元集合を余積での候補として要素を抽出しようと試みることもできるが、2つの射影がそこから出てくるのではなく、2つの入射がそこに入ることになる。それらはその起源について何も教えてくれないだろう(実際、入力パラメーターが無視されるのを見てきた)。また、余積から単元集合への一意な射についても同様だろう。集合の圏は、始対象の向きから見たときと終対象から見たときとでは全く違って見える。

これは集合の固有の特性ではなく、𝐒𝐞𝐭\mathbf{Set}で射として使う関数の特性だ。関数は(一般に)非対称だ。説明しよう。

関数は、その始域 (domain) のすべての要素に対して定義する必要がある(プログラミングでは関数 (total function) と呼ぶ)。しかし、終域 (codomain) 全体を網羅する必要はない。すでにその極端な例をいくつか見てきた。単元集合からの関数たち――終域内の1つの要素だけを選択する関数たちのことだ。(実際に、空集合からの関数は本当に極端だ。) 始域のサイズが終域のサイズよりもずっと小さい場合、我々はよく、始域を終域に埋め込むような関数を思い浮かべる。たとえば、単元集合からの関数は、その1つの要素を終域に埋め込むものだと考えられる。私はそれらを埋め込み (embedding) 関数と呼んでいるが、数学者は反対のものに名前を付ける方を好む。つまり、終域をきっちり満たす関数を全射 (surjective) または上への (onto) 関数と呼ぶ。

非対称性のもう1つの原因は、関数が始域の複数の要素を終域の1つの要素に写せることだ。そういった関数はそれらの要素を潰す (collapse) ことができる。極端な例としては、集合全体を単元集合に写す関数が挙げられる。これまでに、まさにそれを行う多相unit関数を見てきた。合成すると、より酷く潰すことにしかならない。潰す関数2つの合成は、個々の関数よりもさらに潰すことになる。数学者は潰さない関数を単射 (injective) または1対1 (one-to-one) という名前で呼ぶ。

当然、埋め込みも潰しもしない関数もある。それらは全単射 (bijection) と呼ばれ、可逆なので真に対称だ。集合の圏では、同型は全単射と同じだ。

5.8 課題

  1. 終対象が一意な同型を除いて一意であることを示せ。

  2. 半順序集合において2つの対象の積は何か? ヒント:積の普遍的構成を使う。

  3. 半順序集合において2つの対象の余積は何か?

  4. HaskellのEitherに相当するものを、(Haskell以外の)好きな言語で総称型として実装せよ。

  5. Eitherが、次の2つの単射を伴うintよりも「優れた」余積であることを示せ。

    int i(int n) { return n; }
    int j(bool b) { return b? 0: 1; }

    ヒント:関数

    int m(Either const & e);

    を、ijを分解するように定義する。

  6. 前の問題の続き:ijという2つの単射を伴うintEitherよりも「優れている」ことはあり得ないと主張するにはどうすればよいか?

  7. さらに続き:次の単射についてはどうか?

    int i(int n) {
        if (n < 0) return n;
        return n + 2;
    }
    int j(bool b) { return b? 0: 1; }

#. intboolの余積の候補として、Eitherへの射を複数許容するという理由でEitherより劣るものを挙げよ。

5.9 参考文献

  1. CatstersのProducts and Coproducts35の動画

6 シンプルな代数的データ型

型を組み合わせる2つの基本的な方法として、積と余積を使う方法を見たところだ。日常のプログラミングにおける多くのデータ構造は、実はこの2つのメカニズムだけを使って構築できる。この事実は重要かつ有用な帰結をもたらす。データ構造の性質の多くは合成可能だということだ。たとえば、基本型の値が等しいかどうか比較する方法を知っていて、それらの比較を積と余積に相当する型に一般化する方法を知っていれば、自動的に複合型の等値演算子を導出できる。Haskellでは、等しさの検査、大小比較、文字列への変換、文字列からの変換などを、複合型の大きな部分集合に対して自動的に導出できる。

次に、直積型 (product type)36 と直和型 (sum type) がプログラミングに現れる様子を詳しく見てみよう。

6.1 直積型

2つの型の直積(直積型)のプログラム言語における正統な実装はペアだ。Haskellではペアはプリミティブな型構成子 (type constructor) だ。C++では比較的複雑なテンプレートとして標準ライブラリで定義されている。

ペアは厳密には可換ではない。(Int, Bool)型のペアは、同じ情報を保持していても、ペア(Bool, Int)型のペアには置き換えられない。しかし、それらは同型を除いて可換だ。同型写像はswap関数(それ自身の逆関数)によって与えられる:

swap :: (a, b) -> (b, a)
swap (x, y) = (y, x)

これら2つのペア型は、単に同じデータを格納するために異なるフォーマットを使っていると見なせる。ビッグエンディアンとリトルエンディアンのようなものだ。

ペアの中にペアをネストすれば任意の個数の型の直積を作れるが、もっと簡単な方法がある。ネストされたペアは組 (tuple) と等価なのだ。これは、ペアをネストする様々な方法が同型であるという事実からの帰結だ。3つの型abcを順に積にする場合、次の2つの方法がある:

((a, b), c)

あるいは

(a, (b, c))

これらは型が異なる。つまり、一方の型の引数を期待する関数に他方の型の値を渡すことはできない。しかし、これらの要素は1対1で対応している。そのため、一方を他方に写す関数が存在し:

alpha :: ((a, b), c) -> (a, (b, c))
alpha ((x, y), z) = (x, (y, z))

そして、その関数は逆関数を持つ:

alpha_inv :: (a, (b, c)) -> ((a, b), c)
alpha_inv  (x, (y, z)) = ((x, y), z)

したがって、この関数は同型写像だ。データは同じで再パッケージ化するための方法が違うにすぎない。

直積型の生成は、型上の二項演算として解釈できる。この観点から見ると、上記の同型は、モノイドで見た結合律 (associativity law) に非常によく似ている: (a*b)*c=a*(b*c)(a * b) * c = a * (b * c) ただし、モノイドの場合は積を合成する2つの方法が等しかったのに対して、ここでは「同型を除いて」等価であるにすぎない。

同型を認めて、厳密な等しさに固執しないならば、さらに進んで、1が乗算の単位元であるのと同じようにunit型()が積の単位元であるのを示せる。実際、ある型aの値とunit型の値を組み合わせても何の情報も追加されない。型:

(a, ())

aと同型だ。対応する同型写像はこのようになる:

rho :: (a, ()) -> a
rho (x, ()) = x
rho_inv :: a -> (a, ())
rho_inv x = (x, ())

これらの観察は、𝐒𝐞𝐭\mathbf{Set}(集合の圏)はモノイダル圏 (monoidal category) である、と述べることによって形式化できる。それは、対象を(ここではデカルト積で)乗算できるという意味で、モノイドでもある圏だ。モノイダル圏についてさらに説明しよう。完全な定義は将来的に示す。

Haskellには直積型を定義するもっと一般的な方法がある。特に、すぐ後で説明するとおり、直和型と組み合わされたときにはっきりする。その方法は複数の引数を持つ名前付き構成子を使うというものだ。たとえば、ペアは次のようにも定義できる:

data Pair a b = P a b

ここで、Pair a bは他の2つの型abによってパラメーター化された型の名前であり、Pはデータ構成子の名前だ。ペア型を定義するには型構成子Pairに2つの型を渡す。適切な型の2つの値を構成子Pに渡すことで、ペア値を構成する。たとえば、値stmtStringBoolのペアとして定義したとする:

stmt :: Pair String Bool
stmt = P "This statements is" False

最初の行は型シグネチャーだ37。そこでは、型構成子Pairが、そのPairの総称定義のabをそれぞれStringBoolで置き換えた形で使われている。2行目では、具体的な文字列と具体的なブール値をデータ構成子Pに渡すことで、実際の値を定義している。型構成子は型を構成するために使われ、データ構成子は値を構成するために使われる。

Haskellでは型構成子とデータ構成子の名前空間が分離されているため、次のように両方に同じ名前が使われていることがよくある:

data Pair a b = Pair a b

さらに目を細めれば、組み込みのペア型をこの種の宣言のバリエーションとして見ることもできる。組み込みのペア型においては、Pairという名前が(,)という二項演算子で置き換わっている。実際、(,)を他の名前付き構成子と同じように扱い、前置記法を使ってペアを作成できる:

stmt = (,) "This statement is" False

同様に、(,,)を使って3つ組を作成する、などもできる。

総称ペアや組を使う代わりに、次のように特定の名前を付けた直積型を定義してもよい:

data Stmt = Stmt String Bool

これは単にStringBoolの積だが、独自の名前と構成子が与えられている。このスタイルの宣言の利点は、内容は同じでも意味と機能が異なる互いに置き換えられない型を多数定義できることだ。

組や複数の引数を持つ構成子を使ってプログラミングすると、ぐちゃぐちゃになり間違いが生じやすくなる。どの成分が何を表しているかを追うのが大変になるからだ。成分に名前を付ける方が望ましい場合はよくある。名前付きフィールドを持つ直積型は、Haskellではrecord38、Cではstructと呼ばれる。

6.2 レコード

簡単な例を見てみよう。化学元素を記述するために、2つの文字列(名前と元素記号)と整数(原子番号)を組み合わせて1つのデータ構造にしたい。組(String, String, Int)を使い、どの成分が何を表しているかを覚えておくという方法がある。そして、成分はパターンマッチングによって、(HeHeliumの接頭辞だというように)元素記号が元素名の接頭辞かをチェックする次の関数のように抽出することにしよう:

startsWithSymbol :: (String, String, Int) -> Bool
startsWithSymbol (name, symbol, _) = isPrefixOf symbol name

このコードは間違いが生じやすく、読むのもメンテナンスするのも困難だ。レコードを定義する方がはるかに良い。

data Element = Element { name         :: String
                       , symbol       :: String
                       , atomicNumber :: Int }

これら2つの表現は同型だ。そのことは、互いに逆になっている2つの変換関数から分かる。

tupleToElem :: (String, String, Int) -> Element
tupleToElem (n, s, a) = Element { name = n
                                , symbol = s
                                , atomicNumber = a }
elemToTuple :: Element -> (String, String, Int)
elemToTuple e = (name e, symbol e, atomicNumber e)

レコードのフィールド名は、それらのフィールドにアクセスするための関数としても機能することに注意してほしい。たとえば、atomicNumbereからatomicNumberフィールドを取得する。つまり、atomicNumberは次のような型の関数として使われる:

atomicNumber :: Element -> Int

Elementについてのレコード構文によって、関数startsWithSymbolはより読みやすくなる:

startsWithSymbol :: Element -> Bool
startsWithSymbol e = isPrefixOf (symbol e) (name e)

関数isPrefixOfをバッククォーテーションで囲んで中置演算子に変換するというHaskellの小技を使えば、まるで英文のように読めるようにさえできる:

startsWithSymbol e = symbol e `isPrefixOf` name e

中置演算子は関数呼び出しよりも優先順位が低いため、この場合は括弧を省略できる。

6.3 直和型

集合の圏の積が直積型のもととなるのと同じように、余積は直和型のもととなる。Haskellにおける直和型の正統な実装は次のようなものだ:

data Either a b = Left a | Right b

また、ペアと同様に、Eitherは(同型を除いて)可換であり、ネストでき、ネストの順序は(同型を除いて)無関係だ。したがって、たとえば、3つ組に相当する和:

data OneOfThree a b c = Sinistral a | Medial b | Dextral c

などを定義できる。

𝐒𝐞𝐭\mathbf{Set}は余積に関する(対称)モノイダル圏でもあることが分かる。二項演算の役割を演じるのは非交和であり、単位元の役割を演じるのは始対象だ。型に関しては、モノイダル演算子としてEitherがあり、その中立元として住人39がいない型 (uninhabited type) であるVoidがある。 Eitherは加算、Voidは0と見なせる。実際、直和型にVoidを足しても内容は変わらない。例として:

Either a Void

aと同型だ。これは、この型のRight版を構成する方法がないためだ。型Voidに値は存在しない。 Either a Voidの唯一の要素は、Left構成子を使って構築されたものであり、単純に型aの値をカプセル化したものだ。したがって、記号的に表すと、a+0=aa + 0 = aとなる。

直和型がHaskellではごく普通に使われるのに対し、C++で同等のものである共用体やvariantはあまり使われない。その理由はいくつかある。

まず、最も単純な直和型は単なる列挙であり、C++ではenumを使って実装できる。Haskellの直和型:

data Color = Red | Green | Blue

にC++で相当するものは:

enum { Red, Green, Blue };

だ。もっとシンプルな直和型:

data Bool = True | False

は、C++ではプリミティブboolだ。

値の有無を表す単純な直和型は、C++では、特殊なトリックや「不可能な」値(空文字列・負の数・ヌルポインターなど)を使ってさまざまに実装される。この種のオプション性は、意図的な場合、HaskellではMaybe型を使って表現される:

data Maybe a = Nothing | Just a

Maybe型は2つの型の直和だ。このことは構成子の2つの部分を個々の型に分けると分かる。1つ目は次のようになる:

data NothingType = Nothing

これはNothingという名前の1つの値を持つ列挙だ。言い換えると、これは単元集合であり、unit型()と等価だ。2つ目の部分:

data JustType a = Just a

は、型aを単にカプセル化したものだ。Maybeを次のように書いてもよかっただろう:

type Maybe a = Either () a

より複雑な直和型は、C++ではポインターを使って模擬することが多い。ポインターはヌルとなるか、あるいは特定の型の値を指し示す。たとえば、Haskellで(再帰的な)直和型として定義できるリスト型:

List a = Nil | Cons a (List a)

をC++に変換するには、ヌルポインターのトリックを使って空のリストを実装する:

template<class A>
class List {
    Node<A> * _head;
public:
    List() : _head(nullptr) {}  // Nil
    List(A a, List<A> l)        // Cons
      : _head(new Node<A>(a, l))
    {}
};

Haskellでの2つの構成子NilConsが、よく似た引数(Nilは空、Consは値1つとリスト1つ)でオーバーロードされた2つのList構成子へと変換されたことに注目してほしい。このListクラスには、直和型の2つの成分を区別するためのタグは必要ない。その代わり、_headに特別なnullptr値を使ってNilを表現する。

だが、HaskellとC++の型の主な違いは、Haskellではデータ構造が不変であることだ。ある特定の構成子を使ってオブジェクトを作成する場合、オブジェクトはどの構成子が使われ、どの引数が渡されたかを永久に記憶する。したがって、Just "energy"として作成されたMaybeオブジェクトがNothingに変わることはない。同様に、空のリストは永久に空であり、3つの要素のリストは常に同じ3つの要素を持つことになる。

この不変性こそが構成を可逆的にする。オブジェクトがあれば、いつでも構成で使われた部品に分解できる。この分解はパターンマッチングで行われ、構成子をパターンとして再利用する。構成子の引数がある場合は、変数(またはその他のパターン)に置き換えられる。

Listデータ型には2つの構成子があるため、どんなListを分解するときもそれらの構成子に対応する2つのパターンを使う。1つは空であるNilリストにマッチし、もう1つはConsで構成されたリストにマッチする。たとえば、複数のListに対する単純な関数の定義は次のとおりだ。

maybeTail :: List a -> Maybe (List a)
maybeTail Nil = Nothing
maybeTail (Cons _ t) = Just t

maybeTailの定義の最初の部分は、Nil構成子をパターンとして使い、Nothingを返す。2番目の部分では、Cons構成子をパターンとして使っている。構成子の最初の引数には興味がないため、ワイルドカードに置き換わっている。Consの2番目の引数は、変数tに束縛される(厳密に言えば一度式に束縛されたら決して変化しないものの、変数と呼ぶことにする)。戻り値はJust tだ。こうして、Listの作成方法に応じて、節の1つに一致するようになった40。作成にConsが使われるときは、作成時に渡した2つの引数が取得される(最初の引数は破棄される)。

さらに複雑な直和型は、C++では多相クラス階層を使って実装されている。共通の祖先を持つクラスたちは、1つのバリアント型として理解でき、その中では仮想関数テーブルが隠しタグとして機能する。Haskellでは構成子に対するパターンマッチングおよびパターンごとに特化したコードで行っていることを、C++では仮想関数テーブルのポインターに基づいて仮想関数呼び出しをディスパッチすることで実現している。

C++で共用体が直和型として使われることはめったにない。含められるものに厳しい制限があるからだ。std::stringでさえ、コピーコンストラクターを持っているので、共用体に入れられない。

6.4 型の代数

直積型と直和型を別々に用いても有用なデータ構造をいろいろ定義できるが、真の強みはこの2つを組み合わせることで得られる。合成の力が再び発揮される時がきた。

これまでに分かったことをまとめておこう。型システムの下にある2つの可換モノイド構造を見た。中立元としてVoidを持つ直和型と、中立元として()というunit型を持つ直積型だ。それらを加法や乗法から類推したい。この類推では、Voidは0に対応し、()は1に対応する。

この類推をどこまで拡張できるか見てみよう。例として、0を掛けると0になるだろうか? 言い換えれば、1つの成分がVoidである直積型は、Voidと同型だろうか? たとえば、IntVoidのペアを作成できるだろうか?

ペアを作成するには2つの値が必要だ。整数なら簡単だが、型Voidには値がない。したがって、型(a, Void)は、すべての型aについて住人がいない――値を持たない――のでVoidと等価になる。言い換えれば、a×0=0a \times 0 = 0ということだ。

また別の、加算と乗算をつなぐものとして、分配則 (distributive property) がある:

a * (b + c) = a * b + a * c

これは直積型と直和型にも当てはまるだろうか? そう、当てはまる――いつものように同型を除いて。左辺は次の型に相当する:

(a, Either b c)

また、右辺は次の型に相当する:

Either (a, b) (a, c)

これらをある向きで変換する関数は次のとおりだ:

prodToSum :: (a, Either b c) -> Either (a, b) (a, c)
prodToSum (x, e) =
    case e of
      Left  y -> Left  (x, y)
      Right z -> Right (x, z)

また、その逆向きの変換は次のとおりだ:

sumToProd :: Either (a, b) (a, c) -> (a, Either b c)
sumToProd e =
    case e of
      Left  (x, y) -> (x, Left  y)
      Right (x, z) -> (x, Right z)

case of式は、関数の内部でパターンマッチングを行うために使われる。各パターンの後には矢印と、パターンが一致したときに評価される式が続く。たとえば、次の値を引数としてprodToSumを呼び出すとする。

prod1 :: (Int, Either String Float)
prod1 = (2, Left "Hi!")

case e of内のeLeft "Hi!"と等しくなる。これはパターンLeft yとマッチし、y"Hi!"を代入する。xはすでに2とマッチしているので、case of式の結果と関数全体は、期待どおりLeft (2, "Hi!")となる。

この2つの関数が互いの逆関数であることの証明は省くが、よく考えれば分かるだろう。これらは2つのデータ構造の内容を単に再パックしている。データは同じで、フォーマットが異なるだけだ。

数学者たちは、このような絡み合った2つのモノイドに半環 (semiring) という名前をつけている。これは完全な (ring) ではない。型の減算は定義できないからだ。そのため、半環は「n (negative) がない環 (ring)」をかけてリグ (rig) と呼ばれることがある。しかし、そのことを除けば、リグを形成する自然数などに関する命題を型に関する命題に変換することによる多くのメリットが得られる。興味深い項目を含む変換表を以下に示す:

数値
00 Void
11 ()
a+ba + b data Either a b = Left a | Right b
a×ba \times b (a, b)またはdata Pair a b = Pair a b
2=1+12 = 1 + 1 data Bool = True | False
1+a1 + a data Maybe a = Nothing | Just a

リスト型は、非常に興味深いことに、方程式の解として定義される。定義しようとしている型は等式の両辺に現れる:

data List a = Nil | Cons a (List a)

いつもの置換を行い、さらにList axに置き換えると、次の式が得られる:

x = 1 + a * x

型の減算や除算はできないので、これは従来の代数的方法では解けない。しかし、置換の連続なら試せる。つまり、ひたすら右辺のx(1 + a*x)に置き換えては分配則を使う。これによって次の列が得られる:

x = 1 + a*x
x = 1 + a*(1 + a*x) = 1 + a + a*a*x
x = 1 + a + a*a*(1 + a*x) = 1 + a + a*a + a*a*a*x
...
x = 1 + a + a*a + a*a*a + a*a*a*a...

最終的には積(組)の和が無限に続くことになった。これは次のように解釈できる。リストは空集合1か、単元集合aか、ペアa*aか、3つ組a*a*aか、などなど……。まさにそれがリストだ。つまり、aの列だ!

リストについては語るべきことがまだまだある。関手や不動点について学んだ後で、リストやその他の再帰的なデータ構造について再び説明する。

記号変数を使って方程式を解く――これぞ代数だ! それゆえ、これらの型は代数的データ型と呼ばれる。

最後に、型の代数の非常に重要な解釈について述べなければならない。abの2つの型の直積には、型aおよびbの両方の値が含まれている必要があることに注意してほしい。これは、(訳注:2つの型の直積が居住されているためには)両方の型が居住されている (inhabited) 必要があることを意味する41。一方、2つの型の直和には、型a またはbのいずれかの値が含まれるので、(訳注:2つの型の直和が居住されているためには)どちらかが居住されていれば十分だ。論理積 (logical and) と論理和 (logical or) も半環を形成し、型理論の言葉に写せる:

論理
falsefalse Void
truetrue ()
a||ba\,||\,b data Either a b = Left a | Right b
a&&ba\,\&\&\,b (a, b)

この類推はさらに深く、論理と型理論を結ぶカリー・ハワード同型の基礎となっている。それについては関数型について説明するときに再び取り上げる。

6.5 課題

  1. Maybe aEither () aの間の同型を示せ。

  2. 円と長方形の直和型をHaskellで定義する。

    data Shape = Circle Float
               | Rect Float Float

    面積を求めるための関数areaのようなShapeに作用する関数を定義したい場合は、これら2つの構成子に関するパターンマッチングにより行う:

    area :: Shape -> Float
    area (Circle r) = pi * r * r
    area (Rect d h) = d * h

    C++またはJavaでShapeをインタフェースとして実装し、CircleRectという2つのクラスを作成せよ。areaは仮想関数として実装せよ。

  3. 先ほどの例を続ける。Shapeの周の長さを求める新しい関数circは簡単に追加できる。Shapeの定義に触れる必要はない。

    circ :: Shape -> Float
    circ (Circle r) = 2.0 * pi * r
    circ (Rect d h) = 2.0 * (d + h)

    C++またはJavaの実装にcircを追加せよ。もとのコードのどの部分に触れる必要があったか?

  4. さらに続ける。新しい図形として正方形SquareShapeに追加し、必要なすべてを更新する。HaskellならびにC++およびJavaでは、コードのどこに触れる必要があったか42?(Haskellプログラマーでないとしても、変更箇所はごく自明なはずだ。)

  5. 型についてa+a=2×aa + a = 2 \times aが(同型を除いて)成り立つことを示せ。前掲の変換表によれば、22Boolに対応する。

7 関手

壊れたレコードのように聞こえるかもしれないが、関手についてこう述べておきたい:関手は非常に単純だが強力な概念だ。圏論はこのような単純だが強力な概念であふれている。関手は圏の間の写像だ。2つの圏𝐂\mathbf{C}𝐃\mathbf{D}について、関手FF𝐂\mathbf{C}の対象を𝐃\mathbf{D}の対象に写す。これは対象についての関数だ。aa𝐂\mathbf{C}内の対象である場合に、𝐃\mathbf{D}内の像をFaF aと(括弧なしで)書くことにする。しかし、圏は単に対象の集まりではない――対象とそれらを接続する射からなる。関手は射も写す――射についての関数だ。ただし、射を行きあたりばったりに写すわけではない――接続を維持して写す。 つまり、𝐂\mathbf{C}内の射ffが対象aaを対象bbに接続する場合: fabf \Colon a \to b 𝐃\mathbf{D}内のffの像FfF fは、aaの像をbbの像に接続する: FfFaFbF f \Colon F a \to F b (これは数学的記法とHaskellの記法を組み合わせたものであり、ここでは理にかなっているだろう。対象や射に関数を適用するときは括弧を使わないことにする。)

ご覧のとおり、関手は圏の構造を保存している。一方の圏で接続されているものは、もう一方の圏でも接続されている。しかし、圏の構造にはそれ以上の何かがある。それは射の合成だ。hhffggの合成: h=gfh = g \circ f である場合、FFによるhhの像がffggの像の合成になるようにしたい: Fh=FgFfF h = F g \circ F f

最後に、𝐂\mathbf{C}内のすべての恒等射が𝐃\mathbf{D}内の恒等射に写されるようにしたい: F𝐢𝐝a=𝐢𝐝FaF % \mathbf{id}_{a}% = % \mathbf{id}_{F a}% ここで、𝐢𝐝a% \mathbf{id}_{a}% は対象aaにおける恒等射を、𝐢𝐝Fa% \mathbf{id}_{F a}% FaF aにおける恒等射を表す。

これらの条件によって、関手は通常の関数よりもはるかに制約が厳しくなることに注意してほしい。関手は圏の構造を保存しなければならない。圏を、射のネットワークによって織りなされた対象の集まりと見なすなら、関手がこの織物に裂け目を入れることは許されない。対象を潰してまとめたり、複数の射を1つにくっつけたりすることはあるが、何かを引き裂くことは決してない。この引き裂きなしという制約は、微積分学において知られる連続性条件に似ている。この意味では、関手は「連続的」である(もっとも、関手にはさらに制約が厳しい連続性の概念が存在する)。関数と同じように、関手にも潰すものと埋め込むものがある。埋め込みの傾向がより顕著なのは、もとの圏が行き先の圏よりずっと小さいときだ。極端な場合、始域は、自明な単元圏 (singleton category) であり得る。すなわち、ただ1つの対象とただ1つの射(恒等射)を持つ圏だ。単元圏から他の圏への関手は、単にその圏内の対象を選択するだけだ。これは、単元集合からの射は終域内の要素を選択する、という特性と完全に類似している。最も潰す関手は定関手Δc\Delta_cと呼ばれる。それはもとの圏内のすべての対象を、行き先の圏内で選択された1つの対象ccに写す。また、もとの圏のすべての射を恒等射𝐢𝐝c% \mathbf{id}_{c}% に写す。まるでブラックホールのように働き、すべてを1つの特異点に圧縮する。この関手については極限と余極限について議論するときに詳しく見よう。

7.1 プログラミングにおける関手

地に足をつけてプログラミングについて話をしよう。我々には型と関数の圏がある。この圏をそれ自体に写す関手について話そう。そのような関手は自己関手 (endofunctor) と呼ばれる。型の圏での自己関手とは何だろうか? まず、それは型を型に写す。そのような写像の例はすでに見たが、おそらくそれとは気付かなかったのだろう。別の型によってパラメーター化されているような型の定義のことだ。いくつか例を見てみよう。

7.1.1 Maybe関手

Maybeの定義は型aから型Maybe aへの写像だ:

data Maybe a = Nothing | Just a

ここで重要な注意点がある。Maybe自体は型ではなく、型構成子だ。型に変換するには、IntBoolのような型引数を与える必要がある。引数のないMaybeは、型上の関数を表す。だが、Maybeは関手に変えられるだろうか? (これ以降、私がプログラミングの文脈で関手と言うとき、ほとんどの場合は自己関手を意味する。) 関手は、対象(ここでは型)を写すだけでなく、射(ここでは関数)も写す。aからbへの任意の関数:

f :: a -> b

についてMaybe aからMaybe bへの関数を生成したい。そのような関数を定義するには、Maybeの2つの構成子それぞれに対応する2つの場合を考慮する必要がある。Nothingの場合は単純で、Nothingを返すだけでよい。そして引数がJustの場合は、関数fをその中身に適用すればよい。したがって、Maybeの下でのfの像は次の関数だ:

f' :: Maybe a -> Maybe b
f' Nothing = Nothing
f' (Just x) = Just (f x)

(ところで、Haskellでは変数名でアポストロフィーを使えるため、いまのような場合にとても便利だ。) Haskellでは、関手における射の写像の部分はfmapと呼ばれる高階関数として実装されている。Maybeの場合、そのシグネチャーは次のとおりだ:

fmap :: (a -> b) -> (Maybe a -> Maybe b)

fmapは関数をリフトするという言い方がよく使われる。リフトされた関数はMaybe値に対して作用する。いつものように、カリー化のため、このシグネチャーには次の2通りの解釈がある。ひとつは、fmapは1引数関数であり、それ自体が関数である(a -> b)型の引数を取って、(Maybe a -> Maybe b)型の関数を返すという解釈だ。もうひとつは、2つの引数を取り、Maybe bを返すという解釈だ:

fmap :: (a -> b) -> Maybe a -> Maybe b

これまでの議論に基づいて、Maybeに対してfmapを実装する方法は次のとおりだ:

fmap _ Nothing = Nothing
fmap f (Just x) = Just (f x)

型構成子Maybeに関数fmapを合わせたものが関手をなすことを示すには、fmapが恒等射と合成を保存することを証明する必要がある。これらは「関手律」と呼ばれているが、単に圏の構造の保存を保証するだけのものだ。

7.1.2 等式による推論

関手律を証明するために、Haskellでの一般的な証明テクニックである等式による推論 (equational reasoning) を使う。これは、Haskellの関数が等式、つまり左辺が右辺に等しいものである、として定義されているという事実を利用している。左辺と右辺はいつでも入れ替えられる。ただし、名前の競合を避けるために変数名を変更する必要はあるかもしれない。これは、関数をインライン化するか、あるいは逆に式を関数にリファクタリングすることと考えてほしい。例として恒等関数を考えてみよう:

id x = x

たとえば、ある式の中にid yがあるなら、yに置き換えられる(インライン化)。さらに、(たとえばid (y + 2)のように)式にidが適用されているなら、(y + 2)のように式そのものに置き換えられる。そして、この置換は両方向に機能する。つまり、任意の式eid eで置き換えられる(リファクタリング)。関数がパターンマッチングによって定義されている場合は、各サブ定義を独立して使える。たとえば、上記のfmapの定義では、fmap f NothingNothingに置き換えることも、その逆を行うこともできる。これが実際にどのように機能するか見てみよう。まずは恒等射の保存から始めよう:

fmap id = id

NothingJustの2つの場合を考慮する必要がある。1つ目の場合は次のようになる(Haskell疑似コードを使って左辺を右辺に変換している):

  fmap id Nothing
= { fmapの定義 }
  Nothing
= { idの定義 }
  id Nothing

最後のステップでidの定義を逆向きに使ったことに注目してほしい。式Nothingid Nothingに置き換えた。実際には、このような証明は、真ん中の同じ式に辿り着くまで「ロウソクを両端から燃やす」ことで成される。今回については真ん中に残るのはNothingだ。2つ目の場合も簡単だ:

  fmap id (Just x)
= { fmapの定義 }
  Just (id x)
= { idの定義 }
  Just x
= { idの定義 }
  id (Just x)

では、fmapが合成を保存することを示そう:

fmap (g . f) = fmap g . fmap f

まずはNothingのケース:

  fmap (g . f) Nothing
= { fmapの定義 }
  Nothing
= { fmapの定義 }
  fmap g Nothing
= { fmapの定義 }
  fmap g (fmap f Nothing)

次はJustのケース:

  fmap (g . f) (Just x)
= { fmapの定義 }
  Just ((g . f) x)
= { 合成の定義 }
  Just (g (f x))
= { fmapの定義 }
  fmap g (Just (f x))
= { fmapの定義 }
  fmap g (fmap f (Just x))
= { 合成の定義 }
  (fmap g . fmap f) (Just x)

等式による推論はC++スタイルの副作用のある「関数」では使えないことは、強調しておく価値がある。次のコードを考えてみよう:

int square(int x) {
    return x * x;
}

int counter() {
    static int c = 0;
    return c++;
}

double y = square(counter());

等式による推論を使うと、squareをインライン展開して次のようにできる:

double y = counter() * counter();

明らかにこれは有効な変換ではなく、同じ結果は生成されない。それにもかかわらず、マクロとしてsquareを実装すると、C++コンパイラーは等式による推論を使おうとし、悲惨な結果になる。

7.1.3 Optional

関手はHaskellで簡単に表現できるが、総称プログラミングや高階関数をサポートする言語ならどれでも定義できる。MaybeのC++版であるテンプレート型optionalについて考えてみよう。以下に実装の概略を示す(実際の実装ははるかに複雑で、C++に特有の引数のさまざまな渡し方やコピーセマンティクスやリソース管理の問題を扱わなくてはならない)。

template<class T>
class optional {
    bool _isValid; // the tag
    T    _v;
public:
    optional()    : _isValid(false) {}         // Nothing
    optional(T x) : _isValid(true) , _v(x) {}  // Just
    bool isValid() const { return _isValid; }
    T val() const { return _v; }
};

このテンプレートは、関手の定義の一部である型の写像を提供する。これは任意の型Tを新しい型optional<T>に写す。関数に対するその関手の作用を定義しよう:

template<class A, class B>
std::function<optional<B>(optional<A>)>
fmap(std::function<B(A)> f)
{
    return [f](optional<A> opt) {
        if (!opt.isValid())
            return optional<B>{};
        else
            return optional<B>{ f(opt.val()) };
    };
}

これは高階関数で、引数として関数を受け取り、関数を返す。非カリー化版はこうなる:

template<class A, class B>
optional<B> fmap(std::function<B(A)> f, optional<A> opt) {
    if (!opt.isValid())
        return optional<B>{};
    else
        return optional<B>{ f(opt.val()) };
}

fmapoptionalのテンプレートメソッドにするという選択肢もある。このように選択肢に迷うことになるため、C++で関手パターンを抽象化するのは問題となる。関手は継承元となるインターフェースにすべきだろうか?(残念ながら、テンプレート仮想関数は作れない。) フリーテンプレート関数は、カリー化版と非カリー化版のどちらにすべきだろうか? 不足した型情報を、C++コンパイラーは正しく推論してくれるだろうか、それとも明示的に指定しなければならないだろうか? 入力関数fintからboolへの関数である状況を考えてみよう。コンパイラーにgの型が分かるだろうか:

auto g = fmap(f);

特に将来、複数の関手がfmapをオーバーロードするようになった場合は? (近いうちにさらに多くの関手について見てみよう。)

7.1.4 型クラス

では、Haskellは関手の抽象化にどのように対処するのだろうか? それには型クラスの機構を使う。型クラスは、共通のインターフェースをサポートする型の族を定義する。たとえば、等しさの検査をサポートする対象についてのクラスは次のように定義される:

class Eq a where
    (==) :: a -> a -> Bool

この定義は、型aの引数を2つ取りBoolを返す演算子(==)がサポートされる場合、型aはクラスEqであることを示している。特定の型がEqであることをHaskellに伝えたい場合は、その型をこのクラスのインスタンスであると宣言し、(==)の実装を提供する必要がある。たとえば、2次元空間における点を表す型Point(2つのFloatの直積型)が定義されているとする:

data Point = Pt Float Float

点の等しさは次のように定義できる:

instance Eq Point where
    (Pt x y) == (Pt x' y') = x == x' && y == y'

ここでは演算子(==)(いま定義しようとしているもの)を2つのパターン(Pt x y)(Pt x'y Pt x y)の間に中置した。関数の本体は、単一の等号の後に続く。いったんPointEqのインスタンスであると宣言されると、点同士の等しさを直接比較できるようになる。C++やJavaとは異なり、Pointを定義するときにEqクラス(またはインターフェース)を指定する必要はなく、クライアントコード内で後から指定できることに注目してほしい。また、型クラスは関数(および演算子)をオーバーロードするためのHaskellにおける唯一の機構でもある。型クラスはfmapを異なる関数(や演算子)についてオーバーロードするために必要となる。ただし、1つ複雑な点がある。関手は型として定義されるのではなく、型の写像、つまり型構成子として定義される。必要な型クラスは、Eqの場合のような型の族ではなく、型構成子の族だ。幸い、Haskellの型クラスは型だけでなく型構成子に対しても使える。以下にFunctorクラスの定義を示す。

class Functor f where
    fmap :: (a -> b) -> f a -> f b

これは、指定された型シグネチャーを持つ関数fmapが存在する場合、fFunctorだと規定している。小文字のfは型変数であり、型変数abと似ている。しかし、コンパイラーはそれが型ではなく型構成子を表していることを、その使われ方から推論できる。つまり、f af bのように他の型に作用していることから推論できる。したがって、Functorのインスタンスを宣言するときは型構成子を考える必要がある。例としてMaybeの場合を示す:

instance Functor Maybe where
    fmap _ Nothing = Nothing
    fmap f (Just x) = Just (f x)

ちなみに、Functorクラスや、Maybeを含む多くの単純なデータ型のインスタンス定義は、標準のPreludeライブラリの一部となっている。

7.1.5 C++での関手

C++でも同じアプローチを試せるだろうか? 型構成子は、optionalのようなテンプレートクラスに対応しているので、同様にfmapテンプレート・テンプレート引数 (template template parameter) でパラメーター化しよう。構文は次のとおりだ:

template<template<class> F, class A, class B>
F<B> fmap(std::function<B(A)>, F<A>);

このテンプレートをさまざまな関手に特殊化できるようにしたい。残念ながら、C++ではテンプレート関数の部分的な特殊化は禁止されている。そのため、次のような記述はできない:

template<class A, class B>
optional<B> fmap<optional>(std::function<B(A)> f, optional<A> opt)

代わりに関数のオーバーロードに頼る必要がある。結局、もとのカリー化されていないfmapの定義に戻る:

template<class A, class B>
optional<B> fmap(std::function<B(A)> f, optional<A> opt)
{
    if (!opt.isValid())
        return optional<B>{};
    else
        return optional<B>{ f(opt.val()) };
}

この定義は機能するが、どのオーバーロードが使われるかをfmapの2番目の引数が選択しているからにすぎない。より汎用的なfmapの定義を完全に無視している。

7.1.6 リスト関手

プログラミングにおける関手の役割についてある程度の直観を育むには、もっといろいろな例を見る必要がある。別の型によってパラメーター化される型はどれも関手の候補だ。総称コンテナーも格納する要素の型によってパラメーター化されているる。では、ごく単純なコンテナーであるリストを見てみよう:

data List a = Nil | Cons a (List a)

型構成子Listがある。これは、任意の型aから型List aへの写像だ。Listが関手だと示すには、関数のリフトを定義する必要がある。つまり、関数a -> bについて関数List a -> List bを定義する:

fmap :: (a -> b) -> (List a -> List b)

List aに作用する関数は、リストの2つのコンストラクターに対応する2つの場合を考慮する必要がある。Nilの場合は自明で、単にNilを返す。空のリストに対してできることはあまりない。Consの場合は、再帰を伴うため、ややトリッキーだ。そこで、少し前に戻って、何をしようとしているのか考えてみよう。aのリストと、abに変換する関数fがあり、bのリストを生成したい。自明なのは、fを使ってリストの各要素をaからbに変換することだ。(空でない)リストが先頭要素headと先頭を除いた残りのリストtailのConsとして定義されている場合、実際にはどうやるのだろうか? fをheadに適用し、リフト(fmap)されたfをtailに適用すればよい。これは再帰的な定義だ。リフトされたfをリフトされたfを用いて定義しているからだ:

fmap f (Cons x t) = Cons (f x) (fmap f t)

右辺のfmap fが、それに対して定義しようとしているリスト(訳注:Cons x t)よりも短いリストに適用されていることに注意してほしい。fmap fはそのリストのtailに適用されている。再帰的に処理されるにつれてリストが短くなるため、最終的には空リスト、つまりNilに到達することになる。しかし、先ほど決めたとおり、fmap fNilに作用するとNilを返すため、再帰は停止する。最終的な結果を得るために、Consコンストラクターを使って、新しいheadの(f x)と新しいtailの(fmap f t)を結合する。すべてをまとめると、リスト関手のインスタンス宣言はこうなる:

instance Functor List where
    fmap _ Nil = Nil
    fmap f (Cons x t) = Cons (f x) (fmap f t)

C++の方が慣れているなら、std::vectorの場合を考えてみてほしい。それは最も汎用的なC++コンテナーと見なせるかもしれない。fmapstd::vector用の実装は単にstd::transformの単純なカプセル化だ:

template<class A, class B>
std::vector<B> fmap(std::function<B(A)> f, std::vector<A> v)
{
    std::vector<B> w;
    std::transform( std::begin(v)
                  , std::end(v)
                  , std::back_inserter(w)
                  , f);
    return w;
}

これを使えば、たとえば次のように数列の要素を2乗できる43

std::vector<int> v{ 1, 2, 3, 4 };
auto w = fmap([](int i) { return i*i; }, v);
std::copy( std::begin(w)
         , std::end(w)
         , std::ostream_iterator(std::cout, ", "));

ほとんどのC++コンテナーは関手だと言える。それらはstd::transformに渡せるイテレーターを実装していて、std::transformfmapのより原始的な従兄弟だからだ。残念ながら、関手の単純さは、イテレーターや一時変数(上記のfmapの実装を参照)でいつも煩雑になるため失われてしまう。新しく提案されたC++のrangeライブラリによってrangeの関手的な性質がより顕著になったのは喜ばしい。

7.1.7 Reader関手

さて、直観が育ってきただろう。たとえば、関手をある種のコンテナーと見なせるようになった。では、一見非常に異なる例をお見せしよう。型aからの、aを返す関数の型への写像を考えてみよう。関数型についてはあまり詳しく述べていない(完全に圏論的な扱いはこれからだ)が、プログラマーならある程度理解している。Haskellでは、関数型はアロー型構成子(->)を使って構成され、引数の型と結果の型の2つの型を取る。すでに中置記法a -> bで見たことがあるが、括弧で括れば前置記法でも同様に使える:

(->) a b

通常の関数と同様に、複数の引数を取る型関数も部分適用できる。したがって、矢印に対して型引数を1つだけ与えた後もなお、もう1つの型引数が期待される。それが:

(->) a

が型構成子である理由だ。完全な型a -> bを生成するには、もう1つの型bが必要だ。今のところは、aによってパラメーター化された型構成子の族の全体を定義していると言える。これが関手の族でもあるかどうか見てみよう。2つの型パラメーターを扱うのは混乱を招くかもしれないので、名前を変更しておこう。前の関手の型定義に従って、引数の型をr 、結果の型をaと呼ぼう。そうすると、この型構成子は任意の型aを取り、それを型r -> aに写す。これが関手であることを示すために、関数a -> bを、r -> aを受け取ってr -> bを返す関数にリフトしたい。これらの型はabのそれぞれに型構成子(->) rを作用させて作られたものだ。この場合におけるfmapの型シグネチャーは次のとおりだ:

fmap :: (a -> b) -> (r -> a) -> (r -> b)

ここでパズルを解かなくてはならない:関数f :: a -> bと関数g :: r -> aが与えられるとき、関数r -> bを作成せよ。2つの関数を合成する方法は1つしかなく、その結果はまさに必要なものだ。fmapの実装は次のようになる:

instance Functor ((->) r) where
    fmap f g = f . g

うまくいった! 簡潔な表記が好みなら、さらに短く定義できる。そのためには、合成を前置記法で書き直せることと:

fmap f g = (.) f g

そのときの引数を省略できることに着目し、2つの関数の間の等式を得る:

fmap = (.)

型構成子 (->) rと上記のfmapの実装の組み合わせは、reader関手と呼ばれる。

7.2 コンテナーとしての関手

汎用コンテナーを定義できるか、オブジェクトをそれが含む値の型によってパラメーター化して定義できるプログラミング言語において、関手の例をいくつか見た。Reader関手は異端に思える。我々は関数をデータとは見なさないからだ。しかし、純粋関数はメモ化でき、関数の実行はテーブル参照に変えられるのをすでに見た。テーブルはデータだ。逆に、Haskellは遅延評価を採用しているため、リストのような従来のコンテナーは、実際には関数として実装されうる。たとえば、次のように簡潔に定義できる自然数の無限リストを考えてみよう:

nats :: [Integer]
nats = [1..]

最初の行では、一対の角括弧はHaskellの組み込みリスト用の型構成子だ。2行目では、リストを作成するために角括弧が使われている。明らかに、このような無限リストはメモリーに格納できない。コンパイラーはこれを、必要に応じてIntegerを生成する関数として実装する。Haskellは効果的に、データとコードの区別を曖昧にしている。リストは関数と見なせて、関数は引数を結果に写すテーブルと見なせる。後者は、関数の領域が有限かつ大きすぎない場合なら現実的だ。しかし、strlenをテーブル参照として実装するのは現実的でない。無限に多くの異なる文字列が存在するからだ。プログラマーとして、我々は無限大は好きではないが、圏論では朝食に無限大を食べるのを学ぶことになる。すべての文字列の集合であっても、過去・現在・未来の宇宙のすべての可能な状態の集まりであっても、対処できる! そこで、関手オブジェクト(自己関手によって生成された型のオブジェクト)はパラメーター化される型の値を含むと考えたい。それらの値が物理的にそこに存在しない場合でもだ。関手の一例はC++のstd::futureで、ある時点で値を含みうるが、必ず含む保証はない。また、その値にアクセスしたいとき、別スレッドの実行終了を待つためにブロックされることがある。別の例としてはHaskellのIOオブジェクトがあり、ユーザー入力を含んだり、画面に「Hello World!」と表示されているような未来版の宇宙を含んだりできる。この解釈によれば、関手オブジェクトとは、パラメーター化された型の値を含みうるものだ。あるいは、これらの値を生成するためのレシピも含みうる。値にアクセスできるかは全く気にしない――それは完全にオプショナルであり、関手の守備範囲外だ。関心があるのは、これらの値を関数を使って操作できるかだけだ。値にアクセスできるなら、操作の結果を確認できるはずだ。アクセスできないなら、操作が正しく合成され、恒等関数による操作が何も変更しないことに注意するだけでよい。関手オブジェクト内の値へのアクセスを全く気にしていないことを明示するために、引数aを完全に無視する型構成子を例に挙げよう:

data Const c a = Const c

Const型構成子はcaの2つの型を取る。アローコンストラクターで行ったように、部分適用で関手を作成しよう。Const型のデータ構成子(これもConstと呼ばれる)は型cの値を1つだけ取る。これはaには依存しない。この型構成子のfmapの型は次のようになる:

fmap :: (a -> b) -> Const c a -> Const c b

この関手は型引数を無視するので、fmapの実装はその関数引数を無視してよい――その関数は作用するものがない:

instance Functor (Const c) where
    fmap _ (Const v) = Const v

これはC++ではもう少し明確かもしれない(この言葉を口にするとは思わなかった!)。コンパイル時に決まる型引数と実行時に決まる値がよりはっきり区別されるからだ。

template<class C, class A>
struct Const {
    Const(C v) : _v(v) {}
    C _v;
};

fmapのC++実装も、関数の引数を無視し、Constの引数を値は変更せず実質的に再キャストする:

template<class C, class A, class B>
Const<C, B> fmap(std::function<B(A)> f, Const<C, A> c) {
    return Const<C, B>{c._v};
}

その奇妙さにもかかわらず、Const関手は多くの構成で重要な役割を果たしている。圏論では、これは先に述べたΔc\Delta_c関手の特殊なケースであり、ブラックホールの自己関手版だ。今後もっと詳しく知ることになるだろう。

7.3 関手の合成

圏の間の関手が合成できることは、集合の間の関数が合成できるのと同様だと考えれば納得するのは難しくない。2つの関手の合成は、対象に作用するときは、それぞれの対象の写像の合成にすぎず、射に作用するときも同様だ。2つの関手を飛び越えたあと、恒等射は恒等射となり、射の合成は射の合成となる。ただそれだけだ。特に、自己関手を合成するのは簡単だ。関数maybeTailを覚えているだろうか? ここではHaskellの組み込みのリスト実装を使って書き直そう:

maybeTail :: [a] -> Maybe [a]
maybeTail [] = Nothing
maybeTail (x:xs) = Just xs

Nilと呼んでいた空リストコンストラクターは、空の角括弧のペア[]に置き換えられる。Consコンストラクターは、中置演算子:(コロン)に置き換えられる。) maybeTailの結果は、Maybe[]という2つの関手の合成がaに作用するような型だ。これらの関数はそれぞれ独自版のfmapを備えているが、もし何らかの関数fを合成の内容、つまりMaybeリストに適用したい場合はどうなるだろう? 2層の関手を突破しなければならない。fmapを使えば外側のMaybeは突破できる。しかし、fはリストに対しては動かないので、Maybe内にfを単に送ることはできない。内側のリストを操作するには(fmap f)を送る必要がある。たとえば、整数のMaybeリストの要素を2乗するにはどうするか見てみよう:

square x = x * x

mis :: Maybe [Int]
mis = Just [1, 2, 3]

mis2 = fmap (fmap square) mis

コンパイラーは、型を分析した後、外側のfmapに対してはMaybeインスタンスからの実装を使い、内側のものに対してはリスト関手の実装を使う必要があることを理解する。上記のコードを次のように書き換えられるのは、すぐには自明に思えないかもしれない:

mis2 = (fmap . fmap) square mis

だが、fmapは引数が1つだけの関数と見なせることを思い出してほしい:

fmap :: (a -> b) -> (f a -> f b)

この例では、(fmap . fmap)内の2番目のfmapは引数として次のものを取る:

square :: Int -> Int

そして、次の型の関数を返す:

[Int] -> [Int]

最初のfmapがこの関数を受け取り、次の型の関数を返す:

Maybe [Int] -> Maybe [Int]

最後に、この関数はmisに適用される。したがって、2つの関手を合成すると、対応する2つのfmapを合成したfmapを持つ関手になる。圏論に話を戻すと、関手の合成が結合性を持つのはごく自明だ(対象の写像が結合性を持ち、射の写像も結合性を持つ)。また、すべての圏には自明な恒等関手がある。すなわち、どの対象もその対象自身へ写し、どの射もその射自身へ写すような関手だ。つまり、関手はある圏の射と全く同じ性質を持っている。しかし、それはどのような圏だろうか? 対象が圏であり射が関手である圏でなければならない。すなわち、圏の圏だ。ところが、すべての圏の圏はそれ自体を含まなければならず、すべての集合の集合を不可能にしたのと同じ種類の矛盾にぶつかることになる。しかし、𝐂𝐚𝐭\mathbf{Cat}と呼ばれる、すべての小さい圏の圏がある(𝐂𝐚𝐭\mathbf{Cat}自体は大きい圏なので、それ自体のメンバーにはなれない)。小さい圏とは、対象が集合よりも大きな何かではなく集合をなすような圏のことだ。圏論では、非可算無限集合であっても「小さい」と見なされることに注意してほしい。これらに言及しようと思ったのは、同じ構造が抽象化の多くのレベルで繰り返されているのを認識できることが、非常に驚くべきことだからだ。関手が圏を形成することについても後で説明する。

7.4 課題

  1. 次のように定義することで、Maybe型構成子を関手に変換できるか?

    fmap _ _ = Nothing

    これは両方の引数を無視する。(ヒント:関手律をチェックする。)

  2. Reader関手について関手律を証明せよ。ヒント:本当に単純だ。

  3. 2番目に好きな言語でReader関手を実装せよ(1番目はHaskell、それ一択だ)。

  4. リスト関手について関手律を証明せよ。その際は、fmap fを適用するリストのtailについて規則が真であると仮定せよ(言い換えると、帰納法を使用せよ)。

8 関手性

関手とは何かを学び、いくつかの例を見てきたのに続いて、小さい関手からより大きい関手を作る方法を見てみよう。特に興味深いのは、(圏内の対象間の写像に対応する)どの型構成子を、(射の間の写像を含む)関手に拡張できるのかという点だ。

8.1 双関手

関手は𝐂𝐚𝐭\mathbf{Cat}(圏の圏)の射であるため、射――典型的には関数――に関する直観の多くは関手にも当てはまる。たとえば、2つの引数を取る関数があるのと同じように、2つの引数を取る関手、すなわち双関手 (bifunctor) もある。対象については、1つは圏𝐂\mathbf{C}から、もう1つは圏𝐃\mathbf{D}からの対象からなるペアを、双関手はすべて圏𝐄\mathbf{E}の対象へと写す。これは単に、圏のデカルト積44𝐂×𝐃\mathbf{C} \times{} \mathbf{D}から𝐄\mathbf{E}への写像だと言っているだけであることに注意してほしい。

実に直截的だ。しかし、関手性によると、双関手は射も写さなければならない。ただし、今回は𝐂\mathbf{C}の射と𝐃\mathbf{D}の射のペアを𝐄\mathbf{E}の射に写す必要がある。

ここでも、射のペアは積圏𝐂×𝐃\mathbf{C} \times{} \mathbf{D}内の1つの射に相当する。圏のデカルト積における射は、ある対象のペアから別の対象のペアへ向かう射のペアと定義される45。これらの射のペアは、自明な方法で合成できる: (f,g)(f,g)=(ff,gg)(f, g) \circ (f', g') = (f \circ f', g \circ g') 合成は結合的であり、恒等射のペア (𝐢𝐝,𝐢𝐝)(\mathbf{id}, \mathbf{id}) を恒等射として持つ。圏のデカルト積は確かに圏だ。

双関手についてもっと簡単に考えたいなら、両方の引数を取る関手だと見なせばよい。そうすれば、関手律――結合性と恒等射の保存――を関手から双関手へ翻訳するのではなく、引数ごとに個別にチェックすれば十分だろう。ただし、一般には、個別の関手性は結合した関手性の証明としては不十分だ。結合した関手性が成り立たない(訳注:ような積を持つ)圏は前モノイダル圏 (premonoidal category) と呼ばれる。

Haskellで双関手を定義しよう。この場合の3つの圏はすべて同じで、Haskellの型の圏だ。双関手は2つの型引数を取る型構成子だ。型クラスBifunctorの定義をライブラリControl.Bifunctorから採ると、次のとおりだ:

class Bifunctor f where
    bimap :: (a -> c) -> (b -> d) -> f a b -> f c d
    bimap g h = first g . second h
    first :: (a -> c) -> f a b -> f c b
    first g = bimap g id
    second :: (b -> d) -> f a b -> f a d
    second = bimap id

bimap

型変数fは双関手を表す。どの型シグネチャーにおいてもfは常に2つの型引数に適用されている。最初の型シグネチャーは、bimapが2つの関数を同時に写すものであることを規定46している。その結果はリフトされた関数(f a b -> f c d)であり、双関手の型構成子fが生成する型に対して作用する。 bimapにはfirstsecondによるデフォルト実装がある(前述のとおり、これは常に機能するわけではなく、2つの写像が可換でない場合47にはfirst g . second hsecond h . first gは同じではない)。

他の2つの型シグネチャーfirstsecondは、2つのfmapであり、それぞれ最初と2番目の引数についてfの関手性を示す48

first second

型クラス定義は、この両方のデフォルト実装をbimapに基づいて提供する。

Bifunctorのインスタンスを宣言するときには、bimapを実装してデフォルトのfirstsecondを受け入れるか、firstsecondの両方を実装してデフォルトのbimapを受け入れるか、どちらかを選べる(もちろん3つすべてを実装してもよいが、それらが相互に正しく関連付けられているのを確認するのはプログラマーの責任になる)。

8.2 積と余積の双関手

双関手の重要な例として、圏論的な積、つまり普遍的構成によって定義される2つの対象の積がある。対象の任意のペアに対して積が存在する場合、これらの対象から積への写像は双関手的だ。これは一般に真であり、特にHaskellについてもそうだ。最も単純な直積型である、ペア型の型構成子に対するBifunctorインスタンスはこうなる:

instance Bifunctor (,) where
    bimap f g (x, y) = (f x, g y)

選択の余地はあまりない。bimapでは単に、最初の関数をペアの第一成分に適用し、2番目の関数を第二成分に適用するだけだ。その型から、このコードが何をするかは一目瞭然だ:

bimap :: (a -> c) -> (b -> d) -> (a, b) -> (c, d)

ここでの双関手の作用は、たとえば次のような型のペアを作ることだ:

(,) a b = (a, b)

双対性より、余積も、圏内の対象のすべてのペアに対して定義されているなら双関手だ。Haskellにおいては、型構成子EitherBifunctorのインスタンスであることが良い例だ:

instance Bifunctor Either where
    bimap f _ (Left x)  = Left (f x)
    bimap _ g (Right y) = Right (g y)

このコードも何をするか一目瞭然だ。

モノイダル圏について述べたときのことを覚えているだろうか? モノイダル圏は、対象に作用する二項演算子と単位対象とを定義する。𝐒𝐞𝐭\mathbf{Set}について、デカルト積に関して単元集合を単位元とするモノイダル圏だと述べた。また、非交和に関しても空集合を単位元とするモノイダル圏だ。しかし、モノイダル圏の要件の1つとして、二項演算子が双関手でなければならないことは述べていなかった。これは非常に重要な要件だ。射によって定義される圏の構造とモノイド的な積とを両立させたいからだ。我々はいま、モノイダル圏の完全な定義に一歩近づいている(そこに到達するまでには、まだ自然性について学ぶ必要がある)。

8.3 関手的代数的データ型

これまでに、パラメーター化されたデータ型が関手であるような例、つまりそれについてfmapを定義できるような型をいくつか見てきた。複雑なデータ型は単純なデータ型から構成される。特に、代数的データ型 (ADT) は、和と積を使って作成される。和と積が関手的なのは先ほど見た。関手が合成可能であることもすでに知っている。したがって、ADTの基本的な構成要素が関手的であると示せれば、パラメーター化されたADTも関手的だと分かる。

では、パラメーター化された代数的データ型の構成要素は何だろうか? まず、MaybeにおけるNothingListにおけるNilのように、関手の型パラメーターに依存しない要素がある。それらはConst関手と等価だ。Const関手は型パラメーターを無視することを思い出してほしい(いま述べているのは2番目の型パラメーターのことで、最初のパラメーターはそのままにされる)。

次に、MaybeにおけるJustのように、単に型パラメーター自体をそのままカプセル化する要素がある。これらは恒等関手と等価だ。以前、恒等関手について𝐂𝐚𝐭\mathbf{Cat}の恒等射として言及したが、Haskellでの定義は説明しなかった。それをここに示す:

data Identity a = Identity a
instance Functor Identity where
    fmap f (Identity x) = Identity (f x)

Identityは、型aの(不変な)値を常に1つだけ格納する、最も単純なコンテナーと見なせる。

代数的データ構造の他のすべては、これら2つのプリミティブから積と和を使って構成される。

この新しい知識に基づいて、Maybe型構成子を改めて見てみよう。

data Maybe a = Nothing | Just a

これは2つの型の和だ。和が関手的なのは知っている。1つ目の部分であるNothingは、Const ()aに対する作用として表せる(Constの最初の型パラメーターはunit型に設定されている――後でConstのさらに興味深い使い方を説明する)。2つ目の部分は、恒等関手の別名だ。Maybeは、同型を除いて、次のようにも定義できる:

type Maybe a = Either (Const () a) (Identity a)

したがって、Maybeは双関手Eitherを2つの関手Const ()Identityに合成したものだ。(Constは実際には双関手だが、ここでは常に部分適用で使う。)

関手の合成が関手であることはすでに見た。同じことが双関手にも当てはまるのは簡単に納得できる。必要なのは、2つの関手との双関手の合成が、射にどのように作用するかを理解することだけだ。2つの射が与えられたとき、まず片方の関手で片方の射を、もう1つの関手でもう1つの射をそれぞれ単にリフトする。次に、そのようにして得られるリフトされた射のペアを、双関手でリフトする。

この合成はHaskellで表現できる。双関手bf(2つの型を引数に取る双関手コンストラクターである型変数)と、2つの関手fugu(それぞれ1つの型変数を取る型構成子)と、2つの通常の型abとによってパラメーター化されるデータ型を定義しよう。fuaに適用し、gubに適用し、それからbfを結果の2つの型に適用する:

newtype BiComp bf fu gu a b = BiComp (bf (fu a) (gu b))

これが対象、つまり型の合成だ。Haskellで型構成子を型に適用する方法が、関数を引数に適用するのと同じであることに注目してほしい49。それらは同じ構文だ。

少し迷ったなら、BiCompを、EitherConst ()Identityabの順に適用してみてほしい。Maybe bのベアボーン版を復元できるだろう(aは無視される)。

bf自体がBifunctorでありfuおよびguFunctorである場合については、新しいデータ型BiCompaおよびbについて双関手だ。そのため、BiComp bf fu guBifunctorのインスタンスにするためには、bfに対するbimapの定義と、fuguに対するfmapの定義とが存在することをコンパイラーが認識している必要がある。Haskellでは、これはインスタンス宣言の前提条件として表現される。つまり、以下においてクラス制約のセットとして二重矢印の前に書かれた部分だ:

instance (Bifunctor bf, Functor fu, Functor gu) =>
  Bifunctor (BiComp bf fu gu) where
    bimap f1 f2 (BiComp x) = BiComp (bimap (fmap f1) (fmap f2) x)

BiCompに対するbimapの実装は、bfに対するbimapと、fuおよびguに対する2つのfmapによって与えられる。コンパイラーは、BiCompが使われるたびに、すべての型を自動的に推測し、適切なオーバーロード関数を選択する。

bimapの定義内のxは次のような型だ:

bf (fu a) (gu b)

これについて述べると長くなる。外側のbimapは外側のbfの層を貫通しており、2つのfmapはそれぞれfuguの下まで掘り下げている。f1f2の型が次の場合:

f1 :: a -> a'
f2 :: b -> b'

最終結果の型はbf (fu a') (gu b')となる:

bimap ::(fu a -> fu a') -> (gu b -> gu b')
  -> bf (fu a) (gu b) -> bf (fu a') (gu b')

ジグソーパズルが好きな人なら、この種の型操作で何時間も楽しめるだろう。

Maybeが関手だと証明する必要はなかったと分かった。この事実は、Maybeが2つの関手的プリミティブの和として構築できることから導かれたものだ。

鋭い読者ならこう尋ねるだろう:代数的データ型に対するFunctorインスタンスをそれほど機械的に導出できるのなら、コンパイラーによって自動化して実行できないのか? 実際、可能であり、行われている。ただし、特定のHaskell拡張を、ソースファイルの先頭に次の行を含めることで有効にする必要がある:

{-# LANGUAGE DeriveFunctor #-}

そして、データ構造にderiving Functorを追加する:

data Maybe a = Nothing | Just a
  deriving Functor

すると、対応するfmapが自動的に実装される。

代数的データ構造の規則性により、Functorだけでなく、前に述べたEq型クラスを含む、いくつかの型クラスのインスタンスを導出できる。コンパイラーに独自の型クラスのインスタンスを派生させるように教えるという選択肢もあるが、それはもう少し高度だ。もっとも、基本的な構成要素と和と積とに対する動作を提供し、残りの部分はコンパイラーに計算させるというアイデアは変わらない。

8.4 C++での関手

C++プログラマーなら、関手の実装に関しては、明らかに自分でやることになる。しかし、C++でもある種の代数的データ構造は見つかるはずだ。そのようなデータ構造を総称テンプレートにすれば、fmapを素早く実装できるだろう。

データ木構造を見てみよう。Haskellでは再帰的な直和型として定義される:

data Tree a = Leaf a | Node (Tree a) (Tree a)
    deriving Functor

前にも述べたように、C++で直和型を実装する方法の1つは、クラス階層を使うことだ。オブジェクト指向言語では、fmapを基底クラスFunctorの仮想関数として実装し、それをすべての派生クラスでオーバーライドするのが自然だ。残念ながらこれは不可能だ。なぜなら、fmapはテンプレートであり、それが作用する対象の型(thisポインター)だけでなく、それに適用された関数の戻り型によってもパラメーター化されているからだ。C++では仮想関数はテンプレート化できない。fmapを総称フリー関数として実装し、パターンマッチングをdynamic_castに置き換えよう。

基底クラスでは、動的キャストをサポートするために少なくとも1つの仮想関数を定義する必要があるため、デストラクターを仮想関数にする(いずれにしても良い考えだ50):

template<class T>
struct Tree {
    virtual ~Tree() {}
};

Leafは単なる変装したIdentity関手だ:

template<class T>
struct Leaf : public Tree<T> {
    T _label;
    Leaf(T l) : _label(l) {}
};

Nodeは直積型だ:

template<class T>
struct Node : public Tree<T> {
    Tree<T> * _left;
    Tree<T> * _right;
    Node(Tree<T> * l, Tree<T> * r) : _left(l), _right(r) {}
};

fmapを実装するときには、Treeの型で動的ディスパッチを利用する。Leafの場合はIdentity版のfmapを適用し、Nodeの場合は2つのTree関手に合成された双関手のように扱う。C++プログラマーとしては、これらの用語を使ってコードを分析することに慣れていないかもしれないが、圏論的な考え方の良いエクササイズになる。

template<class A, class B>
Tree<B> * fmap(std::function<B(A)> f, Tree<A> * t)
{
    Leaf<A> * pl = dynamic_cast <Leaf<A>*>(t);
    if (pl)
        return new Leaf<B>(f (pl->_label));
    Node<A> * pn = dynamic_cast<Node<A>*>(t);
    if (pn)
        return new Node<B>( fmap<A>(f, pn->_left)
                          , fmap<A>(f, pn->_right));
    return nullptr;
}

簡単のため、メモリーとリソース管理の問題は無視することにしたが、本番コードではおそらくスマートポインターを使うことになるだろう(unique_ptrshared_ptrかはポリシーによる)。

fmapのHaskell実装と比較してほしい:

instance Functor Tree where
    fmap f (Leaf a) = Leaf (f a)
    fmap f (Node t t') = Node (fmap f t) (fmap f t')

この実装は、コンパイラーによって自動的に導出することもできる。

8.5 Writer関手

前にクライスリ圏を説明したとき、戻ってくることを約束した。クライスリ圏の射は、Writerデータ構造を返す「装飾された」関数として表現されていた。

type Writer a = (a, String)

すでに述べたように、装飾は自己関手と何らかの関係にある。そして実際、Writerの型構成子は、aについて関手的だ。単純な直積型なので、fmapを実装する必要すらない。

しかし、クライスリ圏と関手の間には、どのような一般的な関係があるのだろうか? クライスリ圏は圏なので、合成と恒等射が定義されている。合成はfish演算子によって与えられるのを思い出してほしい:

(>=>) :: (a -> Writer b) -> (b -> Writer c) -> (a -> Writer c)
m1 >=> m2 = \x ->
    let (y, s1) = m1 x
        (z, s2) = m2 y
    in (z, s1 ++ s2)

また、恒等射はreturnという関数によって計算される:

return :: a -> Writer a
return x = (x, "")

この2つの関数の型を十分に長く見れば(つまり、十分に長く見れば)、それらを組み合わせて、fmapとして機能するための適切な型シグネチャーを持つ関数を作成する方法が見つかる。それは次のようになる:

fmap f = id >=> (\x -> return (f x))

この例では、fish演算子は2つの関数を組み合わせている。1つはおなじみのidであり、もう1つは、引数にfを適用した結果にreturnを適用するラムダ関数だ。理解するのが最も難しいのはidを使うところだろう。Fish演算子の引数となる関数は、「通常の」型を受け取って装飾された型を返す関数ではないのだろうか? 実は、そんなことはない。a -> Writer baが「普通の」型でなければならないとは誰も言っていない。これは型変数なので何でも良く、特に、Writer bのような装飾された型でも構わない。

そういうわけで、このidWriter aを受け取り、Writer aに変換する。Fish演算子はaの値を取り出し、xとしてラムダに渡す。ここで、fはそれをbに変換し、returnはそれを装飾してWriter bにする。これらすべてをまとめると、Writer aを受け取り、Writer bを返す関数が完成する。これは、fmapが生成するはずのものと全く同じだ。

注目してほしいのは、この議論が非常に汎用的であることだ。つまり、Writerは任意の型構成子で置き換えられる。Fish演算子とreturnをサポートしていれば、fmapも定義できる。したがってクライスリ圏での装飾は常に関手となる。(ただし、すべての関手からクライスリ圏を構成できるわけではない。)

先ほど定義したfmapは、derivating Functorを使ってコンパイラーによって導出されたfmapと同じものだろうかと疑問に思うかもしれない。とても興味深いことに、同じものだ。そうなっているのは、Haskellが多相関数を実装する方法に由来する。それはパラメトリック多相と呼ばれ、いわゆるtheorems for free51の根源となっている。それらの定理の1つは、ある型構成子に対して恒等射を保存するようなfmapの実装があるなら、それは一意である、と述べている52

8.6 共変関手と反変関手

Writer関手を振り返り終えたのでReader関手に戻ろう。それは部分適用されたアロー(関数)型構成子に基づいていた:

(->) r

これは型シノニム53で書き直せる:

type Reader r a = r -> a

これに対するFunctorインスタンスは、これまで見てきたように、次のようになる54

instance Functor (Reader r) where
    fmap f g = f . g

だが、ペア型構成子やEither型構成子と同じく、この関数型構成子は2つの型引数を取る。ペアやEitherは両方の引数について関手的であり、すなわち双関手だった。この関数のコンストラクターも双関手だろうか?

最初の引数で関手的にすることを試みてみよう。Readerとは引数が反転している、よく似た型シノニムから始めることにする:

type Op r a = a -> r

今回は、戻り値の型rを固定し、引数の型aを変化させる。次のような型シグネチャーを持つfmapを実装するために、何らかの方法で型を一致させられるか見てみよう:

fmap :: (a -> b) -> (a -> r) -> (b -> r)

aを取りそれぞれbrを返す2つの関数だけでは、bを取りrを返す関数を作成する方法が全くない。代わりに1つ目の関数を逆にして、bを受け取りaを返すようにできれば、状況は違ってくるだろう。任意の関数について逆関数が存在するわけではないが、反対圏に行くことはできる。

要点を再掲すると、圏𝐂\mathbf{C}ごとに反対圏𝐂𝑜𝑝\mathbf{C}^\mathit{op}が存在する。これは𝐂\mathbf{C}と同じ対象を持つ圏だが、すべての射が逆になっている。

𝐂𝑜𝑝\mathbf{C}^\mathit{op}から他の圏𝐃\mathbf{D}への関手を考えてみよう: F𝐂𝑜𝑝𝐃F \Colon \mathbf{C}^\mathit{op} \to \mathbf{D} このような関手は𝐂𝑜𝑝\mathbf{C}^\mathit{op}の射f𝑜𝑝abf^\mathit{op} \Colon a \to b𝐃\mathbf{D}の射Ff𝑜𝑝FaFbF f^\mathit{op} \Colon F a \to F bに写す。しかし、射f𝑜𝑝f^\mathit{op}は、もとの圏𝐂\mathbf{C}のある射fbaf \Colon b \to aと密かに対応している。反転に注意してほしい。

さて、FFは通常の関手だが、FFに基づいて定義できる別の写像があり、それは関手ではない。それをGGと呼ぼう。このGG𝐂\mathbf{C}から𝐃\mathbf{D}への写像だ。対象はFFと同じ方法で写すが、射は逆にする。つまり、𝐂\mathbf{C}の射fbaf \Colon b \to aを取り、それをまず逆向きの射f𝑜𝑝abf^\mathit{op} \Colon a \to bに写し、次に関手FFを使ってFf𝑜𝑝FaFbF f^\mathit{op} \Colon F a \to F bを得る。

FaF aGaG aと同じで、FbF bGbG bと同じであることを考慮すると、この旅の全体は Gf(ba)(GaGb)G f \Colon (b \to a) \to (G a \to G b) のように記述できる。 これは「ひねりのある関手」だ。このように射の方向を反転させる圏の写像は、反変 (contravariant) 関手と呼ばれる。反変関手は、反対圏からの通常の関手にすぎないことに注意してほしい。その一方で、これまでに学んできた通常の関手は共変 (covariant) 関手と呼ばれる。

反変

以下の型クラスは、Haskellにおける反変関手(実のところ、反変自己関手)を定義している:

class Contravariant f where
    contramap :: (b -> a) -> (f a -> f b)

前述の型構成子Opはこのインスタンスだ:

instance Contravariant (Op r) where
    -- (b -> a) -> Op r a -> Op r b
    contramap f g = g . f

関数fが、Opの内容、つまり関数gより(つまり右側)に挿入されることに注意してほしい。

Opに対するcontramapの定義は、単に引数を反転した関数合成演算子であることに注意すれば、さらに簡潔にできるだろう。引数を反転するためにはflipという専用の関数がある:

flip :: (a -> b -> c) -> (b -> a -> c)
flip f y x = f x y

これを使うと次のようになる:

contramap = flip (.)

8.7 プロ関手

これまで見てきたように、アロー型演算子->は、最初の引数では反変、2番目の引数では共変だ。このような怪物に名前はあるのだろうか? 行き先の圏が𝐒𝐞𝐭\mathbf{Set}の場合、この怪物はプロ関手 (profunctor) と呼ばれる。反変関手は反対圏からの共変関手と等価なので、プロ関手は次のように定義される: 𝐂𝑜𝑝×𝐃𝐒𝐞𝐭\mathbf{C}^\mathit{op} \times \mathbf{D} \to \mathbf{Set} Haskellの型は一次近似的には集合と見なせるので、引数が2つの型構成子pで、1番目の引数について反関手的で、2番目について関手的であるようなものをProfunctorと呼んでしまうことにする。Data.Profunctorライブラリから適切な型クラスを引用しよう:

class Profunctor p where
  dimap :: (a -> b) -> (c -> d) -> p b c -> p a d
  dimap f g = lmap f . rmap g
  lmap :: (a -> b) -> p b c -> p a c
  lmap f = dimap f id
  rmap :: (b -> c) -> p a b -> p a c
  rmap = dimap id

これら3つの関数すべてにデフォルト実装がある。Bifunctorと同じように、Profunctorのインスタンスを宣言するとき、dimapを実装してデフォルトのlmaprmapを受け入れるか、lmaprmapの両方を実装してデフォルトのdimapを受け入れるか、どちらかを選択できる。

dimap

ここで、関数アロー演算子はProfunctorのインスタンスだと確認しておく:

instance Profunctor (->) where
  dimap ab cd bc = cd . bc . ab
  lmap = flip (.)
  rmap = (.)

プロ関手の応用としてはHaskellのlensライブラリがある55。また、プロ関手についてはエンドとコエンドについて述べるときに再び触れる。

8.8 Hom関手

上記の例は、対象aabbのペアを取ってそれらの間の射の集合を割り当てる写像、すなわちhom集合𝐂(a,b)\mathbf{C}(a, b) が関手であるという、より一般的な命題を反映している。それは積圏𝐂𝑜𝑝×𝐂\mathbf{C}^\mathit{op}\times{}\mathbf{C}から集合の圏𝐒𝐞𝐭\mathbf{Set}への関手だ。

射に対する作用を定義してみよう。𝐂𝑜𝑝×𝐂\mathbf{C}^\mathit{op}\times{}\mathbf{C}の射は、𝐂\mathbf{C}における射のペアだ: faagbb \begin{gathered} f \Colon a' \to a \\ g \Colon b \to b' \end{gathered} このペアをリフトしたものは集合𝐂(a,b)\mathbf{C}(a, b) から集合𝐂(a,b)\mathbf{C}(a', b') への射(関数)になる必要がある。𝐂(a,b)\mathbf{C}(a, b) の任意の要素hhaaからbbへの射)を選んで、それを: ghfg \circ h \circ f に写せば𝐂(a,b)\mathbf{C}(a', b') の要素となる。

ご覧のとおり、hom関手はプロ関手の特殊なケースだ。

8.9 課題

  1. データ型:

    data Pair a b = Pair a b

    が双関手であることを示せ。追加の課題として、Bifunctorの3つのメソッド(訳注:bimapfirstsecond)の実装をすべて与え、それらの定義が、デフォルトの実装(それが適用できる場合には)と整合していることを等式変形によって示せ。

  2. Maybeの標準的な定義と次の脱糖との同型性を示せ:

    type Maybe' a = Either (Const () a) (Identity a)

    ヒント:2つの実装の間に2つの写像を定義する。追加の課題として、等式変形により、それらが互いに逆であることを示せ。

  3. 別のデータ構造を試してみよう。私はこれをPreListと呼んでいる。Listの前身だからだ。Listにおける再帰が型パラメーターbに置き換わっている。

    data PreList a b = Nil | Cons a b

    PreListをそれ自体に再帰的に適用すれば、Listの以前の定義を復元できる(具体的にどうするかは不動点について述べるときに説明する)。

    PreListBifunctorのインスタンスであることを示せ。

  4. 次のデータ型がaおよびbについての双関手を定義していることを示せ:

    data K2 c a b = K2 c

    data Fst a b = Fst a

    data Snd a b = Snd b

    追加の課題として、Conor McBrideの論文Clowns to the Left of me, Jokers to the Right56と照らし合わせて解答を確認せよ。

  5. Haskell以外の言語で双関手を定義せよ。その言語でbimapを総称ペアに対して実装せよ。

  6. std::mapは、2つのテンプレート引数KeyTについて双関手またはプロ関手と見なすべきか? そう見なせるようにするには、このデータ型をどう再設計すればよいだろう?

9 関数型

ここまでは、関数型の意味について言い繕ってきた。関数型は他の型とは異なる。

たとえばIntegerを考えてみる。これは単に整数の集合だ。Boolは2要素の集合だ。しかし、関数型aba \to bはそれ以上のもので、対象aabbの間の射の集合だ。任意の圏における2つの対象間の射の集合はhom集合と呼ばれる。たまたま𝐒𝐞𝐭\mathbf{Set}圏では、どのhom集合もそれ自体がまさにその圏の対象だ。結局それは集合だからだ。

Set圏のなかのhom集合は単なる集合だ。

同じことは、hom集合が圏の外にあるような他の圏には言えない。それらは外部 (external) hom集合と呼ばれることもある。

この圏Cのhom集合は外部集合だ。

𝐒𝐞𝐭\mathbf{Set}圏の自己参照的な性質によって、関数型は特殊なものになっている。しかし、少なくともいくつかの圏では、hom集合を表す対象を構成する方法がある。そういった対象は内部 (internal) hom集合と呼ばれる。

9.1 普遍的構成

関数型が集合であることをいったん忘れて、ゼロから関数型を、というより一般的に言えば内部hom集合を構成してみよう。いつものように、ここでは𝐒𝐞𝐭\mathbf{Set}圏からヒントを得る。ただし、集合の性質に一切頼らないように気を付けることで、その構成が他の圏でも自動的に機能するようにする。

関数型は、引数の型と結果の型の関係性から複合型と見なせる。複合型の構成のうち、対象間の関係性が関わっているものについてはすでに見た。積と余積に相当する型を定義するのに普遍的構成を使った。同じトリックを使って関数型を定義できる。そのためには3つの対象が関わるパターンが必要になる。すなわち、構成しようとする関数型自体と、その関数型における引数の型と結果の型だ。

これら3つの型を結びつける自明なパターンは関数適用 (function application) あるいは評価 (evaluation) と呼ばれる。ある関数型の候補をzzと呼ぼう(𝐒𝐞𝐭\mathbf{Set}圏でない場合、これは他の対象と同様の単なる対象であることに注意)。また、引数の型をaaと呼ぼう(これは対象だ)。関数適用はこれらのペアを結果の型bb(つまり対象)に写す。対象が3つあり、そのうち(引数の型と結果の型を表す)2つはすでに決まっている。

関数適用は写像だ。どうすればその写像をパターンに組み込めるだろう? 対象の内部を見ることが許されていたなら、関数ffzzの要素)と引数xxaaの要素)をペアにした後でそれをfxf xffxxへ適用したものであり、bbの要素)に写せただろう。

Setでは、関数の集合zから関数fを選び、集合(型)aから引数xを選べる。その結果、集合(型)bの中の要素f xが得られる。

しかし、個々のペア (f,x)(f, x) を扱う代わりに、関数型zzと引数の型aa全体について述べることもできる。積z×az\times{}aは対象であり、関数適用を表す射として、その対象からbbへの射ggを選べる。𝐒𝐞𝐭\mathbf{Set}では、ggは任意のペア (f,x)(f, x)fxf xに写す関数となるだろう。

したがって、関数適用のパターンは、2つの対象zzaaの積が別の対象bbに射ggで接続されている、というものになる。

普遍的構成の出発点である対象と射のパターン

このパターンは、普遍的構成を使って関数型を一意に特定できるほど十分に具体的だろうか? すべての圏でそうだとは言えない。しかし、我々が関心を持っている圏ではそうだ。さらに、別の疑問もある:積を先に定義することなく関数対象 (function object) を定義できるだろうか? 積が全く存在しない圏や、対象のすべてのペアに対しては積が存在しない圏もある。答えはノーだ。直積型がなければ関数型はない。これについては、後で冪について述べるときに再び説明する。

普遍的構成をおさらいしよう。まず対象と射のパターンから始める。これは粗い検索であり、通常はヒットするものが多すぎる。特に、𝐒𝐞𝐭\mathbf{Set}では、ほとんどすべてのものがすべてに接続されている。任意の対象zzを選んでaaとの積を作れば、そこからbbへの関数を作れる(bbが空集合の場合を除く)。

そこで秘密兵器の出番となる。順位付けだ。それを行うには通常、候補となる対象たちの間に(構成を何らかの形で分解する)一意な写像が存在することが必要となる。今回の場合においては、z×az \times aからbbへの射ggを伴うzzがそれ自身の関数適用gg'を伴う別のzz'よりも優れているとして選別するのは、関数適用gg'が関数適用ggを通じて分解するような、zz'からzzへの一意な写像hhが存在する場合、かつその場合に限る(ヒント:この文は図を見ながら読むこと)。

関数対象の候補間の順位付けの確立

ここでトリッキーな点がある。それが、この特定の普遍的構成の説明をいままで延期した主な理由だ。射hzzh \Colon z'\to zが与えられたときに、zz'zzをそれぞれaaと掛けた図式を閉じたい。だが、与えられているのはzz'からzzへの写像hhなのに、本当に必要なのはz×az'\times aからz×az\times aへの写像だ。そしていま、積の関手性についてすでに第8章2節で議論したので、そのやり方は分かっている。積自体が関手(正確には自己双関手)なので射のペアをリフトできる。言い換えると、対象の積だけでなく、射の積も定義できる。

z×az' \times aの2番目の要素には触れていないので、射のペア (h,𝐢𝐝)(h, \mathbf{id}) をリフトしよう。ここで、𝐢𝐝\mathbf{id}aaについての恒等射だ。

そして、ある関数適用ggで別の関数適用gg'を分解する(gggg'からくくり出す)とこうなる: g=g(h×𝐢𝐝)g' = g \circ (h \times \mathbf{id}) ここで鍵となるのは、射に対する積の作用だ。

普遍的構成の第3の部分は、普遍的に最も優れた対象を選ぶことだ。その対象をaba \Rightarrow bと呼ぶことにしよう(これは1つの対象に対する記号的な名前だと考えてほしい。Haskellの型クラス制約と混同しないように。後で別の命名方法について議論する)。この対象は独自の関数適用を伴う。それは (ab)×a(a \Rightarrow b) \times aからbbへの射だ。これを𝑒𝑣𝑎𝑙\mathit{eval}と呼ぶことにしよう。対象aba \Rightarrow bは、他のどの関数対象の候補も、その関数適用の射gg𝑒𝑣𝑎𝑙\mathit{eval}を通じて分解するようなかたちでその対象へと一意に写せる場合、最も優れている。我々の順位付けにおいてこの対象は他のどの対象よりも優れている。

普遍的な関数対象の定義。これは上記と同じ図式だが、対象a \Rightarrow bは普遍だ。

形式的には:

aaからbbへの関数対象は、対象aba \Rightarrow bに射 𝑒𝑣𝑎𝑙((ab)×a)b\mathit{eval} \Colon ((a \Rightarrow b) \times a) \to b を伴ったものであり、他の任意の対象zzに射 gz×abg\Colon z \times a \to b を伴ったものに対して、一意な射 hz(ab)h \Colon z \to (a \Rightarrow b) が存在してgg𝑒𝑣𝑎𝑙\mathit{eval}を通じて分解する: g=𝑒𝑣𝑎𝑙(h×𝐢𝐝)g=\mathit{eval} \circ (h \times \mathbf{id})

当然、このような対象aba \Rightarrow bが、与えられた圏内の任意の対象aabbについて存在する保証はない。しかし、𝐒𝐞𝐭\mathbf{Set}では常に存在する。さらに、𝐒𝐞𝐭\mathbf{Set}では、この対象はhom集合𝐒𝐞𝐭(a,b)\mathbf{Set}(a, b) と同型だ。

そのため、Haskellにおいて我々は、関数型a -> bを圏論の関数対象aba \Rightarrow bとして解釈する。

9.2 カリー化

関数対象の全候補を見てみよう。ただし今回は、射ggを2つの引数zzaaの関数として考えてみよう: gz×abg \Colon z \times a \to b 積からの射であることと2引数関数であることは極めて近い。特に、𝐒𝐞𝐭\mathbf{Set}ではggは値のペアを取る関数であり、そのペアの片方は集合zzから、もう片方は集合aaからの値だ。

一方、普遍性 (universal property) は、このようなggごとに、zzを関数対象aba \Rightarrow bに写す一意な射hhが存在することを示している: hz(ab)h \Colon z \to (a \Rightarrow b) 𝐒𝐞𝐭\mathbf{Set}において、これは単に関数hhzz型の引数を1つ受け取ってaaからbbへの関数を返すことを意味する。これによってhhは高階関数になる。したがって、普遍的構成は、2引数関数と、関数を返す1引数関数との間に1対1の対応を確立する。この対応はカリー化 (currying) と呼ばれ、hhggをカリー化したものと言える。

これは1対1の対応だ。任意のggに対し一意なhhが存在し、任意のhhに対して次の式を使って引数2つの関数ggを常に再生成できるからだ: g=𝑒𝑣𝑎𝑙(h×𝐢𝐝)g = \mathit{eval} \circ (h \times \mathbf{id}) 関数gghh非カリー化 (uncurrying) したものと言える。

カリー化はHaskellの構文に本質的に組み込まれている。関数を返す関数:

a -> (b -> c)

は2引数関数と見なされることが多い。実際に我々は括弧を外した型をそのように読む:

a -> b -> c

この解釈は、複数の引数を取る関数を定義する方法において明確だ。たとえば:

catstr :: String -> String -> String
catstr s s' = s ++ s'

と同じ関数を、関数を返す1引数関数、すなわちラムダとして記述できる:

catstr' s = \s' -> s ++ s'

これら2つの定義は等価であり、どちらも1つの引数だけに部分適用でき、次のような1引数関数が得られる:

greet :: String -> String
greet = catstr “Hello

厳密に言えば、2引数関数というのはペア(直積型)を取る関数のことだ:

(a, b) -> c

2つの表現の間の変換は自明であり、それを行う2つの(高階)関数は、もちろん、curryuncurryと呼ばれる:

curry :: ((a, b) -> c) -> (a -> b -> c)
curry f a b = f (a, b)

および

uncurry :: (a -> b -> c) -> ((a, b) -> c)
uncurry f (a, b) = f a b

curryは、関数対象の普遍的構成のfactorizerであることに注目してほしい。これは、次の形に書き直した場合に特に顕著だ57

factorizer :: ((a, b) -> c) -> (a -> (b -> c))
factorizer g = \a -> (\b -> g (a, b))

(備忘録:factorizerは候補から分解関数を生成する。)

C++のような非関数型言語でもカリー化は可能だが、簡単ではない。C++の複数引数関数は、Haskellでの組を取る関数に対応すると見なせる(ただし、さらに混乱を招くことに、C++では明示的なstd::tupleを取る関数や、可変長引数関数や、初期化子リストを取る関数も定義できる)。

テンプレートstd::bindを使えばC++でも関数を部分適用できる。たとえば、文字列2つを取る関数があるとする:

std::string catstr(std::string s1, std::string s2) {
    return s1 + s2;
}

文字列1つを取る関数は次のように定義できる:

using namespace std::placeholders;

auto greet = std::bind(catstr, "Hello ", _1);
std::cout << greet("Haskell Curry");

ScalaはC++やJavaよりも関数型寄りで、中間的な立場に立っている。定義したい関数が部分適用されると予想されるときは、複数引数のリストを使って定義する:

def catstr(s1: String)(s2: String) = s1 + s2

当然、これにはある程度の先見の明や予測がライブラリの作者に求められる。

9.3

数学の文献では、関数対象、すなわち2つの対象aabbの間の内部hom対象を (exponential) と呼んでbab^{a}と記すことが多い。引数の型が指数に含まれていることに注目してほしい。この記法は一見奇妙に思えるかもしれないが、関数と積の関係を考えると完全に理にかなっている。内部hom対象の普遍的構成で積を使わなければならないことはすでに見たが、つながりはそれよりも深い。

関数と積の密接なつながりは、有限型の間の関数を考えるときに最もよく見える。有限型とは、BoolChar、さらにはIntDoubleなど、有限個の値しか持たない型のことだ。それらの間の関数は、少なくとも原理的には、完全にメモ化したり、データ構造に変換してルックアップしたりできる。そしてこれが、射である関数と、対象である関数型との同値性の本質だ。

たとえば、Boolを取る(純粋)関数は、Falseに対応する値とTrueに対応する値のペアによって完全に決まる。Boolから、たとえばIntへのすべての可能な関数の集合はIntのすべてのペアの集合だ。これは積Int ×\times{} Intと同じ集合であり、記法を少し創意工夫するならInt2^{2}とも書ける。

別の例として、256種類の値を含むC++の型charを見てみよう(HaskellのCharはUnicodeを使っているのでもっと多い)58。C++標準ライブラリの一部には、実装に通常はルックアップが使われる関数がいくつかある。isupperisspaceのような関数はテーブルを使って実装される。テーブルは256個のブール値の組と等価だ。組は直積型であるため、256個のブーリアンの積bool × bool × bool × ... × boolを扱っていることになる。算術で学んだとおり、積を繰り返したものが冪だった。boolを256(つまりchar)回「掛ける」と、boolchar乗、つまりbool𝐜𝐡𝐚𝐫^\mathbf{char}になる。

boolの256個の組として定義される型には何通りの値が含まれているだろう? ちょうど22562^{256}通りだ。これはまた、charからboolへの関数の種類の数でもあり、各関数は一意な256要素の組に対応する。同様に、boolからcharへの関数の数は2562256^{2}と計算できる。以下同様だ。このような場合には、関数型の冪記法が完全に理にかなっている。

intdoubleを取る関数を完全にメモ化したいとは思わないだろう。だが、関数とデータ型の間には、常に実用的だとは限らないにしても、同値性がある。(有限型だけでなく)リスト・文字列・木などの無限型もある。それらの型を取る関数の積極的 (eager) なメモ化には、無限のストレージが必要になるだろう。しかし、Haskellは遅延評価言語であるため、遅延評価された(無限)データ構造と関数の境界は曖昧だ。この関数とデータの双対性は、Haskellの関数型と圏論の冪対象との同一視を説明している。冪対象の方がデータという概念によく対応している。

9.4 デカルト閉圏

私はこれ以降も型や関数のモデルとして集合の圏を使うが、同じ目的に使えるような、圏のより大きな族があることは言及する価値がある。それらはデカルト閉 (Cartesian closed) 圏と呼ばれ、𝐒𝐞𝐭\mathbf{Set}はそのような圏の一例だ。

デカルト閉圏は以下のものを含む必要がある:

  1. 終対象
  2. 任意の対象のペアの積
  3. 対象の任意のペアの冪

冪を(無限に繰り返される可能性がある)積の反復と見なすなら、デカルト閉圏は任意のアリティ59の積をサポートするものと見なせる。特に、終対象は0個の対象の積、すなわち対象の0乗と見なせる。

コンピューター科学の観点から興味深いのは、デカルト閉圏が単純型付きラムダ計算(simply typed lambda calculus)のモデルを提供し、あらゆる型付きプログラミング言語の基礎を形成していることだ。

終対象と積には、始対象と余積という双対がある。それら2つも含むデカルト閉圏では、余積に対し積を分配できる: a×(b+c)=a×b+a×c(b+c)×a=b×a+c×a \begin{aligned} a\times{}(b + c) = a\times{}b + a\times{}c \\ (b + c)\times{}a = b\times{}a + c\times{}a \end{aligned} そのような圏は双デカルト閉 (bicartesian closed) 圏と呼ばれる。次の節では、𝐒𝐞𝐭\mathbf{Set}に代表される双デカルト閉圏の興味深い特性について説明する。

9.5 冪と代数的データ型

関数型を冪として解釈すると、代数的データ型の体系に非常にうまく適合する。高校で習う代数における数0と1や和・積・冪に関する基本的な恒等式はどれも、双デカルト閉圏においてそれぞれ始対象と終対象や余積・積・冪を考えれば、ほとんどそのまま使えることが分かる。それらについて、証明する手段(随伴や米田の補題など)はまだ得ていないが、価値ある直観の源としてここに挙げておこう。

9.5.1 0乗

a0=1a^{0} = 1 圏論的解釈では、0を始対象に、1を終対象に、等しさを同型に置き換える。冪は内部hom対象だ。ここに示した冪は、始対象から任意の対象aaへの射の集合を表している。始対象の定義によれば、そのような射は1つだけ存在するので、hom集合𝐂(0,a)\mathbf{C}(0, a) は単元集合となる。単元集合は𝐒𝐞𝐭\mathbf{Set}内の終対象なので、この恒等式は𝐒𝐞𝐭\mathbf{Set}内で自明に成り立つ。言いたいことは、これがどんな双デカルト閉圏でも成り立つということだ。

Haskellでは、0をVoidで、1をunit型()で、冪を関数型でそれぞれ置き換える。これは、Voidから任意の型aへの関数の集合と、単元集合であるunit型が等価だと主張している。言い換えると、関数Void -> aは1つしかない。この関数は以前見た。absurdと呼ばれる関数だ。

2つの理由から、これは少しトリッキーだ。1つは、Haskellは実際には住人がいない型を持たないことだ――すべての型には「終わりのない計算の結果」、つまりボトムが含まれている。第2の理由は、absurdのすべての実装は等価であるということだ60。なぜなら、それらが何をしようと、誰も実行できないからだ。absurdに渡せる値はない。(そして、終わりのない計算を渡せたとしても、決して値は返らない!)

9.5.2 1の冪

1a=11^{a} = 1 この恒等式を𝐒𝐞𝐭\mathbf{Set}で解釈すると、終対象の定義である「どの対象にも終対象への一意な射がある」を言い換えている。一般に、aaから終対象への内部hom対象は、終対象そのものと同型だ。

Haskellでは、型aからunit型への関数は1つしかない61。この関数は以前にも見たことがある。unitと呼ばれる関数だ。()に部分適用されたconst関数とも見なせる。

9.5.3 1乗

a1=aa^{1} = a これは、終対象からの射が対象aの「要素」を選ぶのに利用できるという観点を言い換えたものだ。このような射の集合は対象そのものと同型だ。𝐒𝐞𝐭\mathbf{Set}とHaskellでは、集合aの要素と、それらの要素を選択する関数() -> aとの間に同型が成り立つ。

9.5.4 和による冪

ab+c=ab×aca^{b+c} = a^{b} \times a^{c} これは、圏論では2つの対象の余積による冪が2つの冪の積と同型だと明示している。Haskellでは、この代数的恒等式に非常に実用的な解釈がある。これは、2つの型の直和型を取る関数が、それら個々の型を取る関数のペアと等価だと示している。直和型を取る関数を定義するときに使う場合分け (case analysis) そのものだ。case式で1つの関数定義を記述する代わりに、通常はそれを2つ(またはそれ以上)の関数に分割して、それぞれの構成子を別々に処理する。たとえば、直和型 (Either Int Double) を取る関数を考えよう:

f :: Either Int Double -> String

これは、それぞれIntDoubleを取る2つの関数のペアとして定義できる。

f (Left n)  = if n < 0 then "Negative int" else "Positive int"
f (Right x) = if x < 0.0 then "Negative double" else "Positive double"

ここで、nIntで、xDoubleだ。

9.5.5 冪の冪

(ab)c=ab×c(a^{b})^{c} = a^{b \times c} これは単にカリー化を純粋に冪対象の観点で表現したものだ。関数を返す関数は、積を引数に取る関数(2引数の関数)と等価だ。

9.5.6 積の冪

(a×b)c=ac×bc(a \times b)^{c} = a^{c} \times b^{c} Haskellでは、ペアを返す関数は、それぞれがペアの1つの要素を生成する関数のペアと等価だ。

これらの高校数学の単純な代数的恒等式が、このように圏論に持ち上げられ、関数プログラミングで実用的な応用があるのは、実に驚くべきことだ。

9.6 カリー・ハワード同型

論理と代数的データ型の対応についてはすでに述べた。Void型とunit型()は、偽と真に対応する。直積型と直和型は、論理積\wedge (AND) と論理和\vee (OR) に対応する。この図式では、先ほど定義した関数型は論理包含\Rightarrowに対応する。つまり、型a -> bは「aならばb」と読める。

カリー・ハワード同型 (Curry-Howard isomorphism) によれば、すべての型は命題として解釈できる。命題とは、真または偽に定まる言明や判断だ。そのような命題は、型が居住されているならば真とされ、そうでなければ偽とされる。論理包含では、対応する関数型が居住されている(その型の関数が存在する)ならば真となる。したがって、関数の実装は定理の証明になる。プログラムを書くのは定理を証明するのと等価だ。いくつか例を見てみよう。

関数対象の定義で導入した関数evalを取り上げよう。シグネチャーは次のとおりだ:

eval :: ((a -> b), a) -> b

これは関数とその引数のペアを取り、適切な型の結果を生成する。つまり、次の射をHaskellで実装したものだ: 𝑒𝑣𝑎𝑙(ab)×ab\mathit{eval} \Colon (a \Rightarrow b) \times a \to b この射は関数型aba \Rightarrow b(すなわち冪対象bab^{a})を定義する。この型を、カリー・ハワード同型を使って論理の命題に変換しよう。 ((ab)a)b((a \Rightarrow b) \wedge a) \Rightarrow b この命題の読み方はこうだ:aaならばbbが真であり、かつaaが真ならば、bbは必ず真である。これは完全に直観に適っていて、古代からmodus ponensとして知られていた。次の関数を実装することで、この定理を証明できる:

eval :: ((a -> b), a) -> b
eval (f, x) = f x

aを取りbを返す関数fと、型aの具体的な値xとのペアがあれば、fxに適用するだけで型bの具体的な値を得られる。この関数を実装することで、型((a -> b), a) -> bが居住されていることが示せた。modus ponensは我々の論理では真だ。

では、あからさまに間違っている命題ではどうだろうか? 例:aaまたはbbが真ならばaaは真でなければならない: abaa \vee b \Rightarrow a これは明らかに間違っている。なぜなら、aaが偽でbbが真の場合が反例となるからだ。

この命題をカリー・ハワード同型を使って関数型に写すと、次のようになる:

Either a b -> a

いくらやってみても、この関数は実装できない――Rightの形の値で呼び出された場合、型aの値は生成できない(ここでは純粋関数について説明していることを思い出してほしい)。

最終的に、absurd関数が意味するものに辿り着く:

absurd :: Void -> a

Voidが偽に変換されることを考えると、次のようになる: 𝑓𝑎𝑙𝑠𝑒a\mathit{false} \Rightarrow a 虚偽からは何でも導ける (ex falso quodlibet)。Haskellにおけるこの命題(関数)の証明(実装)として可能なものを以下に1つ示す。

absurd (Void a) = absurd a

ここで、Voidは次のように定義される:

newtype Void = Void Void

いつものように、型Voidはトリッキーだ。この定義により、値を構成するには値を提供する必要があるため、値を構成できない62。したがって、この関数absurdは決して呼び出せない。

いずれも興味深い例だが、カリー・ハワード同型に実用面はあるのだろうか? おそらく日々のプログラミングではないだろう。しかし、AgdaやCoqのようなプログラミング言語では、定理を証明するためにカリー・ハワード同型が利用されている。

コンピューターは数学者の仕事を助けている63だけでなく、数学の基礎そのものに革命をもたらしている。この分野の注目の最新のホットな研究テーマはホモトピー型理論 (Homotopy Type Theory, HoTT) と呼ばれ、型理論の派生物だ。ブーリアン、整数、積と余積、関数型などでいっぱいだ。そして、疑念を払拭するかのように、その理論はCoqとAgdaで定式化されようとしている。コンピューターは世界にさまざまな形で革命を起こしている。

9.7 参考文献

  1. Ralf Hinze, Daniel W. H. James, Reason Isomorphically!64. この論文には、この章で述べた圏論におけるすべての高校数学の代数的恒等式の証明が含まれている。

10 自然変換

関手については圏と圏との間で構造を保存する写像としてすでに述べた。

関手はある圏を別の圏に「埋め込む」。複数のものが1つに潰されることはあっても、接続が切断されることはない。関手は、それによって1つの圏を別の圏の中でモデル化していると捉えられる。もとの圏は、行き先の圏の一部である構造物のモデル、あるいは青写真として機能する。

1つの圏を別の圏に埋め込む方法はいろいろある。それらは等価なこともあれば、大きく異なることもある。もとの圏全体を1つの対象に潰すこともあれば、すべての対象を異なる対象に写し、すべての射を異なる射に写すこともある。同じ青写真を実現する方法はいろいろある。自然変換は、それらの実現方法を比較するのに役立つ。自然変換は関手間の写像であり、その関手的性質を保存する特別な写像だ。

𝐂\mathbf{C}𝐃\mathbf{D}の間にFFGGという2つの関手があるとする。𝐂\mathbf{C}内の1つの対象aaだけに注目すると、それが2つの対象FaF aGaG aに写されているのが分かる。したがって、関手の写像はFaF aGaG aに写すべきだ。

FaF aGaG aは同じ圏𝐃\mathbf{D}内の対象であることに注意してほしい。同じ圏内の対象間の写像は、圏の特性に反するものであってはならない。対象同士の間に取ってつけたような接続を作成したくはない。したがって、既存の接続、つまり射を使用するのは自然だ。自然変換は射たちを選択することであり、対象aaごとにFaF aからGaG aへの射を1つ選択する。その自然変換をα\alphaと呼ぶとき、この射はα\alphaaaにおける成分 (component) と呼ばれ、αa\alpha_aと表記される: αaFaGa\alpha_a \Colon F a \to G a aa𝐂\mathbf{C}の対象であり、αa\alpha_a𝐃\mathbf{D}の射であることに注意してほしい。

あるaaについて、𝐃\mathbf{D}内のFaF aGaG aの間に射がない場合、FFGGの間に自然変換はない。

もちろん、これは話の半分にすぎない。なぜなら、関手は対象だけでなく射も写すからだ。では自然変換はこれらの射の写像をどうするのだろうか? 実は射の写像は固定されていて、FFGGの間の自然変換ではFfF fGfG fに変換されなければならない。さらに、2つの関手による射の写像は、それに適合する自然変換を定義する際の選択肢を大幅に制限する。𝐂\mathbf{C}の2つの対象aabbの間に射ffがあるとする。それは𝐃\mathbf{D}内の2つの射FfF fGfG fに写される: FfFaFbGfGaGb \begin{gathered} F f \Colon F a \to F b \\ G f \Colon G a \to G b \end{gathered} 自然変換α\alpha𝐃\mathbf{D}内の追加の射を2つ与え、図式を完成させる: αaFaGaαbFbGb \begin{gathered} \alpha_a \Colon F a \to G a \\ \alpha_b \Colon F b \to G b \end{gathered}

いま、FaF aからGbG bへの移行には2つの方法がある。これらが等しいことを確認するには、任意のffに対して成り立つような自然性条件 (naturality condition) を課す必要がある: Gfαa=αbFfG f \circ \alpha_a = \alpha_b \circ F f 自然性条件はかなり厳しい要件だ。たとえば、射FfF fが可逆である場合、自然性はαa\alpha_aに基づいてαb\alpha_bを決定する。それはffに沿ってαa\alpha_aトランスポート (transport) する: αb=(Gf)αa(Ff)1\alpha_b = (G f) \circ \alpha_a \circ (F f)^{-1}

2つの対象間に2つ以上の可逆な射がある場合、トランスポートはすべて一致する必要がある65。もっとも、一般には射は可逆ではない。しかし、2つの関手間に自然変換が存在するとは全く保証されていないことは理解できる。したがって、自然変換によって関連する関手が多いか少ないかは、それらが作用する圏の構造について多くのことを教えてくれるだろう。極限と米田の補題について話すときに、いくつかの例を見ることになる。

自然変換を成分ごとに見ると、対象を射に写していると言える。自然性条件があるので、射を四角い可換図式に写しているとも言えるだろう――𝐂\mathbf{C}のすべての射に対して𝐃\mathbf{D}内に四角い自然性の可換図式が1つ存在する。

自然変換のこの性質は、多くの圏論的構成で非常に便利になる。それらは可換図式を伴うことが多いからだ。関手を適切に選択すれば、それらの可換性条件の多くは自然性条件に変換できる。その例は、極限・余極限・随伴に辿り着いたときに見ることになるだろう。

最後に、自然変換を使って関手の同型を定義できる。2つの関手が自然に同型であると言うのは、全く同じだと言っているようなものだ。自然同型 (natural isomorphism) は、成分がすべて同型(可逆な射)である自然変換として定義される。

10.1 多相関数

プログラミングにおける関手(より正確には自己関手)の役割についてはすでに述べた。それらは型を型に写すような型構成子に対応する。また、そういった関手は関数を関数に写すが、この写像は高階関数fmap(あるいはC++におけるtransformthenに類するもの)によって実装される。

つまり、自然変換を構成するのに、まずは1つの対象、ここでは型aから始めることにする。関手Fはそれを型FaF aに写すとし、別の関手GGaG aに写すとする。aにおける自然変換alphaの成分は、FaF aからGaG aへの関数だ。疑似Haskellではこうなる: αaFaGa\alpha_a \Colon F a \to G a 自然変換は、すべての型aに対して定義される多相関数だ:

alpha :: forall a . F a -> G a

このforall aはHaskellでは必須ではない(そして実際にそれを明記するには言語拡張ExplicitForAllを有効にする必要がある)。通常は次のように記述する:

alpha :: F a -> G a

これは実際にはaによってパラメーター化された関数の族であることに注意してほしい。これもまた、Haskellの構文の簡潔さを示す一例だ。C++では同様の構文はもう少し冗長になる:

template<class A> G<A> alpha(F<A>);

Haskellの多相 (polymorphic) 関数とC++の総称 (generic) 関数との間にはさらに大きな違いがあり、そういった関数を実装したり型検査したりする方法はそれを反映している。Haskellでは、多相関数はすべての型に対して一様に定義されなければならない。1つの解決法があらゆる型にわたって機能する必要がある。これはパラメトリック多相 (parametric polymorphism) と呼ばれる。

一方、C++はデフォルトでアドホック多相66 (ad hoc polymorphism) をサポートしており、テンプレートはすべての型に対して明確に定義されている必要はない。ある型に対してテンプレートが機能するかどうかは、型パラメーターが具体的な型で置換されるインスタンス化時に決定される。型検査が遅延されるため、残念ながら、理解し難いエラーメッセージにつながることがよくある。

C++には関数のオーバーロードとテンプレートの特殊化のための機構もあり、同じ関数で異なる型に対して異なる定義を行える。Haskellでは、この機能は型クラスと型族 (type family) によって提供されている。

Haskellのパラメトリック多相は予想外の結果をもたらす。次のような型の多相関数:

alpha :: F a -> G a

を関手FGについて考えると、それらはすべて自動的に自然性条件を満たす。圏論の表記法で表すとこうなる(ffは関数fabf \Colon a \to bだ): Gfαa=αbFfG f \circ \alpha_a = \alpha_b \circ F f Haskellでは、関手fの射Gに対する作用はfmapを使って実装される。最初に疑似Haskellで、明示的な型注釈を付けて書こう:

fmap fG . alphaa = alphab . fmapF f

型推論によって、これらの型注釈は不要になり、次の等式が成り立つ:

fmap f . alpha = alpha . fmap f

これはまだ本物のHaskellではない――実際のHaskellでは関数の等しさをコードで表現できない――が、この恒等式はプログラマーによる等式による推論やコンパイラーによる最適化の実装に使える。

Haskellで自然性条件が自動で成り立つ理由は “theorems for free” に関係している。Haskellで自然変換を定義するのに使われるパラメトリック多相は、実装に非常に強い制限を課す――すべての型に対して1つの解決法という制限だ。それらの制限は、そのような関数に関する等式定理に変換される。関手を変換する関数の場合、free theoremは自然性条件だ67

関手のHaskellにおける捉え方の1つとして以前述べたのは、一般化されたコンテナーと見なすことだった。その類推を続けると、自然変換は、あるコンテナーの中身を別のコンテナーに再パッケージするレシピと見なせる。要素自体に触れることはなく、要素を変更したり新しい要素を作成したりはしない。それら(の一部)を、ときには複数回、新しいコンテナーにコピーするだけだ。

自然性条件は、最初にfmapを適用して要素を変更してから後で再パッケージするのか、それとも最初に再パッケージしてからfmapを独自に実装して新しいコンテナー内の要素を変更するのか、は問題ではないという宣言になる。再パッケージ化とfmapの2つの作用は直交している。「一方は卵を動かし、もう一方は茹でる。」68

Haskellでの自然変換の例をいくつか見てみよう。1つ目はリスト関手とMaybe関手の間の自然変換だ。これはリストが空でない場合に先頭要素を返す:

safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:xs) = Just x

これはaについて多相な関数だ。aがどんな型であっても制限なく機能するので、パラメトリック多相の一例だと言える。したがって、これは2つの関手の間の自然変換だ。だが、我々自身が納得するために、自然性条件を証明してみよう。

fmap f . safeHead = safeHead . fmap f

考慮すべきケースは2つある。1つは空リストだ:

fmap f (safeHead []) = fmap f Nothing = Nothing
safeHead (fmap f []) = safeHead [] = Nothing

もう1つは空でないリストだ:

fmap f (safeHead (x:xs)) = fmap f (Just x) = Just (f x)
safeHead (fmap f (x:xs)) = safeHead (f x : fmap f xs) = Just (f x)

ここで、fmapの実装として以下の2つを利用した。リスト用:

fmap f [] = []
fmap f (x:xs) = f x : fmap f xs

Maybe用:

fmap f Nothing = Nothing
fmap f (Just x) = Just (f x)

興味深いのは、関手の1つが自明なConst関手であるケースだ。Const関手からの自然変換は戻り値の型について多相な関数のように見え、Const関手への自然変換は引数の型について多相な関数のように見える。

たとえば、lengthはリスト関手からConst Int関手への自然変換と見なせる。

length :: [a] -> Const Int a
length [] = Const 0
length (x:xs) = Const (1 + unConst (length xs))

ここで、unConstConstコンストラクターを引き剥がすのに使われる:

unConst :: Const c a -> c
unConst (Const x) = x

当然、実用上はlengthは次のように定義される:

length :: [a] -> Int

これは実質的に、自然変換であるという事実を隠してしまう。

Const関手からのパラメトリック多相関数を見つけるのは少し難しい。無から値を生成する必要があるからだ。最善を尽くすと、次のようになる:

scam :: Const Int a -> Maybe a
scam (Const x) = Nothing

すでに見たもう1つの一般的な関手で、後ほど米田の補題で重要な役を果たすのは、Reader関手だ。定義をnewtypeに書き直そう:

newtype Reader e a = Reader (e -> a)

これは2つの型によってパラメーター化されているが、(共変)関手的なのは2番目の型だけだ:

instance Functor (Reader e) where
    fmap f (Reader g) = Reader (\x -> f (g x))

すべての型eについて、Reader eから他の任意の関手fへの自然変換の族を定義できる。この族の元が常にf eの要素と1対1に対応している(米田の補題)ことを後で説明する。

たとえば、1つの要素()を持つ、いくぶんつまらないunit型()について考えてみよう。関手Reader ()は、任意の型aを取り、それを関数型() -> aに写す。これらは単に、集合aから1つの要素を選択するすべての関数だ。それらはaにある要素と同じ数だけある。ここで、この関手からMaybe関手への自然変換を考えてみよう:

alpha :: Reader () a -> Maybe a

あるのはdumbobviousの2つだけだ:

dumb (Reader _) = Nothing

および

obvious (Reader g) = Just (g ())

(gについてできるのはunit値()に適用することだけだ。)

そして実際、米田の補題によって予言されるように、これらはMaybe ()型の2つの要素、NothingJust ()に対応している。米田の補題には後で戻ってこよう――ここではほんの少し垣間見ただけだ。

10.2 自然性を越えて

2つの関手の間のパラメトリック多相関数(Const関手という特殊な例を含む)は常に自然変換だ。標準的な代数的データ型はすべて関手なので、これらの型の間の多相関数はすべて自然変換だ。

また、関数型も自由に使えて、それらは戻り値の型について関手的だ。それらを使って(Reader関手のような)関手を構成し、高階関数である自然変換を定義できる。

しかし、関数型は引数について共変ではない。それらは反変だ。当然、反変関手は反対圏からの共変関手と等価だ。2つの反変関手の間の多相関数は、反対圏からHaskellの型への関手を処理する点を除けば、圏論的には自然変換だ。

反変関手の例を前に見たのを覚えているだろう:

newtype Op r a = Op (a -> r)

この関手はaについて反変だ:

instance Contravariant (Op r) where
    contramap f (Op g) = Op (g . f)

たとえば、Op BoolからOp Stringへの多相関数を書ける:

predToStr (Op f) = Op (\x -> if f x then "T" else "F")

ただし、2つの関手は共変ではないので、これは𝐇𝐚𝐬𝐤\mathbf{Hask}での自然変換ではない。しかし、どちらも反変なので、「反対」の自然性条件は満たしている:

contramap f . predToStr = predToStr . contramap f

contramapのシグネチャーは次のとおりなので、関数ffmapで使うのとは逆方向でなければならないことに注意してほしい:

contramap :: (b -> a) -> (Op Bool a -> Op Bool b)

共変にしろ反変にしろ、関手ではない型構成子は存在するのだろうか? 次のような例が挙げられる:

a -> a

同じ型aが負(反変)と正(共変)の両方の位置で使われているので、これは関手ではない。この型にはfmapcontramapを実装できない。したがって、次のシグネチャーを持つ関数:

(a -> a) -> f a

は自然変換にはなれない。ここで、fは任意の関手だ。興味深いことに、このような場合を扱う一般化された自然変換として、対角自然変換と呼ばれるものがある。これについてはエンドについて議論するときに説明しよう。

10.3 関手圏

関手間の写像――自然変換――ができたいま、関手が圏を形成するのかを問うのは自然だ。そして、実際に形成する、というのが答だ! 𝐂\mathbf{C}𝐃\mathbf{D}の圏のペアごとに、関手の圏が1つある。この圏の対象は𝐂\mathbf{C}から𝐃\mathbf{D}への関手であり、射はこれらの関手間の自然変換だ。

2つの自然変換の合成を定義する必要があるが、それは非常に簡単だ。自然変換の成分は射であり、射を合成する方法は分かっている。

実際に、関手FFからGGへの自然変換α\alphaを取ろう。対象aaにおけるその成分はこのような射だ: αaFaGa\alpha_a \Colon F a \to G a α\alphaを、関手GGからHHへの自然変換であるβ\betaと合成したい。aaでのβ\betaの成分は次の射だ: βaGaHa\beta_a \Colon G a \to H a これらの射は合成可能であり、その合成は次のような別の射だ: βaαaFaHa\beta_a \circ \alpha_a \Colon F a \to H a この射を自然変換βα\beta \cdot \alpha――自然変換β\betaを自然変換α\alphaの後に合成したもの――の成分として使う。 (βα)a=βaαa(\beta \cdot \alpha)_a = \beta_a \cdot \alpha_a

図式を(長く)見ていると、この合成の結果は本当にFFからHHへの自然変換だと確信できる。 Hf(βα)a=(βα)bFfH f \circ (\beta \cdot \alpha)_a = (\beta \cdot \alpha)_b \circ F f

自然変形の合成は結合的だ。それらの構成要素は通常の射であり、合成に関して結合的だからだ。

最後に、各関手FFについて恒等自然変換1F1_Fがあり、その成分は恒等射だ: 𝐢𝐝FaFaFa\mathbf{id}_{F a} \Colon F a \to F a 以上より、確かに関手は圏を形成している。

記法について述べておこう。ソーンダーズ・マックレーンにしたがって、私は先ほど述べたような自然変換の合成にドットを使う。問題は、自然変換を合成する方法が2つあることだ。ここで使ったものは垂直合成 (vertical composition) と呼ばれている。通常は関手を上下に積んだ図式で説明されるからだ。垂直合成は関手圏を定義するうえで重要だ。水平合成についてもすぐに説明する。

𝐂\mathbf{C}𝐃\mathbf{D}の間の関手圏は、𝐅𝐮𝐧(𝐂,𝐃)\mathbf{Fun(C, D)}または[𝐂,𝐃]\mathbf{[C, D]}、場合によっては𝐃𝐂\mathbf{D^C}と書かれる。この最後の表記法は、関手圏自体が他の圏では関数対象(冪)と見なせることを示唆している。だが、本当にそうだろうか?

これまでに構成してきた抽象化の階層を見てみよう。最初は対象と射の集まりとしての圏から始めた。圏自体(厳密に言えば、対象たちが集合を形成する小さい圏)は、それ自体がより高いレベルの圏𝐂𝐚𝐭\mathbf{Cat}における対象だ。その圏における射は関手だ。𝐂𝐚𝐭\mathbf{Cat}におけるhom集合は関手の集合だ。たとえば、𝐂𝐚𝐭(𝐂,𝐃)\mathbf{Cat(C, D)}は、2つの圏𝐂\mathbf{C}𝐃\mathbf{D}の間の関手の集合だ。

関手圏[𝐂,𝐃]\mathbf{[C, D]}も、2つの圏の間の関手の集合に、射としての自然変換を加えたものだ。その対象は𝐂𝐚𝐭(𝐂,𝐃)\mathbf{Cat(C, D)}の元と同じだ。さらに、関手圏は圏なので、それ自体が𝐂𝐚𝐭\mathbf{Cat}の対象でなければならない(2つの小さい圏の間の関手圏も、それ自体が小さい圏だ)。ある圏のhom集合と、同じ圏の対象の間の関係は知っている。この状況は、前の章で見た冪対象と全く同じだ。後者を𝐂𝐚𝐭\mathbf{Cat}において構成する方法を見てみよう。

覚えていると思うが、冪を構成するには、まず積を定義する必要がある。𝐂𝐚𝐭\mathbf{Cat}では、これは実は比較的簡単だ。なぜなら、小さい圏は対象の集合であり、集合のデカルト積を定義する方法は知っているからだ。したがって、積圏𝐂×𝐃\mathbf{C \times D}内の対象は単なる対象のペア (c,d)(c, d) であり、𝐂\mathbf{C}𝐃\mathbf{D}からそれぞれ1つ取ったものだ。同様に、そのような2つのペア (c,d)(c, d)(c,d)(c', d') の間の射は、射のペア (f,g)(f, g) であり、ここでfccf \Colon c \to c'およびgddg \Colon d \to d'だ。これらの射のペアは成分ごとに合成でき、恒等ペアとしての単なる恒等射のペアも常に存在する。手短に言うと、𝐂𝐚𝐭\mathbf{Cat}は完全なデカルト閉圏であり、そこにはあらゆる圏のペアに対する冪対象𝐃𝐂\mathbf{D^C}が存在する。そして、𝐂𝐚𝐭\mathbf{Cat}の「対象」は圏を意味するので、𝐃𝐂\mathbf{D^C}は圏であり、𝐂\mathbf{C}𝐃\mathbf{D}の間の関手圏と同一視できる。

10.4 2-圏

閑話休題、ここで𝐂𝐚𝐭\mathbf{Cat}を詳しく見てみよう。定義より、𝐂𝐚𝐭\mathbf{Cat}内のどのHom集合も関手の集合だ。しかし、これまで見てきたように、2対象間の関手は単なる集合よりも豊かな構造を持っている。それらは圏を形成し、自然変換は射として作用する。関手は𝐂𝐚𝐭\mathbf{Cat}では射と見なされるので、自然変換は射の間の射だ。

より豊かなこの構造は、圏の一般化である𝟐\mathbf{2}-圏の例であり、対象と(この文脈では11-射とも呼べる)射の他に、射の間の射である22-射も存在する。

𝐂𝐚𝐭\mathbf{Cat}𝟐\mathbf{2}-圏と見なす場合、次のようになる:

  • 対象:(小さい)圏
  • 1-射:圏の間の関手
  • 2-射:関手の間の自然変換

2つの圏𝐂\mathbf{C}𝐃\mathbf{D}の間のHom集合の代わりにHom圏――関手圏𝐃𝐂\mathbf{D^C}がある。通常の関手合成があり、𝐃𝐂\mathbf{D^C}からの関手FF𝐄𝐃\mathbf{E^D}からの関手GGと合成して𝐄𝐂\mathbf{E^C}からの関手GFG \circ Fを与える。さらに、それぞれのHom圏内での合成もある――関手間での自然変換(すなわち2つの射)の垂直合成だ。

𝟐\mathbf{2}-圏に2種類の合成があるので、それらはどのように相互作用するのか、という疑問が生じる。

𝐂𝐚𝐭\mathbf{Cat}内の関手すなわち1-射を2つ選択しよう: F𝐂𝐃G𝐃𝐄 \begin{gathered} F \Colon \mathbf{C} \to \mathbf{D} \\ G \Colon \mathbf{D} \to \mathbf{E} \end{gathered} これらの合成は次のとおりだ: GF𝐂𝐄G \circ F \Colon \mathbf{C} \to \mathbf{E} α\alphaβ\betaという2つの自然変換があって、それぞれFFGGに作用するとする: αFFβGG \begin{gathered} \alpha \Colon F \to F' \\ \beta \Colon G \to G' \end{gathered}

α\alphaの終域とβ\betaの始域が異なるため、このペアには垂直合成を適用できないことに注意してほしい。実際、これらは別々の関手圏𝐃𝐂\mathbf{D^C}𝐄𝐃\mathbf{E^D}のメンバーだ。しかし、関手FF'GG'に合成を適用することはできる。FF'の終域もGG'の始域も圏𝐃\mathbf{D}だからだ。関手GFG' \circ F'GFG \circ Fはどのような関係だろうか?

α\alphaβ\betaを自由に使えるので、GFG \circ FからGFG' \circ F'への自然変換を定義できるだろうか? 構成をスケッチしよう。

いつものように、𝐂\mathbf{C}内の対象aaから始める。その像は𝐃\mathbf{D}の2つの対象FaF aFaF'aに分割される。また、α\alphaの成分である射が2つの対象を接続している: αaFaFa\alpha_a \Colon F a \to F'a 𝐃\mathbf{D}から𝐄\mathbf{E}に移行するとき、これら2つの対象はさらに4つの対象G(Fa)G(F a)G(Fa)G'(F a)G(Fa)G'(F'a)G(Fa)G'(F'a) に分割される。四角い図式を形成する4つの射もある。これらの射のうちの2つは、自然変換β\betaの成分だ: βFaG(Fa)G(Fa)βFaG(Fa)G(Fa) \begin{gathered} \beta_{F a} \Colon G (F a) \to G'(F a) \\ \beta_{F'a} \Colon G (F'a) \to G'(F'a) \end{gathered} 他の2つは、2つの関手によるαa\alpha_aの像だ(関手は射を写す): GαaG(Fa)G(Fa)GαaG(Fa)G(Fa) \begin{gathered} G \alpha_a \Colon G (F a) \to G (F'a) \\ G'\alpha_a \Colon G'(F a) \to G'(F'a) \end{gathered} 射がとてもたくさんある。目標は、G(Fa)G(F a) からG(Fa)G'(F'a) への射を見つけることだ。これは2つの関手GFG \circ FGFG \circ F'を接続する自然変換の成分の候補だ。実際、G(Fa)G(F a) からG(Fa)G'(F'a) への道は1つではなく2つある: GαaβFaβFaGαa \begin{gathered} G'\alpha_a \circ \beta_{F a} \\ \beta_{F'a} \circ G \alpha_a \end{gathered} ありがたいことに、これらは等しい。我々が形成した四角い図式はβ\betaの四角い自然性の図式だからだ。

GFG \circ FからGFG \circ F'への自然変換の成分が定義できた。この変換に対する自然性の証明は、十分に忍耐強い人にとっては、ごく簡単だ。

この自然変換を、α\alphaβ\beta水平合成 (horizontal composition) と呼ぶ: βαGFGF\beta \circ \alpha \Colon G \circ F \to G' \circ F' ここでも私はマックレーンに従って水平合成を表すのに小さな円を使うが、代わりにアスタリスクが使われることもある。

圏論的経験から言えば、合成に出会うたびに圏を探すべきだ。自然変換には垂直合成があり、それは関手圏の一部だ。しかし、水平合成についてはどうだろうか? それはどの圏にあるのだろう?

これを解明する方法は、𝐂𝐚𝐭\mathbf{Cat}を横から見ることだ。自然変換を、関手の間の矢としてではなく、圏の間の矢として見てほしい。自然変換は、それが変換する関手で接続された2つの圏の間に位置する。つまり、それら2つの圏を結びつけるものと見なせる。

ここでは𝐂𝐚𝐭\mathbf{Cat}の2つの対象――圏𝐂\mathbf{C}𝐃\mathbf{D}に焦点を当てる。𝐂\mathbf{C}𝐃\mathbf{D}に接続する関手間をつなぐ自然変換の集合がある。それらの自然変換が𝐂\mathbf{C}から𝐃\mathbf{D}への新しい矢だ。同様に、𝐃\mathbf{D}𝐄\mathbf{E}に接続する関手間をつなぐ自然変換が存在する。これは𝐃\mathbf{D}から𝐄\mathbf{E}へ向かう新しい矢として扱える。水平合成はこれらの矢の合成だ。

また、𝐂\mathbf{C}から𝐂\mathbf{C}への恒等射も存在する。これは恒等自然変換であり、𝐂\mathbf{C}上の恒等関手をそれ自体に写す。水平合成の恒等射は垂直合成の恒等射でもあるが、逆は成り立たないことに注意してほしい。

最後に、2つの合成は相互交換法則 (interchange law) を満たす: (βα)(βα)=(ββ)(αα)(\beta' \cdot \alpha') \circ (\beta \cdot \alpha) = (\beta' \circ \beta) \cdot (\alpha' \circ \alpha) ここでソーンダーズ・マックレーンの言葉を引用しよう:「読者はこの事実を証明するのに必要となる証明の図式を書き下すと楽しいだろう。」69

あともう1つ、将来役に立つだろう表記法がある。𝐂𝐚𝐭\mathbf{Cat}のこの水平方向の新解釈では、対象から対象へ行く方法が2つある:関手を使う方法と自然変換を使う方法だ。しかし、関手の矢は特別な種類の自然変換として再解釈できる。すなわち、その関手に作用する恒等自然変換として解釈すればよい。したがって、このような記法をよく目にすることになるだろう: FαF \circ \alpha ここで、FF𝐃\mathbf{D}から𝐄\mathbf{E}への関手で、α\alpha𝐂\mathbf{C}から𝐃\mathbf{D}への2つの関手間の自然変換だ。関手と自然変換は合成できないので、これは恒等自然変換1F1_Fα\alphaの後に水平合成したものと解釈される。

同様に: αF\alpha \circ Fα\alpha1F1_Fの後に水平合成したものだ。

10.5 おわりに

この本の第I部はここで締めくくろう。我々は圏論の基本的な語彙を学んだ。対象・圏は名詞、射・関手・自然変換は動詞と見なせる。射は対象たちを結びつけ、関手は圏たちを結びつけ、自然変換は関手たちを結びつける。

しかし、ある抽象化レベルで作用として現れるものが、次のレベルでは対象になることもまた見た。射の集合は関数対象になる。それは対象なので、別の射の始点や終点になり得る。これが高階関数の背景にある概念だ。

関手は対象を対象に写すので、型構成子、すなわちパラメーター化された型として使える。関手は射も写すので、高階関数fmapでもある。Const・積・余積などの単純な関手がいくつかあって、さまざまな代数的データ型の生成に使える。関数型も共変関手的[訳注:戻り値の型においては。]かつ反変関手的[訳注:引数の型においては。]で、代数的データ型を拡張するのに使える。

関手は関手圏での対象とも見なせる。そうすることで、それらは自然変換、すなわち射の始点および終点になる。自然変換は特別な多相関数だ。

10.6 課題

  1. Maybe関手からリスト関手への自然変換を定義せよ。その自然性条件を証明せよ。

  2. Reader ()とリスト関手の間に、少なくとも2つの異なる自然変換を定義せよ。要素がunit型()のリストは何種類あるか?

  3. Reader BoolMaybeを使って前の課題を続けよ。

  4. 自然変換の水平合成が自然性条件を満たしていることを示せ(ヒント:成分を使う)。これは図式を追う良い練習になる。

  5. 相互交換法則を証明するために必要な明確な図を描くのを楽しむ方法について、短いレポートを書け。

  6. 異なるOp関手間の変換についての反対の自然性条件について、テストケースをいくつか作成せよ。以下は選択肢の1つだ:

    op :: Op Bool Int
    op = Op (\x -> x > 0)

    および

    f :: String -> Int
    f x = read x

11 圏論と宣言的プログラミング

第I部では、圏論とプログラミングはどちらも合成可能性に関するものだと論じた。プログラミングでは、扱える程度の詳細さに達するまで問題を分解し続け、それぞれの部分問題を順番に解決し、それらの解決策をボトムアップで再合成する。これには大きく2つの方法がある。コンピューターに何をすべきか指示する方法と、どのようにすべきか指示する方法だ。前者は宣言的と呼ばれ、後者は命令的と呼ばれる。

このことは最も基本的なレベルでさえ見られる。合成自体は宣言的にも定義できる。たとえば、hgfの後に合成したものだ:

h = g . f

あるいは、命令的にも定義できる。まずfを呼び出し、その呼び出しの結果を記憶し、それからその結果を使ってgを呼び出す:

h x = let y = f x
      in g y

命令的な方のプログラムは通常、動作を時系列で並べたものとして記述される。特に、fの実行が完了する前にgの呼び出しは起こりえない。少なくとも、概念としてはそうだ。ただし、call-by-needで引数が渡される遅延評価言語では、実際の実行順序は異なる可能性がある。

実際、コンパイラーの賢さによっては、宣言的コードと命令的コードの実行方法にほとんど違いがない場合もある。しかし、この2つの方法論は、問題解決へのアプローチ方法や実装コードの保守性とテスト可能性において、ときには劇的に異なる。

最大の疑問はこうだ。問題に直面したとき、解決のための選択肢として、宣言的アプローチと命令的アプローチの両方が常にあるのだろうか? そして、宣言的な解決策があるなら、それは常にコンピューターコードに翻訳できるのだろうか? この疑問への答えは自明とは全く言えず、もしその答えを見つけられたなら、宇宙の理解に革命が起こるだろう。

詳しく説明させてほしい。物理学にも似たような双対性がある。それは、何か深い基本原理を指し示したり、我々の心の働きについて何かを教えてくれたりする。リチャード・ファインマンはこの双対性について、自身の量子電磁力学の研究におけるインスピレーションとして言及している。

ほとんどの物理法則には2つの表現形式がある。1つは局所的すなわち無限小的な考察を用いる。我々はごく近傍の系の状態を見て、それが次の瞬間にどう変化するかを予測する。これは通常、ある期間にわたって積分すなわち合計する必要があるような微分方程式で表される。

このアプローチが命令的思考に似ていることに注目してほしい。つまり、前のステップの結果に応じた一連の小さなステップに従って最終的な解に到達する。実際、物理系のコンピューターシミュレーションの実装では、微分方程式を差分方程式に変換して反復実行するのが常道だ。小惑星ゲームの宇宙船はそのようにしてアニメーション化される。各時間ステップにおける宇宙船の位置は、速度に時間間隔を掛け算した小さな増分を加えることで変化する。同様に、速度は加速度(力を質量で割った値となる)に比例した小さな増分によって変化する。

ニュートンの運動法則に対応する微分方程式を直接的に記述すると次のようになる: F=mdvdtv=dxdt \begin{aligned} F &= m \frac{dv}{dt} \\ v &= \frac{dx}{dt} \end{aligned} 同様の方法は、より複雑な問題にも適用できる。たとえば、電磁場の伝播はマクスウェル方程式で記述でき、陽子内部のクォークやグルーオンの挙動さえも格子量子色力学で記述できる。

この局所的な考え方と、デジタルコンピューターの使用によって促進された空間と時間の離散化とが組み合わさった最たるものは、宇宙全体の複雑さをセルオートマトンの系に縮約しようとするスティーブン・ウルフラム70の英雄的な試みの中に表れている。

もう1つのアプローチは大域的なものだ。我々はシステムの初期状態と最終状態を見て、それらを結ぶ軌道を、特定の汎関数を最小化することで計算する。最も簡単な例はフェルマーの最小時間の原理だ。それは、光線は伝搬時間が最小となる経路に沿って伝搬すると述べている。特に、反射や屈折をする物体がない場合、点AAから点BBへの光線は最短経路である直線を通る。しかし、水やガラスのような(透明な)高密度の媒質中では光の伝播速度は遅くなる。したがって、始点を空気中とし、終点を水中とすると、光にとって空中をより長く進んでから水中を近道する方が有利になる。最小時間の経路では光線が空気と水の境界で屈折し、スネルの屈折の法則が導かれる: sin(θ1)sin(θ2)=v1v2\frac{sin(\theta_1)}{sin(\theta_2)} = \frac{v_1}{v_2} ここで、v1v_1は空気中の光速、v2v_2は水中の光速だ。

古典力学のすべては最小作用の原理から導出できる。作用は任意の経路について、運動エネルギーからポテンシャルエネルギーを引いた差であるラグランジアンを積分することで計算できる(注:和ではなく差だ――和は全エネルギーとなる)。大砲を撃って標的に命中させようとするとき、弾はまず重力によるポテンシャルエネルギーがより高い場所へと上昇し、しばらくの間そこで作用への負の寄与を蓄積する。しかも、放物線の頂点に向けて減速することで運動エネルギーを最小限に抑える。それから加速することでポテンシャルエネルギーの低い領域を素早く通過する。

ファインマンの最大の功績は、最小作用の原理が量子力学に一般化できると示したことだ。ここでも、問題は初期状態と最終状態について定式化されている。ファインマン経路積分をそれらの状態間に用いると、遷移確率を計算できる。

重要なのは、我々が物理法則を記述できる方法には奇妙で説明のつかない双対性があるということだ。局所的な描像を採用し、物事が連続して小さな増分で起こると捉えてもよい。あるいは、大域的な描像を採用し、初期条件と最終条件を宣言し、途中のすべてはそれらにただ従うと捉えてもよい。

大域的アプローチはプログラミングでも使える。たとえば、レイトレーシングを実装する場合などだ。眼の位置と光源の位置を宣言し、それらを光線が接続できる経路を見つければよい。各光線について飛行時間を明示的には最小化しないが、実際にスネルの法則と反射の幾何学を用いて同じ効果を得ている。

局所的アプローチと大域的アプローチの大きな違いは、空間の扱いと、さらに重要なことに、時間の扱いだ。局所的なアプローチでは、いま・ここの即時的な満足を受け入れるのに対し、大域的なアプローチでは、あたかも未来があらかじめ決まっていて、我々は不変なる宇宙の性質をただ分析しているかのように、長期的で静的な見方をする。

ユーザーインタラクションに対する関数型リアクティブプログラミング (FRP) アプローチほど、これが分かりやすく説明されているものはない。想定されるすべてのユーザーアクションに対して個別のハンドラーを記述して、そのすべてが共有の可変状態にアクセスできるようにする代わりに、FRPでは外部イベントを無限リストとして扱って一連の変換を適用する。概念的には、将来のすべてのアクションのリストがそこにあり、プログラムへの入力データとして利用できる。プログラムの観点からは、π\piの数字のリスト、擬似乱数のリスト、コンピューターのハードウェアから得られるマウス座標のリストの間に違いはない。いずれの場合も、第nn項を得るには最初のn1n-1個の項を先に調べる必要がある。時間的イベントについて述べる場合、この性質は因果律 (causality) と呼ばれる。

それで、圏論と何の関係があるのだろうか? 私の主張としては、圏論は大域的アプローチを奨励しており、それゆえ宣言的プログラミングを支持している。第一に、微積分とは違って、距離・近傍・時間などの概念が組み込まれていない。あるのは抽象的な対象たちとそれらの間の抽象的な接続だけだ。AAからBBへ一連のステップで到達できるなら、一足飛びにも到達できる。さらに、圏論の主要なツールはまさに普遍的構成であり、それは大域的アプローチの典型だ。実際の使用例は、たとえば、圏論的な積の定義ですでに見た。それは積の性質を指定することによってなされた、まさしく宣言的なアプローチだ。積とは2つの射影を伴う対象であり、そういった対象のうち最良のものだ。つまり、ある特性を最適化している。それは、他の同様の対象の射影を分解する特性だ。

これをフェルマーの最短時間の原理、あるいは最小作用の原理と比較してほしい。

逆に、デカルト積の従来の定義と対比させるとどうだろう。後者の方がはるかに命令的だ。積の要素を作るには、ある集合から1つの要素を選択し、別の集合から別の要素を選択する、という説明になる。これはペアを作るためのレシピだ。また、ペアを分解するためのレシピもある。

Haskellなどの関数型言語を含め、ほとんどすべてのプログラム言語では、直積型・余積型・関数型は組み込まれており、普遍的構成で定義されるのではない。ただし、圏論的プログラム言語の作成も試みられている (たとえば、萩野達也の博士論文71を参照)。

直接使われるかどうかにかかわらず、圏論的な定義は既存のプログラミング構成を正当なものにするとともに、新しい構成を生み出す。最も重要なのは、宣言的レベルでコンピュータープログラムについて推論するためのメタ言語を圏論が提供することだ。また、コードとして表す前に問題の仕様について推論することも奨励する。

12 極限と余極限

圏論では、すべてがすべてに関係していて、すべてのものを様々な角度から見られるようだ。たとえば、(第5章)の普遍的構成を考えてみよう。今は関手自然変換(第10章)についても学んだので、それらを単純化し、可能なら一般化できないだろうか? やってみよう。

積の構成は、積を構築したい2つの対象aabbを選択することから始まる。しかし、対象を選択するとは何を意味するのだろう? もっと圏論らしい言葉で言い換えられないだろうか? 2つの対象はあるパターンを形成する――ごく単純なパターンだ。このパターンは圏に抽象化できる――ごく単純な圏だが、それでも紛れもなく圏だ。それは𝟐\mathbf{2}と呼ばれる圏だ。それは2つの対象(それぞれ1122と呼ぶことにする)と、2つの必須の恒等射だけを含む。ここで、𝐂\mathbf{C}内の2つの対象を選択するということを、圏𝟐\mathbf{2}から𝐂\mathbf{C}への関手DDを定義することだと言い換えられる。関手は対象を対象に写すため、その像はちょうど2つの対象になる(関手が対象を潰す場合は1つの対象になるが、その場合も問題ない)。この関手は射も写すが、ここでは単に恒等射を恒等射に写す。

このアプローチの素晴らしいところは、圏論の概念に基づいて構築されていることであり、「対象を選択する」といった不正確な、我々の祖先たる狩猟採集民の語彙からそのまま採ったような記述を避けていることだ。おまけに、一般化するのも簡単だ。パターンを定義するために𝟐\mathbf{2}より複雑な圏を使ってはいけない理由はないからだ。

だが、ここではこのまま進もう。積を定義する次のステップは候補となる対象ccの選択だ。ここでも、単元圏からの関手によって対象の選択を言い換えてしまってもよいかもしれない。そして確かに、もし我々が既にカン拡張を使っていたとしたら、そうするのが正しかっただろう。しかし、まだカン拡張を使う準備ができていないので、別のトリックを使おう。同じ圏𝟐\mathbf{2}から𝐂\mathbf{C}への定関手Δ\Deltaだ。Δc\Delta_cによって𝐂\mathbf{C}からccを選択できる。Δc\Delta_cはすべての対象をccに写し、すべての射を𝐢𝐝c% \mathbf{id}_{c}% に写すことを思い出してほしい。

Δc\Delta_cDDという、𝟐\mathbf{2}𝐂\mathbf{C}の間の2つの関手が手に入ったからには、これらの関手の間の自然変換について問うのは自然なことだ。𝟐\mathbf{2}の対象は2つだけなので、自然変換は2つの成分を持つことになる。𝟐\mathbf{2}内の対象11は、Δc\Delta_cによってccに、DDによってaaに写される。したがって、Δc\Delta_cDDの間の自然変換の11における成分はccからaaへの射だ。それをppと呼ぼう。同様に、2番目の成分はccからbbへの射qqだ。ここで、bb𝟐\mathbf{2}内の対象22DDによる像だ。しかし、これらはもともとの積の定義で用いた2つの射影とそっくりだ。つまり、対象の選択と射影について議論する代わりに、単に関手の選択と自然変換について議論するのでよいということだ。たまたま、ここでの単純なケースでは変換の自然性条件が自明に満たされている。𝟐\mathbf{2}には射が(恒等射以外には)存在しないからだ。

この構成を𝟐\mathbf{2}以外の圏(たとえば、自明でない射を含む圏)に一般化すると、Δc\Delta_cDDの間の変換に自然性条件が課される。このような変換は (cone) と呼ばれる。Δ\Deltaの像が、自然変換の成分たちが側面を構成するような錐体(円錐・角錐など)72の頂点になっているからだ。DDの像は錐の底面を形成する。

一般に、錐を構築するには、そのパターンを定義する圏𝐈\mathbf{I}から始める。𝐈\mathbf{I}は小さい圏で、しばしば有限だ。𝐈\mathbf{I}から𝐂\mathbf{C}への関手DDを選択し、それ(またはその像)を図式 (diagram) と呼ぶ。また、𝐂\mathbf{C}内のccを錐の頂点として選択する。それを使って𝐈\mathbf{I}から𝐂\mathbf{C}への定関手Δc\Delta_cを定義する。Δc\Delta_cからDDへの自然変換こそが、ここで言うところの錐となる。有限の𝐈\mathbf{I}に対しては、錐はccと図式、つまりDDによる𝐈\mathbf{I}の像とを接続するただの射の集まりだ。

自然性は、この図式のすべての三角形(角錐の側面)が可換であることを必要とする。実際に、𝐈\mathbf{I}内の任意の射ffを取ったとしよう。関手DDは、それを𝐂\mathbf{C}内の射DfD fに写し、その射はある三角形の底辺となる。定関手Δc\Delta_cは、ffccにおける恒等射に写す。Δ\Deltaはその射の両端を1つの対象にまとめ、自然性の四角い図式は可換な三角形になる。この三角形の2辺は自然変換の成分になっている。

これは錐のひとつではある。だが、関心があるのは普遍錐 (universal cone) だ――普遍的な対象を積の定義としたのと同様だ。

その普遍錐の構成を目指す方法はいろいろある。たとえば、与えられた関手DDに基づいて錐の圏を定義できる。その圏における対象は錐だ。ただし、𝐂\mathbf{C}のすべての対象ccが錐の頂点になれるわけではない。Δc\Delta_cDDの間に自然変換が存在しないことがあるからだ。

圏にするには、錐の間の射も定義しなければならない。それらは頂点間の射によって完全に決定されることになる。しかし、どんな射でも良いわけではない。我々の積の構成では、候補となる対象(頂点)の間の射は射影の共通因子でなければならない、という条件を課したのを思い出してほしい。具体的には:

p' = p . m
q' = q . m

一般の場合73には、この条件は、1辺が分解射である三角形はすべて可換である、という条件に翻訳される。

分解射hによって2つの錐を接続する可換な三角形(ここで、下側の錐は%
\mathbf{Lim}{D}%
を頂点とする普遍錐)

これらの分解射を錐の圏における射とする。これらの射が実際に合成できることと、恒等射が分解射であることを確認するのは簡単だ。したがって、錐たち74は圏を形成する。

ここで、普遍錐を錐の圏の終対象として定義できる。終対象の定義から、どの対象からも終対象への一意な射がある。ここでは、それは他のどの錐の頂点からも普遍錐の頂点への一意な分解射があることを意味する。この普遍錐は図式DD極限 (limit) 𝐋𝐢𝐦D% \mathbf{Lim}{D}% と呼ばれる(この分野では𝐋𝐢𝐦% \mathbf{Lim}{}% 記号の下にIIに向かう左矢印が記されていることも多い)。しばしば、この錐の頂点を指して極限(あるいは極限対象)と略称する。

直観的には、極限は図式全体の特性を単一の対象で具現化している。たとえば、2対象の図式の極限は2つの対象の積だ。積(および2つの射影を合わせたもの)には両方の対象についての情報が含まれている。そして、普遍であるということは要らないものを含まないことを意味する。

12.1 自然同型としての極限

この極限の定義にはまだ不満が残る。どういうことかと言うと、使えはするものの、任意の2つの錐を結ぶ三角形には依然として前述のような可換性条件が課されている。それを何らかの自然性条件に置き換えられれば、はるかにエレガントになるだろう。だが、どうやって?

もはや我々は、1つの錐ではなく、錐の集まり(実際には圏)全体を扱っている。極限が存在するなら(そして――明言しておくと――存在する保証はない)、それらの錐の1つは普遍錐だ。他のすべての錐のそれぞれに対して、その頂点(ccと呼ぼう)を普遍錐の頂点𝐋𝐢𝐦D% \mathbf{Lim}{D}% に写す一意な分解射が存在する。(実際には「他の」という言葉は省ける。普遍錐が恒等射によって普遍錐自身に写され、それ自身を分解するのは自明だからだ。) 要点を繰り返そう。任意の錐について、特別な種類の一意な射が存在する。つまり、それは錐からそういった特別な射への写像があって、それは1対1の写像であるということだ。

この特別な射はhom集合𝐂(c,𝐋𝐢𝐦D)\mathbf{C}(c, % \mathbf{Lim}{D}% ) の元だ。このhom集合のそれ以外の元は、残念ながら2つの錐の写像を分解しない。ここで必要なのは、ccごとに、𝐂(c,𝐋𝐢𝐦D)\mathbf{C}(c, % \mathbf{Lim}{D}% ) から1つの射――特定の可換性条件を満たす射――を選択できるようにすることだ。まるで自然変換を定義しているかのように聞こえるだろうか? まさしくそのとおり!

だが、この変換によって関連付けられるのはどのような関手だろう?

関手の1つは、ccから集合𝐂(c,𝐋𝐢𝐦D)\mathbf{C}(c, % \mathbf{Lim}{D}% ) への写像だ。これは𝐂\mathbf{C}から𝐒𝐞𝐭\mathbf{Set}への関手で、対象たちを集合たちに写す。正確には反変関手だ。射に関する作用は次のように定める。cc'からccへの射ffを取ってきたとしよう: fccf \Colon c' \to c 我々の関手はcc'を集合𝐂(c,𝐋𝐢𝐦D)\mathbf{C}(c', % \mathbf{Lim}{D}% ) に写す。この関手のffに対する作用を定義する(言い換えると、ffを持ち上げる)には、それに対応する、𝐂(c,𝐋𝐢𝐦D)\mathbf{C}(c, % \mathbf{Lim}{D}% )𝐂(c,𝐋𝐢𝐦D)\mathbf{C}(c', % \mathbf{Lim}{D}% ) の間の写像を定義しなければならない。そこで、𝐂(c,𝐋𝐢𝐦D)\mathbf{C}(c, % \mathbf{Lim}{D}% ) の要素uuを1つ選択し、それを𝐂(c,𝐋𝐢𝐦D)\mathbf{C}(c', % \mathbf{Lim}{D}% ) の要素に写せるか見てみよう。hom集合の要素は射なので、次のことが言える: uc𝐋𝐢𝐦Du \Colon c \to % \mathbf{Lim}{D}% uuffに前合成75することで次が得られる: ufc𝐋𝐢𝐦Du \circ f \Colon c' \to % \mathbf{Lim}{D}% そして、これは𝐂(c,𝐋𝐢𝐦D)\mathbf{C}(c', % \mathbf{Lim}{D}% ) の要素だ――つまり、実際に射の写像が見つかった:

contramap :: (c' -> c) -> (c -> Lim D) -> (c' -> Lim D)
contramap f u = u . f

反変関手の特徴である、cccc'の順序の反転に注目してほしい。

自然変換を定義するには、同じく𝐂\mathbf{C}から𝐒𝐞𝐭\mathbf{Set}への写像である別の関手が必要だ。今回については錐の集合を考えてみる。錐は単なる自然変換なので、自然変換の集合𝑁𝑎𝑡(Δc,D)\mathit{Nat}(\Delta_c, D) を見ていることになる。ccをこの特定の自然変換の集合へ対応させる写像は(反変)関手となる。どうやってそれを示そう? ここでも、次の射に対するその関手の作用を定義してみよう: fccf \Colon c' \to c ffを持ち上げると𝐈\mathbf{I}から𝐂\mathbf{C}へ向かう2つの関手間の自然変換たちの写像になる必要がある。つまり: 𝑁𝑎𝑡(Δc,D)𝑁𝑎𝑡(Δc,D)\mathit{Nat}(\Delta_c, D) \to \mathit{Nat}(\Delta_{c'}, D) 自然変換はどう写せば良いだろう? 自然変換は射――その成分――の選択であり、𝐈\mathbf{I}の要素ごとに射を1つ選択する。ある(𝑁𝑎𝑡(Δc,D)\mathit{Nat}(\Delta_c, D) の元である)α\alphaの、(𝐈\mathbf{I}内の対象である)aaにおける成分は、次の射となる: αaΔcaDa\alpha_a \Colon \Delta_c a \to D a つまり、定関手Δ\Deltaの定義から: αacDa\alpha_a \Colon c \to D a 任意のffα\alphaについて、𝑁𝑎𝑡(Δc,D)\mathit{Nat}(\Delta_{c'}, D) の元であるβ\betaを構築する必要がある。β\betaaaにおける成分は次の射となる: βacDa\beta_a \Colon c' \to D a 後者(βa\beta_a)は前者(αa\alpha_a)をffに前合成すれば簡単に得られる: βa=αaf\beta_a = \alpha_a \circ f これらの成分たちが実際に自然変換をなすことは比較的簡単に示せる。

以上より、与えられた射ffについて、2つの自然変換の間の写像を成分ごとに構築できた。この写像は次の関手についてcontramapを定義する76c𝑁𝑎𝑡(Δc,D)c \to \mathit{Nat}(\Delta_c, D) ここまでで示したのは、𝐂\mathbf{C}から𝐒𝐞𝐭\mathbf{Set}への2つの(反変)関手があるということだ。示すにあたっては何の仮定も置かなかった。よって、これらの関手は常に存在すると言える。

ちなみに、これらの関手のうち1つ目は圏論で重要な役割を果たしている。米田の補題について話すときに再び見ることになるだろう。任意の圏𝐂\mathbf{C}から𝐒𝐞𝐭\mathbf{Set}への反変関手は「前層」(presheaf) と呼ばれる。ここでのものは表現可能前層 (representable presheaf) と呼ばれている。2つ目の関手も前層だ。

2つの関手が出てきたので、その間の自然変換について述べられるようになった。では、これ以上何も言わず、結論を述べよう。𝐈\mathbf{I}から𝐂\mathbf{C}への関手DDが極限𝐋𝐢𝐦D% \mathbf{Lim}{D}% を持つのは、先ほど定義した2つの関手間に自然同型がある場合、かつその場合に限る: 𝐂(c,𝐋𝐢𝐦D)𝑁𝑎𝑡(Δc,D)\mathbf{C}(c, % \mathbf{Lim}{D}% ) \simeq \mathit{Nat}(\Delta_c, D) 自然同型とは何かを思い出してほしい。すべての成分が同型、すなわち可逆な射である自然変換だった。

この命題の証明をなぞるつもりはない。退屈とまでは言わないにしても、ごく単純な手順で済むからだ。自然変換を扱うときには通常、その成分、つまり射に注目する。ここでは両方の関手の終域が𝐒𝐞𝐭\mathbf{Set}なので、前述の自然同型の成分は関数になる。そして、それらの関数はhom集合から自然変換の集合へと向かうので、高階関数となる。再び、関数が引数に対して何を行うかを調べることで関数を分析できる。ここで、引数は𝐂(c,𝐋𝐢𝐦D)\mathbf{C}(c, % \mathbf{Lim}{D}% ) の元である射になり、結果は𝑁𝑎𝑡(Δc,D)\mathit{Nat}(\Delta_c, D) の元である自然変換、すなわち錐と呼んだものになる。この自然変換自身にもそれぞれの成分があり、それらは射だ。このようにどこまでも射が続くので、それらを追跡できれば先ほどの命題を証明できる。

最も重要な結果は、この同型に対する自然性条件がまさしく錐の写像についての可換性条件になっているということだ。

今後のアトラクションの予告として、集合𝑁𝑎𝑡(Δc,D)\mathit{Nat}(\Delta_c, D) を関手圏のhom集合と見なせることに触れておきたい。つまり、ここでの自然同型は2つのhom集合を関連づけ、随伴と呼ばれるさらに一般的な関係を指し示している。

12.2 極限の例

すでに見たように、圏論的な積は、𝟐\mathbf{2}と呼ばれる単純な圏が生成する図式の極限だ。

極限のさらに単純な例としては終対象がある。単元圏が終対象につながる、と初期衝動で考えるかもしれないが、真実はさらに殺風景だ。終対象は空圏が生成する極限なのだ。空圏からの関手は対象を選択しないので、錐は頂点だけに収縮する。普遍錐はその唯一の頂点で、他の頂点からの一意な射を持つ。これが終対象の定義だと気付くだろう。

次に興味深い極限は等化子 (equalizer) と呼ばれる。これは2つの要素からなる圏によって生成された極限であり、要素間の2つの並行した射を伴う(そして当然、恒等射も持つ)。この圏は、2つの対象aabbと、2つの射とで構成される𝐂\mathbf{C}の図式を選択する:

f :: a -> b
g :: a -> b

この図式の上に錐を構築するには、頂点ccと2つの射影を追加する必要がある:

p :: c -> a
q :: c -> b

可換でなければならない三角形が2つある:

q = f . p
q = g . p

これはqqが、これらの等式の1つ、たとえばq = f . pによって一意に決定され、図から省けることを示している。残りの条件は1つだけになる:

f . p = g . p

これは、注目する範囲を𝐒𝐞𝐭\mathbf{Set}だけに限れば、関数ppの像はaaの部分集合を選択している、と解釈できる。この部分集合に限れば、関数ffggは等しくなる。

たとえば、aaを座標xxyyでパラメーター化された2次元平面とする。bbを実数の直線とし、次のようにする:

f (x, y) = 2 * y + x
g (x, y) = y - x

これら2つの関数の等化子は、実数の集合(頂点cc)と次の関数だ:

p t = (t, (-2) * t)

(pt)(p~t) は2次元平面内の直線を定義することに注意してほしい。この線上では2つの関数は等しい。

当然、この等しさは他の集合cc'と関数pp'でも満たしうる:

f . p' = g . p'

ただし、それらはすべてppを通じて一意に分解する。たとえば、単元集合()\mathbf{()}cc'として受け取れば次の関数を使える:

p'() = (0, 0)

f(0,0)=g(0,0)f(0, 0) = g(0, 0) だから、これは良い錐だ。しかし、普遍錐ではなく、hhによって一意に分解できる:

p' = p . h

ここで:

h () = 0

したがって、等化子は型fx=gxf~x = g~xの方程式を解くのに使える。しかも、もっと一般化されている。代数的な定義ではなく、対象や射によって定義されているからだ。

方程式を解くという考え方をさらに一般化したものが、別の極限で具体化されている――引き戻し (pullback) だ。ここに、等価にしたい2つの射がまたあるが、今回はそれらの始点が異なる。まず1231\rightarrow2\leftarrow3という形の3対象の圏から始める。この圏に対応する図式は、3つの対象(aa, bb, cc)と2つの射で構成されている:

f :: a -> b
g :: c -> b

この図式は余スパン (cospan) と呼ばれることが多い。

この図式の上に構築された錐は、頂点ddと3つの射から構成されている:

p :: d -> a
q :: d -> c
r :: d -> b

可換性条件からrrは他の射によって完全に決定されることが分かり、図から省略できる。したがって、次の条件のみが残る:

g . q = f . p

引き戻しは、この形の普遍錐だ。

再び、注目する範囲を集合だけに絞ると、対象ddaaccからの要素のペアのうち、ffが1番目の成分に作用したものとggが2番目の成分に作用したものとが等しいという条件を満たすものからなると見なせる。これがまだ一般的すぎるなら、ggが定数関数である特別な場合を考えてみてほしい。たとえば、(bbが実数の集合だと仮定して)g_=1.23g~\_ = 1.23とする。そうすれば、実際に方程式を解いていることになる:

f x = 1.23

この場合、ccとして何を選択しても(空集合でない限り)関係ないので、単元集合として構わない。たとえば、集合aaが3次元ベクトルの集合で、ffがベクトル長だとする。そうすれば、引き戻しはペア (v,())(v, ()) の集合で、vvは長さ1.23のベクトル(方程式(x2+y2+z2)=1.23\sqrt{(x^{2}+y^{2}+z^{2})} = 1.23の解)となる。()() は単元集合のダミー要素だ。

もっとも、引き戻しにはより一般的な用途があり、プログラミングでも使える。たとえば、C++のクラスを圏と見なし、派生クラスを基底クラスに結ぶ矢印を射と見なそう。継承関係は推移的だと見なせる。つまり、CBを継承し、BAを継承しているなら、CAを継承していると言える(要するに、Aへのポインターを想定する箇所にCへのポインターを渡せるということだ)。また、CCを継承すると見なそう。つまり、すべてのクラスに恒等射がある。このようにすれば、派生クラス関係は部分型関係と一致することになる。C++は多重継承もサポートしているので、Aを継承する2つのクラスBC、およびBCとを多重継承する4番目のクラスDを含むダイヤモンド継承の図式を構築できる。通常、DAを2つ持つことになる。ほとんどの場合、これは望ましくない。だが、仮想継承を使えばD内のAを1つだけにできる。

Dがこの図式の引き戻しになるとは何を意味するのだろう? それは、BCを多重継承するすべてのクラスEが、Dの派生クラスでもあることを意味する。これはC++では直接表現できない。C++の部分型付け77は名前に基づくもの (nominal) だからだ(C++コンパイラーはこの種のクラス関係を推測しない――それには「ダックタイピング」が必要となる78)。しかし、部分型付け関係の外に出て、代わりにEからDへのキャストが安全かどうか確かめることはできる。このキャストが安全なのは、DBCの必要最小限の組み合わせで、追加データやメソッドのオーバーライドがない場合だ。そしてもちろん、BCのメソッドに名前の衝突がある場合、引き戻しはない。

さらに、型推論では引き戻しがより高度な使い方をされる。2つの表現の型を単一化 (unify) したいことはよくある。たとえば、コンパイラーが関数の型を推測しようとしているとする:

twice f x = f (f x)

これはすべての変数と部分式に予備的な型を割り当てる。具体的には、以下を割り当てる:

f       :: t0
x       :: t1
f x     :: t2
f (f x) :: t3

そこから次のことが演繹される:

twice :: t0 -> t1 -> t3

また、関数適用の規則から生じる一連の制約も課される:

t0 = t1 -> t2 -- fがxに適用されるため
t0 = t2 -> t3 -- fが (f x) に適用されるため

これらの制約は、両方の式の未知の型に代入したときに両辺が同じになるような型(あるいは型変数)の集合を見つけることによって単一化される。そのような代入の1つは次のとおりだ:

t1 = t2 = t3 = Int
twice :: (Int -> Int) -> Int -> Int

しかし、明らかに、これは最も一般的なものではない。最も一般的な代入79を得るには引き戻しを用いる。詳細は本書の範囲外であるため説明しないが、次のような結果になるはずであることは確信できるだろう:

twice :: (t -> t) -> t -> t

ここでtは自由 (free) 型変数だ。

12.3 余極限

圏論のすべての構造と同じく、極限には双対となる像が反対圏にある。錐内のすべての射の方向を反転させると余錐 (co-cone) ができ、余錐たちのうち普遍なものが余極限 (colimit) と呼ばれる。反転が分解射にも影響することに注意してほしい。分解射は普遍な余錐から他の余錐へと向かうことになる。

2つの頂点を結ぶ分解射hを持つ余錐。

余極限の典型的な例は余積だ。これは、積の定義で使った𝟐\mathbf{2}-圏によって生成される図式に対応する。

積も余積も、別々の方法で対象のペアの本質を具現化している。

終対象が極限であったように、始対象は空圏に基づく図式に対応する余極限だ。

引き戻しの双対は押し出し (pushout) と呼ばれる。それは圏1231\leftarrow2\rightarrow3によって生成されるスパン (span) と呼ばれる図式に基づく。

12.4 連続性

すでに述べたように、関手は圏の連続写像の概念に近い。既存の接続(射)を決して壊さないからだ。圏𝐂\mathbf{C}から𝐂\mathbf{C'}への連続関手 (continuous functor) FFの実際の定義には、関手が極限を保存するという要件が含まれている。𝐂\mathbf{C}内のすべての図式DDは、単に2つの関手を合成すれば、𝐂\mathbf{C'}内の図式FDF \circ Dに写せる。FFの連続性条件は、図式DDが極限𝐋𝐢𝐦D% \mathbf{Lim}{D}% を有するなら、図式FDF \circ Dも極限を有し、それはF(𝐋𝐢𝐦D)F (% \mathbf{Lim}{D}% ) に等しいことを示している。

関手は射を射に、合成を合成に写すので、錐の像は常に錐であることに注意してほしい。可換な三角形は常に可換な三角形に写される(関手は合成を保存する)。分解射についても同じことが言えて、分解射の像も分解射となる。したがって、どの関手もほぼ連続している。問題になりうるのは一意性条件だ。𝐂\mathbf{C'}の分解射は一意でないことがある。𝐂\mathbf{C'}には、𝐂\mathbf{C}にはなかった「より優れた錐」が他にあるかもしれない。

Hom関手は連続関手の一例だ。Hom関手𝐂(a,b)\mathbf{C}(a, b) が、最初の変数について反変であり、2番目の変数について共変であることを思い出してほしい。言い換えれば、次の関手だ: 𝐂𝑜𝑝×𝐂𝐒𝐞𝐭\mathbf{C}^\mathit{op} \times{} \mathbf{C} \to \mathbf{Set} 第2引数が固定されると、hom集合関手(表現可能前層になる)は𝐂\mathbf{C}の余極限を𝐒𝐞𝐭\mathbf{Set}の極限に写す。第1引数が固定されると、極限を極限に写す。

Haskellでのhom関手は、任意の2つの型を関数型に対応させる写像であるため、単なるパラメーター化された関数型だ。2番目のパラメーターを、たとえばStringに固定すると、次の反変関手が得られる:

newtype ToString a = ToString (a -> String)
instance Contravariant ToString where
    contramap f (ToString g) = ToString (g . f)

連続性は、ToStringが余極限、たとえば余積Either b cに適用された場合に極限を生成することを意味する。この場合、極限は2つの関数型の積である。つまり:

ToString (Either b c) ~ (b -> String, c -> String)

実際に、Either b cのどんな関数も、関数のペアに対応する2つの場合分けを持つcase式として実装される。

同様に、hom集合の第1引数を固定すると、おなじみのreader関手が得られる。その連続性は、たとえば、積を返す関数どの関数も積と等価であることを意味する。具体的にはこうなる:

r -> (a, b) ~ (r -> a, r -> b)

読者がどう思っているかは分かっている:これらを理解するのに圏論は必要ない。そのとおりだ! それでも、このような結果がビットやバイト、プロセッサーアーキテクチャ、コンパイラー技術、さらにはラムダ計算に頼ることなく第一原理から得られるのは驚くべきことだと思う。

「極限」と「連続性」という名前の由来が何なのか気になるなら、それらは微積分学において対応する概念を一般化したものだ。微積分学では、極限と連続性は開近傍によって定義される。開集合たちは、位相 (topology) を定義し、圏(半順序集合)もなす。

12.5 課題

  1. C++のクラスの圏における押し出しはどのようなものだろうか?

  2. 恒等関手𝐈𝐝𝐂𝐂\mathbf{Id} \Colon \mathbf{C} \to \mathbf{C}の極限が始対象であることを示せ。

  3. 与えられた集合の部分集合たちは圏をなす。その圏の射は、1番目の集合が2番目の集合の部分集合である場合、それら2つの集合を接続する矢として定義される。そのような圏内の集合2つの引き戻しは何か? 押し出しは何か? 始対象と終対象は何か?

  4. 余等化子 (coequalizer) とは何か予想できるか?

  5. 終対象が存在する圏では、終対象に向かう引き戻しが積であることを示せ。

  6. 同様に、始対象が存在するなら、始対象からの押し出しが余積であることを示せ。

13 自由モノイド

モノイドは圏論とプログラミングの両方において重要な概念となる。圏は強く型付けされた言語に対応し、モノイドは型なし言語に対応する。型なしの言語で任意の2つの関数を合成できる(当然、プログラムを実行したときにランタイムエラーが発生する可能性はある)ように、モノイドでは任意の2つの射を合成できるからだ。

これまでに見てきたように、モノイドは単一の対象を持つ圏として記述され、その圏ではすべての論理が射の合成の規則として表現されている。この圏論的モデルは、より伝統的で集合論的なモノイドの定義と完全に等価だ。そこでは集合の2つの要素を「乗算」することで第3の要素が得られる。この「乗算」の過程はさらに細かく分割できる。すなわち、まず要素のペア1つを形成するという過程と、次にこのペアを既存の要素――それらの「積」と同一視するという過程だ。

乗算の2番目の部分、つまりペアと既存の要素との同一視を省くとどうなるだろうか? たとえば、任意の集合から始めて、ペアにできる要素をすべてペアにし、それらを新しい要素と呼ぶことができる。次に、それらの新しい要素とペアにできる要素をすべてペアにしていく。以下同様だ。これは連鎖反応だ――新しい要素を永久に追加し続けられる。その結果は、無限集合であり、ほぼモノイドとなる。ただし、モノイドには単位元と結合律も必要となる。しかし、問題ない。特別な単位元を追加し、生成されたペアの一部を同一視すれば、単位律 (unit law) と結合律をきちんと満たせる。

実際にどうなるかを簡単な例で見てみよう。2要素の集合{a,b}\{a, b\}から始めることにしよう。それらの要素を自由モノイドの生成元 (generator) と呼ぶことにする。まず、単位元となる特別な要素eeを追加する。次に、要素のすべてのペアを追加し、それらを「積」と呼ぶ。aabbの積はペア (a,b)(a, b) となる。bbaaの積はペア (b,a)(b, a) となり、aaaaの積は (a,a)(a, a) となり、bbbbの積は (b,b)(b, b) となる。eeとのペアも作れて、(a,e)(a, e)(e,b)(e, b) などになるが、それらはaabbなどと同一視することにする。結局このラウンドでは (a,a)(a, a)(a,b)(a, b)(b,a)(b, a)(b,a)(b, a)(b,a)(b, a)(b,b)(b, b) だけを追加して、結果として{e,a,b,(a,a),(a,b),(b,a),(b,b)}\{e, a, b, (a, a), (a, b), (b, a), (b, b)\}という集合が得られる。

次のラウンドでは (a,(a,b))(a, (a, b))((a,b),a)((a, b), a) といった要素を追加していく。この時点で、結合律が確実に満たされるようにする必要がある。そのため、たとえば(a,(b,a))(a, (b, a))((a,b),a)((a, b), a) を同一視する。要するに、内側の括弧は必要ないということだ。

このプロセスの最終的な結果が予想できるだろう。要素がaabbであるようなすべてのリストが作られる。実際、eeを空リストで表せば、「乗算」はリストの連接に他ならないことが分かる。

この種の構成では、要素の組み合わせとして可能なものすべてを生成し続け、同一視は最小限に――その構造の規則を維持するのにちょうど十分なだけに留める。こういった構成は自由構成と呼ばれる。上記では、生成元の集合{a,b}\{a, b\}から自由モノイドを構築した。

13.1 Haskellにおける自由モノイド

Haskellにおける二元集合は型Boolと等価であり、この集合から生成される自由モノイドは型[Bool]Boolのリスト)と等価だ。(無限リストの問題は意図的に無視している。)

Haskellのモノイドは型クラスによって定義されている:

class Monoid m where
    mempty  :: m
    mappend :: m -> m -> m

このクラスが意味しているのは、すべてのMonoidmemptyと呼ばれる中立元と、mappendと呼ばれる二項関数(乗算)を持たなければならないということだ。Haskellでは単位律と結合律は表現できず、具体的なモノイドを与えるたびにプログラマーによって検証されなければならない。

任意の型のリストがモノイドを形成するという事実は、次のインスタンス定義によって説明される:

instance Monoid [a] where
    mempty  = []
    mappend = (++)

これはリストモノイドについて、空リスト[]が単位元であり、リスト連接(++)が二項演算であることを述べている。

これまで見てきたように、型aのリストは集合aを生成元たちとする自由モノイドに対応する。その一方で、乗算を伴う自然数の集合は、多くの積が同一視されるので自由モノイドではない。次の2つを比べてみよう:

2 * 3 = 6
[2] ++ [3] = [2, 3] -- [6]と同じではない

これは簡単な例だったが、圏論では対象の中を見るのが許されないのにどうやってこの自由構成を実現できるのか、という疑問がある。そこで我々の働き者にもうひと仕事してもらうとしよう:普遍的構成だ。

2番目の興味深い疑問は、単位律と結合律が最小限必要とするよりも多くの要素を同一視すれば、ある自由モノイドから任意のモノイドを得られるのか、ということだ。これが普遍的構成から直接導かれることをお見せしよう。

13.2 自由モノイドの普遍的構成

普遍的構成についての経験を振り返れば、それは何かを構築するというより、特定のパターンに最もよく適合する対象を選択するものだと気付くだろう。なので、普遍的構成を使って自由モノイドを「構築」したいなら、選択肢となる多数のモノイドの全体を考慮する必要がある。つまり、選択のもととなるモノイドの圏がまるごと必要だ。だが、モノイドたちは圏をなすのだろうか80

まずは、単位元と二項演算による構造が加わった集合としてモノイドを見てみよう。そして、モノイドの構造を保存するような関数を射として採用する。構造を保存するそういった関数は準同型 (homomorphism) と呼ばれる。モノイド準同型は2つの要素の積を2つの要素の写し先の積へと写さなければならない:

h (a * b) = h a * h b

そして、単位元を単位元に写さなければならない。

例として、整数のリストから整数への準同型を考えよう81[2]を2に写し、[3]を3に写すなら、[2, 3]を6に写す必要がある。なぜなら、連接:

[2] ++ [3] = [2, 3]

が次のような乗算に写されるからだ:

2 * 3 = 6

ここで、個々のモノイドの内部構造については忘れて、対応する射を持つ対象としてのみ見よう。すると、モノイドの圏𝐌𝐨𝐧\mathbf{Mon}が得られる。

さて、内部構造を忘れる前に、重要な性質に注目しておいた方がよいだろう。𝐌𝐨𝐧\mathbf{Mon}のどの対象も自明に集合へと写せる。写し先の集合は単にもとのモノイドの要素の集合だ。この集合は集合 (underlying set) と呼ばれる。実際には、𝐌𝐨𝐧\mathbf{Mon}の各対象を集合に写せるだけでなく、𝐌𝐨𝐧\mathbf{Mon}の各射(準同型)も関数に写せる。これも自明なだけに見えるが、すぐに役に立つ。この𝐌𝐨𝐧\mathbf{Mon}から𝐒𝐞𝐭\mathbf{Set}への対象と射の写像は、実は関手になっている。この関手はモノイドの構造を「忘れている」。いったん通常の集合の中に入ると、もはや単位元を区別したり乗算を気にしたりすることはない。こういった関手は忘却関手 (forgetful functor) と呼ばれる。忘却関手は圏論ではよく出てくる。

これで、2つの異なる観点から𝐌𝐨𝐧\mathbf{Mon}を見たことになる。𝐌𝐨𝐧\mathbf{Mon}は対象と射を伴う他のすべての圏と同じように扱える。この観点では、モノイドの内部構造は見えない。𝐌𝐨𝐧\mathbf{Mon}の中の特定の対象について言えるのは、それ自身や他の対象と射を通じて接続しているということだけだ。射の「乗算」表――つまり、合成の規則――は、もう一方の観点、すなわち集合としてのモノイドから導かれる。圏論に進んだことでこの観点が完全に失われたわけではない――忘却関手を通じてならアクセスできる。

普遍的構成を適用するには、モノイドの圏を探索して自由モノイドの最良の候補を選べるようにするための特別な性質を定義する必要がある。しかし、自由モノイドはその生成元たちによって定義される。別の生成元たちを選べば、生成される自由モノイドは変わる(BoolのリストとIntのリストは違う)。我々の構成は、生成元の集合から始めなければならない。つまり、集合に戻ってきたということだ!

ここで忘却関手が役立つことになる。忘却関手によってモノイドをレントゲン撮影できる。そして、それらのかたまりたちのレントゲン写真から生成元たちを特定できる。その仕組みは以下のとおりだ:

生成元の集合xxから始める。この集合は𝐒𝐞𝐭\mathbf{Set}の対象だ。

マッチさせようとしているパターンは、モノイドmm――𝐌𝐨𝐧\mathbf{Mon}の対象――と𝐒𝐞𝐭\mathbf{Set}内の関数ppで構成されている:

p :: x -> U m

ここで、UU𝐌𝐨𝐧\mathbf{Mon}から𝐒𝐞𝐭\mathbf{Set}への忘却関手だ。これは奇妙な混成パターンだ――半分は𝐌𝐨𝐧\mathbf{Mon}で半分は𝐒𝐞𝐭\mathbf{Set}になっている。

意図としては、mmのレントゲン写真に写った生成元の集合が関数ppによって特定されるようにするということだ。関数たちが集合内の点たち82をきちんと特定できない(それらを潰す)かもしれないことは問題ない。普遍的構成によってこのパターンの最良の代表が選ばれ、すべてが整理されるだろう83

候補間の順位付けも定義しなければならない。もう1つの候補があるとしよう。モノイドnnと、そのレントゲン写真に写った生成元たちを特定しようとする関数だ:

q :: x -> U n

以下のような条件を満たす場合、mmnnよりも優れていると言うことにしよう。その条件とは、モノイドの射(準同型なので構造を保存する):

h :: m -> n

があり、そのUUの下の像(UUは関手なので射を関数に写す)がppを通じてqqを分解することだ:

q = U h . p

ppmm内で生成元たちを選択し、qqnn内で「同じ」生成元たちを選択していると見なすなら、hhはこれらの生成元たちを2つのモノイドの間で写していると見なせる。hhは、定義より、モノイドの構造を保存することを思い出してほしい。これは、一方のモノイドにおける2つの生成元の積が、もう一方のモノイドにおける対応する2つの生成元の積に写されることなどを意味する。

モノイドの順位付け

この順位付けは、最も優れた候補、すなわち自由モノイドを見つけるために使われる。定義は次のとおりだ:

(関数ppを伴う)mmを生成元xxを伴う自由モノイドと呼ぶのは、mmから(関数qqを伴う)他の任意のモノイドnnへの、前述の分解特性を満たす一意なhhが存在する場合、かつその場合に限る。

ところで、これは2番目の疑問に対する答えになっている。関数UhU hには、UmU mの複数の要素をUnU nの1つの要素へ潰す力がある。ここで、潰すことは自由モノイドのいくつかの要素を同一視することに対応する。したがって、生成元たちとしてxxを持つモノイドはどれも、xxに基づく自由モノイドから、いくつかの要素を同一視することによって得られる。自由モノイドとは、最小限のものだけしか同一視されていないモノイドのことだ。

自由モノイドについては、随伴について話すときにまた戻ってくることにしよう。

13.3 課題

  1. モノイドの準同型が単位元を保存する、という条件は(当初は私もそう思ったように)冗長だと思うかもしれない。何しろ、すべてのaaについて次が言えるのだから:

    h a * h e = h (a * e) = h a

    つまり、heh eは右単位元のように働く(同様に、左単位元のようにも働く)。問題は、すべてのaaに対してhah aを集めてきたものを考えると、終域モノイドの部分モノイドしかカバーできないということだ。hhの像の外部に「真の」単位元が存在するかもしれない。モノイド間で乗算を保存する同型は自動的に単位元を保存することになることを示せ。

  2. 連接を二項演算とする整数リストから乗算を二項演算とする整数へのモノイド準同型について考える。空リスト[]の像は何か? すべての単要素リストは、それが含む整数に写されるとする。たとえば、[3]は3に写される。[1, 2, 3, 4]の像は何か? 整数12に写されるリストはいくつあるか? 2つのモノイド間に他の準同型はあるか?

  3. 単元集合によって生成される自由モノイドとは何か? それが何と同型なのか分かるか?

14 表現可能関手

そろそろ集合について少し話すべき時が来た。数学者たちは集合論に対して愛憎する関係にある。集合論は数学にとってのアセンブリー言語だ――少なくともかつてはそうだった。圏論はある程度、集合論から距離を置こうとする。たとえば、すべての集合の集合は存在しないが、すべての集合の圏𝐒𝐞𝐭\mathbf{Set}なら存在する、というのはよく知られた事実だ。これは良い。一方で我々は、圏内の任意の2対象間の射たちは集合を形成する、と仮定した。そしてそれをhom集合と名付けさえした。公平のために言うと、圏論には射たちが集合をなさないことがあるような分野もある84。その代わりにそれらは別の圏の対象となる。hom集合の代わりにhom対象を使う圏は豊穣圏 (enriched category) と呼ばれる。だが、以下では古き良きhom集合を持つ圏に留まることにする。

集合は、圏論における対象たちから取り出せる特徴のない塊、というのが最も近い。集合は要素を含むが、それらについて多くは語れない。有限集合なら要素数を数えられる。基数 (cardinal number) を使えば、無限集合の要素数をある意味で数えられる。たとえば、自然数の集合も実数の集合も無限集合だが、前者は後者よりも小さい。しかし、驚くかもしれないが、有理数の集合は自然数の集合と同じ大きさだ。

それ以外には、集合に関するすべての情報は、集合間の関数――特に、同型と呼ばれる可逆関数――として表せる。どこからどう見ても同型集合は同一だ。数学基礎論の研究者の逆鱗に触れる前に、等しさと同型には根本的に重要な区別があることを説明しておこう。これは数学の最新分野であるホモトピー型理論 (Homotopy Type Theory, HoTT) の主要な関心事の1つだ。ここでHoTTについて触れる理由は、それが計算からインスピレーションを得た純粋な数学理論で、主唱者の1人であるVladimir Voevodskyによる大きな発見が定理証明器Coqを研究しているときに得られたものだからだ。数学とプログラミングの相互作用は双方向なのだ。

集合に関する重要な教訓は、別種の要素からなる集合同士を比較しても問題ないということだ。たとえば、任意の自然変換の集合は何らかの射の集合と同型である、などと主張できる。集合は集合にすぎないからだ。この場合の同型は単に、一方の集合に属するどの自然変換に対しても他方の集合に属する一意な射が存在し、その逆もまた成り立つことを意味する。それらは互いにペアにできる。リンゴとオレンジが異なる圏の対象なら比較できないが、リンゴの集合とオレンジの集合は比較できる。多くの場合、圏論の問題を集合論の問題に変換すれば、必要な洞察が得られ、有用な定理を証明することさえ可能になる。

14.1 Hom関手

すべての圏には、𝐒𝐞𝐭\mathbf{Set}への写像の正統な族が用意されている。それらの写像は実際には関手であるため、圏の構造を保存している。そのような写像を1つ構築しよう。

𝐂\mathbf{C}内のある対象aaを固定し、同じく𝐂\mathbf{C}内の別の対象xxを選択しよう。Hom集合𝐂(a,x)\mathbf{C}(a, x) は集合であり、𝐒𝐞𝐭\mathbf{Set}の対象だ。aaを固定したままxxを変化させると、𝐂(a,x)\mathbf{C}(a, x)𝐒𝐞𝐭\mathbf{Set}内で変化する。それゆえ、xxから𝐒𝐞𝐭\mathbf{Set}への写像が得られる。

hom集合を2番目の引数に関する写像と見なしていることを強調したい場合は𝐂(a,)\mathbf{C}(a, -) と表記する。ここで、ダッシュは引数のプレースホルダーを表している。

この対象の写像は射の写像へと容易に拡張できる。𝐂\mathbf{C}内の任意の2対象xxyyの間の射ffを考えてみよう。先ほど定義した写像によって、対象xxは集合𝐂(a,x)\mathbf{C}(a, x) に写され、対象yy𝐂(a,y)\mathbf{C}(a, y) に写される。この写像が関手となるなら、ffは2つの集合間の関数𝐂(a,x)𝐂(a,y)\mathbf{C}(a, x) \to \mathbf{C}(a, y) に写されなければならない。

この関数を点ごとに、つまり、引数ごとに個別に定義しよう。引数として𝐂(a,x)\mathbf{C}(a, x) の任意の要素を1つ選択する必要がある。それをhhと呼ぶことにする。射たちは端と端が一致すれば合成できる。たまたまhhの終点がffの始点と一致しているので、それらの合成: fhayf \circ h \Colon a \to yaaからyyへの射となる。したがって、これは𝐂(a,y)\mathbf{C}(a, y) の元だ。

Hom関手

先ほど、𝐂(a,x)\mathbf{C}(a, x) から𝐂(a,y)\mathbf{C}(a, y) への関数を見つけた。これはffの像として使える。混乱のおそれがない場合は、この持ち上げられた関数を𝐂(a,f)\mathbf{C}(a, f)と記し、射hhに対する作用を次のように記すことにしよう: 𝐂(a,f)h=fh\mathbf{C}(a, f) h = f \circ h この構成はどの圏でも機能するので、Haskellの型の圏でも機能するはずだ。Haskellでは、hom関手はReader関手としてよく知られている:

type Reader a x = a -> x
instance Functor (Reader a) where
    fmap f h = f . h

さて、Hom集合の始点を固定する代わりに終点を固定すると何が起こるかを考えてみよう。言い換えれば、写像𝐂(,a)\mathbf{C}(-, a) も関手なのかを問うている。答えはそのとおり、関手なのだが、共変関手ではなく反変関手になる。射の端と端を同様にマッチングするとffとの後合成となり、𝐂(a,)\mathbf{C}(a, -) の場合のような前合成ではないからだ。

この反変関手はHaskellですでに見た。それはOpと呼ばれていた。

type Op a x = x -> a
instance Contravariant (Op a) where
    contramap f h = h . f

最後に、両方の対象を変化させると、プロ関手𝐂(,=)\mathbf{C}(-, =) が得られる。これは1つ目の引数について反変で、2つ目の引数について共変だ(2つの引数が独立して変化することを強調するため、2つ目のプレースホルダーとしてダブルダッシュを使った)。このプロ関手については、関手性について述べたときにすでに見た:

instance Profunctor (->) where
  dimap ab cd bc = cd . bc . ab
  lmap = flip (.)
  rmap = (.)

重要な教訓として、対象たちをhom集合へ写すことは関手的である、という観察はすべての圏に当てはまる。反変性は反対圏から写すことと等価なので、この事実は簡潔にこう記せる: C(,=)𝐂𝑜𝑝×𝐂𝐒𝐞𝐭C(-, =) \Colon \mathbf{C}^\mathit{op} \times \mathbf{C} \to \mathbf{Set}

14.2 表現可能関手

これまでに見たように、𝐂\mathbf{C}内の対象aaの選択ごとに𝐂\mathbf{C}から𝐒𝐞𝐭\mathbf{Set}への関手が得られる。構造を保存する𝐒𝐞𝐭\mathbf{Set}へのこの種の写像は表現 (representation) とよく呼ばれる。つまり、𝐂\mathbf{C}における対象や射を𝐒𝐞𝐭\mathbf{Set}内の集合や関数として表現しているということだ。

関手𝐂(a,)\mathbf{C}(a, -) 自体を指して表現可能関手と呼ぶこともある。より一般には、あるaaの選択に対してhom関手と自然同型である関手FFはすべて表現可能 (representable) 関手と呼ばれる。このような関手は必然的に集合値 (𝐒𝐞𝐭\mathbf{Set}-valued) 関手となる。𝐂(a,)\mathbf{C}(a, -) がそうだからだ。

以前述べたように、同型集合は同一と見なされることが多い。より一般には、圏の同型対象は同一と見なされる。対象たちは他の対象たち(およびそれら自身)との射による関係だけしか構造を持たないからだ。

たとえば、モノイドの圏𝐌𝐨𝐧\mathbf{Mon}について前に説明した。それは最初は集合たちでモデル化されていた。ただし、それらの集合によるモノイドの構造を保存する関数のみを射として選ぶように気を付けた。そのため、𝐌𝐨𝐧\mathbf{Mon}内の2つの対象は、同型なら、つまり可逆な射が間にあるなら、全く同じ構造を持つ。それらが基づく集合と関数を覗き見ると、一方のモノイドの単位元がもう一方のモノイドの単位元へと写され、2つの要素の積がそれらの要素を写したものの積へと写されているのが分かるだろう。

同じ推論が関手にも適用できる。2つの圏の間の関手は圏を形成し、そこでは自然変換が射の役割を果たしている。したがって、2つの関手の間に可逆な自然変換があれば、それらは同型であり、同一と見なせる。

この観点から表現可能関手の定義を分析してみよう。FFが表現可能となるには次が必要だ:𝐂\mathbf{C}内に対象aaがあり、𝐂(a,)\mathbf{C}(a, -)からFFへの自然変換α\alphaがあり、逆方向に別の自然変換β\betaがあり、それらの合成が恒等自然変換である。

ある対象xxにおけるα\alphaの成分を見てみよう。それは𝐒𝐞𝐭\mathbf{Set}内の関数だ: αx𝐂(a,x)Fx\alpha_x \Colon \mathbf{C}(a, x) \to F x この変換の自然性条件は、xxからyyへの任意の射ffについて、次の関係を表す図式が可換となることを示している: Ffαx=αy𝐂(a,f)F f \circ \alpha_x = \alpha_y \circ \mathbf{C}(a, f) Haskellでは、自然変換を多相関数で置き換えることになる:

alpha :: forall x. (a -> x) -> F x

ここで使った量化子forallは必須ではない。自然性条件:

fmap f . alpha = alpha . fmap f

はパラメトリシティ (parametricity) によって自動的に満たされる(この自然性条件は前述のtheorems for freeのひとつだ)。これは、左辺のfmapが関手FFによって定義され、右辺のfmapがreader関手によって定義されるという理解に基づく。readerのfmapは単なる関数の前合成85なので、さらにはっきりと書ける。𝐂(a,x)\mathbf{C}(a, x) の要素であるhhに作用したときの自然性条件は次のように簡潔に書ける:

fmap f (alpha h) = alpha (f . h)

もう1つの変換betaは方向が逆だ:

beta :: forall x. F x -> (a -> x)

この関数も自然性条件を満たす必要があり、そしてalphaの逆関数でなければならない:

alpha . beta = id = beta . alpha

後で説明するとおり、FaF aが空でないならば常に𝐂(a,)\mathbf{C}(a, -) から任意の集合値関手への自然変換が存在する(米田の補題)が、この変換は可逆であるとは限らない。

Haskellでリスト関手を用い、Intaとした例を挙げよう。以下が具体的な自然変換の一例だ:

alpha :: forall x. (Int -> x) -> [x]
alpha h = map h [12]

勝手な数として12を選び、それを使って単要素リストを作成した。次に、このリストに関数hfmapし、hの戻り値の型のリストを取得する。(実際には、そのような変換は整数のリストと同じくらいたくさんある。)

自然性条件はmapfmapのリスト版)の合成可能性と等価だ:

map f (map h [12]) = map (f . h) [12]

しかし、逆変換を見つけるには、任意の型xのリストをもとにxを返す関数を探さなくてはならない。

beta :: forall x. [x] -> (Int -> x)

headか何かを使ってリストからxを取得しようと考えたかもしれないが、空のリストには使えない。(Intの箇所の)型aの選択肢としてここで使えるものがないことに注目してほしい。すなわち、リスト関手は表現可能ではない。

Haskellの(自己)関手がコンテナーに少し似ていると言ったのを覚えているだろうか? 同じように、表現可能関手は関数呼び出しの結果をメモ化して保存するためのコンテナーと見なせる(Haskellでのhom集合の要素は単なる関数だ)。表現対象 (representing object)、すなわち𝐂(a,)\mathbf{C}(a, -) における型aaはキー型と見なせて、それを用いれば表化 (tabulate) された関数値にアクセスできる。ここでの変換alphatabulateと呼ばれ、その逆のbetaindexと呼ばれる。以下に(少し単純化した)Representableのクラス定義を示す:

class Representable f where
   type Rep f :: *
   tabulate :: (Rep f -> x) -> f x
   index    :: f x -> Rep f -> x

表現型、すなわちaaが、ここではRep fと呼ばれ、Representableの定義の一部であることに注目してほしい。スターはRep fが型である(型構成子や他のエキゾチックな種ではない)ことを意味する86

無限リストや無限ストリームは、空ではあり得ず、表現可能だ:

data Stream x = Cons x (Stream x)

それらはIntegerを引数に取る関数の値をメモ化したものと見なせる。(厳密に言えば、非負整数を使うべきだが、コードを複雑にしたくなかった87。)

このような関数をtabulateにするには、値の無限ストリームを作成する。もちろん、これが可能なのはHaskellが遅延評価だからだ。つまり、値は必要になったとき評価される。メモ化された値にアクセスするにはindexを使う。

instance Representable Stream where
    type Rep Stream = Integer
    tabulate f = Cons (f 0) (tabulate (f . (+1)))
    index (Cons b bs) n = if n == 0 then b else index bs (n - 1)

任意の戻り値型を持つ関数の族をすべてカバーするような単一のメモ化スキームを実装できるのは興味深い。

反変関手の表現可能性も同様に定義される。ただし、𝐂(,a)\mathbf{C}(-, a) の2番目の引数を固定する。あるいは、𝐂𝑜𝑝\mathbf{C}^\mathit{op}から𝐒𝐞𝐭\mathbf{Set}への関手を考えるのでも等価だ。𝐂𝑜𝑝(a,)\mathbf{C}^\mathit{op}(a, -)𝐂(,a)\mathbf{C}(-, a) と同じだからだ。

表現可能性には興味深いひねりがある。デカルト閉圏では、hom集合を内部的には冪対象として扱えることを思い出してほしい。hom集合𝐂(a,x)\mathbf{C}(a, x)xax^aと等価で、表現可能関手FFに対してはa=F-^a = Fと書ける。

試しに、両辺の対数をとってみるとa=𝐥𝐨𝐠Fa = \mathbf{log}Fとなる。

もちろん、これは純粋に形式的な変換だが、対数の性質を多少知っているなら非常に便利だ。特に、直積型に基づく関手は直和型を用いて表現でき88、直和型の関手は一般に表現可能ではないことが知られている(例:リスト関手)。

最後に、表現可能関手が同じものに対する2種類の実装――ひとつは関数、もうひとつはデータ構造――を与えることに注目してほしい。それらの内容は全く同じだ――同じキーを使えば同じ値が取得される。それが、語ろうとしていた「同一性」の感覚だ。2つの自然同型な関手は内容に関する限り同一だ。その一方で、2つの表現は異なる方法で実装されることが多く、パフォーマンス特性が異なる可能性がある。メモ化はパフォーマンス改善策として使われ、実行時間の大幅な短縮につながる可能性がある。同じ計算を背景とする異なる表現を生成できることは、実用上の価値が非常に高い。そのため、驚くべきことに、圏論はパフォーマンスを全く考慮しないにもかかわらず、実用上の価値を持つ別の実装を探求する十分な機会を与えてくれる。

14.3 課題

  1. Hom関手が𝐂\mathbf{C}内の恒等射を𝐒𝐞𝐭\mathbf{Set}内の対応する恒等関数に写すことを示せ。

  2. Maybeが表現可能でないことを示せ。

  3. Reader関手は表現可能か?

  4. Stream表現を使って、引数を2乗する関数をメモ化せよ。

  5. Streamに対するtabulateindexが実際に互いに逆であることを示せ。(ヒント:数学的帰納法を使う。)

  6. 次の関手:

    Pair a = Pair a a

    は表現可能だ。それを表現する型が分かるだろうか? tabulateindexを実装せよ。

14.4 参考文献

  1. 表現可能関手についてのCatstersの動画89

15 米田の補題

圏論における構成物のほとんどは、より具体的な他の数学の分野での結果を一般化したものだ。積・余積・モノイド・冪などは、圏論よりずっと前から知られていた。それらは別の数学の分野では別の名前で知られていたかもしれない。集合論におけるデカルト積、順序理論における交わり (meet)、論理学における論理積――これらはすべて圏論的な積という抽象概念に対応する具体例だ。

米田の補題は、この観点から、数学の他の分野では全くと言ってよいほど前例がないような圏一般に関する包括的な主張として際立っている。一番似ているのは群論におけるケイリーの定理(すべての群はある集合の置換群と同型)90だという説もある。

米田の補題の問題設定は、𝐒𝐞𝐭\mathbf{Set}への関手FFを持つような任意の圏𝐂\mathbf{C}についてのものだ。すでに前章で述べたように、集合値関手のいくつかは表現可能、つまりhom関手と同型だ。米田の補題は、すべての集合値関手がhom関手たちから自然変換によって得られることを示し、そのようなすべての変換を明示的に列挙する。

自然変換について話したとき、自然性条件は制限が非常に強いものになる場合があると述べた。1つの対象における自然変換の成分を定義するとき、射を介して接続されている別の対象へとその成分を「トランスポート」できるほどに自然性が強いことがある。もとの圏と行き先の圏で対象間の射が多いほど、自然変換の成分をトランスポートするための制約が厳しくなる。𝐒𝐞𝐭\mathbf{Set}はたまたま射が豊富な圏だ。

米田の補題から言えるのは、あるhom関手と他の任意の関手FFとの間の自然変換が、その単一成分の値をある1点について指定するだけで完全に決定されるということだ! 残りの成分は単に自然性条件に従って決まる。

では、米田の補題に関わる2つの関手について、その間の自然性条件をおさらいしよう。1番目の関手はhom関手だ。それは𝐂\mathbf{C}内の任意の対象xxを射の集合𝐂(a,x)\mathbf{C}(a, x) に写す。ここで、aa𝐂\mathbf{C}内の固定された対象だ。また、すでに見たとおり、そのhom関手はxxからyyへの射ffをすべて𝐂(a,f)\mathbf{C}(a, f) に写す。

2番目の関手は任意の集合値関手FFだ。

この2つの関手の間の自然変換をα\alphaと呼ぶことにしよう。𝐒𝐞𝐭\mathbf{Set}内を扱っているので、αx\alpha_xαy\alpha_yなどの自然変換の成分は集合間の通常の関数にすぎない: αx𝐂(a,x)Fxαy𝐂(a,y)Fy \begin{gathered} \alpha_x \Colon \mathbf{C}(a, x) \to F x \\ \alpha_y \Colon \mathbf{C}(a, y) \to F y \end{gathered}

そして、これらは単なる関数なので、特定の点での値を見られる。だが、集合𝐂(a,x)\mathbf{C}(a, x) 内の点とは何だろうか? 鍵となる観察はこうだ:集合𝐂(a,x)\mathbf{C}(a, x) 内のすべての点は、aaからxxへの射hhでもある。

したがって、α\alphaについての自然性の四角い図式: αy𝐂(a,f)=Ffαx\alpha_y \circ \mathbf{C}(a, f) = F f \circ \alpha_x の両辺をhhに作用させると、点ごとの等式になる: αy(𝐂(a,f)h)=(Ff)(αxh)\alpha_y (\mathbf{C}(a, f) h) = (F f) (\alpha_x h) 前の節でhom関手𝐂(a,)\mathbf{C}(a, -) の射ffへの作用を、次のような前合成として定義したことを思い出しただろう: 𝐂(a,f)h=fh\mathbf{C}(a, f) h = f \circ h これにより次が導かれる: αy(fh)=(Ff)(αxh)\alpha_y (f \circ h) = (F f) (\alpha_x h) この条件がどれほど強いかはx=ax = aの場合に特化させれば分かる。

この場合、hhaaからaaへの射となる。そのような射が少なくとも1つ存在するのは分かっている。h=𝐢𝐝ah = \mathbf{id}_aだ。これを代入してみよう: αyf=(Ff)(αa𝐢𝐝a)\alpha_y f = (F f) (\alpha_a \mathbf{id}_a) 何が起きたか注目してほしい。左辺はαy\alpha_y𝐂(a,y)\mathbf{C}(a, y) の任意の要素ffに作用させている。そして、その結果は𝐢𝐝a\mathbf{id}_aにおけるαa\alpha_aという単一の値によって完全に決まる。そのような値は任意に選べて、それによって自然変換が生成される。αa\alpha_aたちの値は集合FaF aに含まれるので、FaF aのどの点からも何らかのα\alphaが定義される。

逆に、𝐂(a,)\mathbf{C}(a, -) からFFへの自然変換α\alphaが与えられた場合、𝐢𝐝a\mathbf{id}_aにおいて評価すればFaF aの点を得られる。

以上より、米田の補題が証明された:

𝐂(a,)\mathbf{C}(a, -) からFFへの自然変換とFaF aの要素との間には1対1の対応がある。

言い換えれば: 𝑁𝑎𝑡(𝐂(a,),F)Fa\mathit{Nat}(\mathbf{C}(a, -), F) \cong F a となる。あるいは、[𝐂,𝐒𝐞𝐭][\mathbf{C}, \mathbf{Set}]という表記で𝐂\mathbf{C}𝐒𝐞𝐭\mathbf{Set}の間の関手圏を表すと、自然変換の集合は単にその圏のhom集合であり、次のように書ける: [𝐂,𝐒𝐞𝐭](𝐂(a,),F)Fa[\mathbf{C}, \mathbf{Set}](\mathbf{C}(a, -), F) \cong F a この対応が実際には自然同型である仕組みについては後で説明する。

さて、この結果について直観的に理解してみよう。最も驚くべきことは、自然変換全体の結晶化が、𝐢𝐝a\mathbf{id}_aにおいて割り当てた値というたった1粒の種から始まることだ。結晶化はその1点から自然性条件に従って広がっていく。そして𝐒𝐞𝐭\mathbf{Set}内に𝐂\mathbf{C}の像を溢れさせる。そこで、まずは𝐂(a,)\mathbf{C}(a, -) の下で𝐂\mathbf{C}の像について考えてみたい。

aa自体の像から始めよう。aaは、hom関手𝐂(a,)\mathbf{C}(a, -) の下では集合𝐂(a,a)\mathbf{C}(a, a) に写される。一方、関手FFの下では集合FaF aに写される。自然変換の成分αa\alpha_aは、𝐂(a,a)\mathbf{C}(a, a) からFaF aへの何らかの関数となる。集合𝐂(a,a)\mathbf{C}(a, a) の中の1点だけに注目することにしよう。具体的には射𝐢𝐝a\mathbf{id}_aに対応する点だ。集合内の1点にすぎないという事実を強調するために、それをppと呼ぼう。成分αa\alpha_appFaF a内のある点qqに写すはずだ。どんなqqを選択しても一意な自然変換が得られることを説明しよう。

1つ目の主張は、1点qqを選択すれば関数αa\alpha_aの残りが一意に決まる、というものだ。実際に、aaからaaへのある射ggに対応する他の任意の点pp'𝐂(a,a)\mathbf{C}(a, a) 内で選んでみよう。ここで米田の補題の魔法が起こり、ggは集合𝐂(a,a)\mathbf{C}(a, a) 内の点pp'と見なせる。同時に、それは集合間の2つの関数を選択する。確かに射ggは、hom関手では関数𝐂(a,g)\mathbf{C}(a, g) に写され、FFではFgF gに写される。

ここで、もとのppに対する𝐂(a,g)\mathbf{C}(a, g) の作用を考えてみよう。覚えているとおり、pp𝐢𝐝a\mathbf{id}_aに対応する。それは前合成g𝐢𝐝ag \circ \mathbf{id}_aとして定義され、ggと同じであり、点pp'に対応する。したがって、射ggは、ppに作用するとpp'、すなわちggを生成するような関数に写される。ぐるりと一周した!

さて、qqに対するFgF gの作用を考えてみよう。これはFaF a内のある点qq'となる。自然性の四角い図式[訳注:αa𝐂(a,g)=Fgαa\alpha_a \circ \mathbf{C}(a, g) = F g \circ \alpha_ap=𝐢𝐝ap = \mathbf{id}_aに作用させたもの。いま、𝐂(a,g)p=g\mathbf{C}(a, g) p = gであり、p=gp' = gとおいていたので、左辺がαap\alpha_a p'となり、またq=αapq = \alpha_a pであったので、右辺がFg(αap)=Fgq=qF g (\alpha_a p) = F g q = q'となる。]を完成させるには、pp'αa\alpha_aによってqq'に写される必要がある。任意のpp'(任意のgg)を選択し、αa\alpha_aの下でのその写し先[訳注:qq'、すなわちFgqF g qのこと。]を導出した。したがって、関数αa\alpha_aは完全に決定される。

2つ目の主張は、𝐂\mathbf{C}内でaaに接続された対象xxに対してαx\alpha_xが一意に決定される、というものだ。これも同様の論法による。ただし、ここではさらに2つの集合𝐂(a,x)\mathbf{C}(a, x)FxF xがあり、aaからxxへの(訳注:任意に選んだ)射ggは、hom関手の下では次に写され: 𝐂(a,g)𝐂(a,a)𝐂(a,x)\mathbf{C}(a, g) \Colon \mathbf{C}(a, a) \to \mathbf{C}(a, x) FFの下では次に写される: FgFaFxF g \Colon F a \to F x ここでも、ppに作用した場合の𝐂(a,g)\mathbf{C}(a, g) は前合成g𝐢𝐝ag \circ \mathbf{id}_aによって与えられ、𝐂(a,x)\mathbf{C}(a, x) 内の点pp'となる。自然性により、pp'に作用するαx\alpha_xの値はこう決まる: q=(Fg)qq' = (F g) q pp'は任意に決めていたので、関数αx\alpha_x全体が決定される。

𝐂\mathbf{C}内にaaと接続されていない対象がある場合はどうなるだろう? それらすべてが𝐂(a,)\mathbf{C}(a, -) の下で空集合に写される。空集合は集合の圏における始対象であることを思い出してほしい。これは、この集合から他のどの集合へも一意な関数があることを意味する。その関数をabsurdと呼んだ。したがって、ここでも自然変換の成分には選択の余地がなく、absurdしかあり得ない。

米田の補題を理解する方法の1つは、集合値関手の間の自然変換は関数の族にすぎず、関数は一般には非可逆だと気付くことだ。関数は情報を潰すこともあり、終域の一部しかカバーしないこともある。非可逆でない関数は、可逆なもの、つまり同型たちだけだ。したがって、構造を保存する最良の集合値関手たちは表現可能関手だということになる。それらはhom関手か、あるいはhom関手と自然同型な関手のどちらかだ。その他の関手FFはすべてhom関手を非可逆変換することで得られる。そのような変換は、情報を失わせるだけでなく、関手FFによる𝐒𝐞𝐭\mathbf{Set}内の像のごく一部しかカバーしない可能性がある。

15.1 Haskellにおける米田の補題

Haskellのhom関手には、すでにreader関手という名前で出会っている:

type Reader a x = a -> x

Readerは射たち(ここでは関数たち)を前合成で写す:

instance Functor (Reader a) where
    fmap f h = f . h

米田の補題によれば、reader関手は他の任意の関手へ自然に写せる。

自然変換は多相関数だ。さて、任意の関手Fについて、reader関手からの写像を考えられる:

alpha :: forall x . (a -> x) -> F x

いつものように、forallは必須ではないが、自然変換のパラメトリック多相性を強調するために明示的に書くことにしている。

米田の補題によれば、これらの自然変換はF aの要素たちと1対1に対応している。

forall x . (a -> x) -> F x ≅ F a

この等式の右辺は、通常はデータ構造と見なしているものだった。一般化されたコンテナーとして関手を解釈したのを覚えているだろうか? F aaのコンテナーだ。一方で、左辺は関数を引数に取る多相関数だ。米田の補題によれば、この2つの表現は等価だ――それらは同じ情報を含んでいる。

別の言い方をすると以下のようになる。次のような型の多相関数:

alpha :: forall x . (a -> x) -> F x

を与えてくれればaのコンテナーを作成してみせよう。ここで使うトリックは米田の補題の証明で使ったものだ。つまり、この関数をidで呼び出すことで型F aの要素を得る:

alpha id :: F a

逆もまた真だ。型F aの任意の値:

fa :: F a

について、適切な型の多相関数:

alpha h = fmap h fa

を定義できる。2つの表現の間は簡単に行き来できるということだ。

表現が複数ある利点は、一方が他方よりも合成しやすかったり、用途によってはより効率的だったりすることだ。

この原則の最も単純な例は、コンパイラーの構成でよく使われるコード変換である、継続渡し形式 (continuation passing style, CPS) だ。これは米田の補題を恒等関手へ最も単純に適用したものだ。Fを恒等関手に置き換えると、次のようになる:

forall r . (a -> r) -> r ≅ a

この等式は、任意の型aaに対する「ハンドラー」を取る関数によって置き換えられる、と解釈できる。ハンドラーは、aを受け入れ、残りの計算――継続――を実行する関数だ。(型rは通常、ある種のステータスコードをカプセル化している。)

このスタイルのプログラミングは、UI、非同期システム、並行プログラミングではごく一般的だ。CPSの欠点は、制御の反転を伴うことだ。コードが生産者と消費者(ハンドラー)に分割され、簡単には合成できない。Webプログラミングの経験がある人なら誰でも、ステートフルなハンドラーとやり取りするスパゲッティコードの悪夢をよく知っている91。後で見るように、関手とモナドを慎重に使えばCPSの合成的な特性をいくらか取り戻せる。

15.2 余米田の補題

いつものように、射の方向を逆にすればおまけの構成が得られる。米田の補題を反対圏𝐂𝑜𝑝\mathbf{C}^\mathit{op}に適用すれば反変関手の間の写像が得られる。

同様に、hom関手の始点となる対象の代わりに終点となる対象を固定することで、余米田の補題を導出できる。𝐂\mathbf{C}から𝐒𝐞𝐭\mathbf{Set}への反変hom関手を取れる。すなわち𝐂(,a)\mathbf{C}(-, a) だ。反変版の米田の補題は、この関手から他の任意の反変関手FFへの自然変換と、集合FaF aの要素との間に1対1の対応を確立する: 𝐍𝐚𝐭(𝐂(,a),F)Fa\mathbf{Nat}(\mathbf{C}(-, a), F) \cong F a Haskell版の余米田の補題は次のようになる:

forall x . (x -> a) -> F x ≅ F a

一部の文献では反変版の方を米田の補題と呼んでいるので注意してほしい。

15.3 課題

  1. 米田の同型を成す2つのHaskellの関数phipsiが互いに逆であることを示せ。

    phi :: (forall x . (a -> x) -> F x) -> F a
    phi alpha = alpha id
    
    psi :: F a -> (forall x . (a -> x) -> F x)
    psi fa h = fmap h fa
  2. 離散圏 (discrete category) は、対象はあるが恒等射以外の射はない圏だ。米田の補題はそのような圏の関手でどのように役立つだろうか?

  3. unit型のリスト[()]は長さ以外の情報を含まない。したがって、データ型としては、非負整数を表したものと見なせる。空リストは0を表し、単リスト[()](型ではなく値)は1を表し、以下同様だ。このデータ型の別の表現を、リスト関手に対する米田の補題を使って構成せよ。

15.4 参考文献

  1. Catstersの動画92

16 米田埋め込み

以前見たとおり、圏𝐂\mathbf{C}について対象aaを固定すると、写像𝐂(a,)\mathbf{C}(a, -)𝐂\mathbf{C}から𝐒𝐞𝐭\mathbf{Set}への(共変)関手となる。 x𝐂(a,x)x \to \mathbf{C}(a, x) (Hom集合𝐂(a,x)\mathbf{C}(a, x)集合なのでこの関手の終域は𝐒𝐞𝐭\mathbf{Set}だ。) この写像はhom関手と呼ばれる。射に対するこの関手の作用についてもすでに定義したことを思い出してほしい。

さて、この写像においてaaを変化させてみよう。すると、Hom関手𝐂(a,)\mathbf{C}(a, -) を任意のaaに対して割り当てる新しい写像が得られる。 a𝐂(a,)a \to \mathbf{C}(a, -) これは圏𝐂\mathbf{C}の対象たちから関手たち、すなわち関手圏の対象たちへの写像だ(関手圏については自然変換(第10章3節) を参照)。𝐂\mathbf{C}から𝐒𝐞𝐭\mathbf{Set}への関手圏を[𝐂,𝐒𝐞𝐭][\mathbf{C}, \mathbf{Set}]と表記しよう。また、覚えているかもしれないが、Hom関手は表現可能関手(第14章)の原型だった。

2つの圏の間に対象の写像があるのを見るたび、そのような写像が関手でもあるかを問うのは自然なことだ。言い換えると、一方の圏の射をもう一方の圏の射へと持ち上げられるか、ということだ。𝐂\mathbf{C}の射は𝐂(a,b)\mathbf{C}(a, b) の要素にすぎないが、関手圏[𝐂,𝐒𝐞𝐭][\mathbf{C}, \mathbf{Set}]の射は自然変換だ。つまり、ここでは射から自然変換への写像を探していることになる。

fabf \Colon a \to bに対応する自然変換が見つかるか見てみよう。そのため、まずaabbが何に写されるか見てみよう。それらは2つの関手𝐂(a,)\mathbf{C}(a, -)𝐂(b,)\mathbf{C}(b, -) に写される。求めるものは、これら2つの関手の間の自然変換だ。

そして、ここで秘訣がある。米田の補題: [𝐂,𝐒𝐞𝐭](𝐂(a,),F)Fa[\mathbf{C}, \mathbf{Set}](\mathbf{C}(a, -), F) \cong F a を使い、さらに総称的なFFをhom関手𝐂(b,)\mathbf{C}(b, -) で置き換える。すると、次が得られる: [𝐂,𝐒𝐞𝐭](𝐂(a,),𝐂(b,))𝐂(b,a)[\mathbf{C}, \mathbf{Set}](\mathbf{C}(a, -), \mathbf{C}(b, -)) \cong \mathbf{C}(b, a)

これはまさに探していた2つのhom関手の間の自然変換だが、少しねじれがある。つまり、自然変換と射との対応は見つけたのだが、射――𝐂(b,a)\mathbf{C}(b, a) の要素――の向きが「間違って」いる。でも大丈夫だ。それは単に注目している関手が反変であることを意味する。

実際には、期待以上のものが得られた。𝐂\mathbf{C}から[𝐂,𝐒𝐞𝐭][\mathbf{C}, \mathbf{Set}]への写像は反変関手というだけではない――それは充満忠実 (fully faithful) 関手なのだ。充満性と忠実性という特性は、関手がhom集合をどう写すかを述べている。

忠実 (faithful) 関手はhom集合上の単射だ。つまり、別々の射は別々の射へと写す。言い換えれば、射を潰さない。

充満 (full) 関手はhom集合上の全射だ。つまり、一方のhom集合をもう一方のhom集合の上へ写し、後者を完全にカバーする。

充満忠実関手FFはhom集合上の全単射 (bijection) であり、つまり両方の集合のすべての要素が1対1で対応する。もとの圏𝐂\mathbf{C}内の対象aabbのすべてのペアに対して、𝐂(a,b)\mathbf{C}(a, b)𝐃(Fa,Fb)\mathbf{D}(F a, F b) の間に全単射がある。ここで、𝐃\mathbf{D}FFの行き先の圏(この場合は関手圏[𝐂,𝐒𝐞𝐭][\mathbf{C}, \mathbf{Set}])だ。ただし、これはFF対象について全単射であることを意味しないので注意してほしい。𝐃\mathbf{D}内の対象のうちFFの像内にないものが存在する可能性があり、それらの対象についてのhom集合たちに関しては何も言えない。

16.1 埋め込み

先ほど説明した(反変)関手、すなわち、𝐂\mathbf{C}内の対象を[𝐂,𝐒𝐞𝐭][\mathbf{C}, \mathbf{Set}]内の関手に写す関手: a𝐂(a,)a \to \mathbf{C}(a, -)米田埋め込み (Yoneda embedding) を定義する。それは圏𝐂\mathbf{C}(厳密に言うと反変なので圏𝐂𝑜𝑝\mathbf{C}^\mathit{op})を関手圏[𝐂,𝐒𝐞𝐭][\mathbf{C}, \mathbf{Set}]の内部に埋め込む𝐂\mathbf{C}内の対象を関手に写すだけでなく、それらの間のすべての接続を忠実に保持する。

これは非常に有用な結果だ。なぜなら、数学者は関手圏について、特に終域が𝐒𝐞𝐭\mathbf{Set}である関手について多くのことを知っているからだ。任意の圏𝐂\mathbf{C}について、関手圏へ埋め込むことで多くの知見が得られる。

もちろん米田埋め込みにも双対があり、それは余米田埋め込み (co-Yoneda embedding) と呼ばれることもある。議論の始めの時点で(始点となる対象ではなく)終点となる対象を固定したhom集合𝐂(,a)\mathbf{C}(-, a) を考えても良かったことに着目しよう。そうすれば反変hom関手が得られていたことになる。𝐂\mathbf{C}から𝐒𝐞𝐭\mathbf{Set}への反変関手は、おなじみの前層だ (たとえば、第12章の極限と余極限を参照)。余米田埋め込みは圏𝐂\mathbf{C}の前層圏への埋め込みを定義する。射に対する作用は次によって与えられる: [𝐂𝑜𝑝,𝐒𝐞𝐭](𝐂(,a),𝐂(,b))𝐂(a,b)[\mathbf{C}^\mathit{op}, \mathbf{Set}](\mathbf{C}(-, a), \mathbf{C}(-, b)) \cong \mathbf{C}(a, b) ここでも、数学者は前層圏について多くのことを知っているので、任意の圏をそれに埋め込めるのは大きな戦果だ。

16.2 Haskellへの応用

Haskellでの米田埋め込みは、式の一辺がreader関手同士の自然変換、他辺が(逆方向へ向かう)関数であるような同型として表せる:

forall x. (a -> x) -> (b -> x) ≅ b -> a

(Reader関手が((->) a)と等価であることを思い出してほしい。)

この等式の左辺は、aからxへの関数と型bの値が与えられたときに、型xの値を生成できるような多相関数だ(ここでは非カリー化した視点で、つまり関数b -> xの周りの括弧を除いて考えている)。これをすべてのxに対して行えるのは、関数がbaに変換する方法を知っている場合だけだ。関数b -> aに密かにアクセスできる必要がある。

そのようなコンバーターbtoaがあれば、この左辺を、fromYと呼ぶとして、以下のように定義できる:

fromY :: (a -> x) -> b -> x
fromY f b = f (btoa b)

逆に、関数fromYがあれば、恒等射についてfromYを呼び出すことでそのコンバーターを復元できる:

fromY id :: b -> a

これによって関数fromYbtoaの間に全単射が確立される。

この同型を別の観点で見ると、bからaへの関数を継続渡し形式で表しているとも見なせる。引数a -> xは継続(ハンドラー)であると見なせる93。結果はbからxへの関数であり、型bの値を引数として呼ばれたとき、エンコードされようとしている関数に前合成された継続を実行する。

米田埋め込みでHaskellのデータ構造のうちいくつかの別の表現についても説明できる。特に、非常に便利なレンズの表現94Control.Lensライブラリで提供する。

16.3 前順序での例

この節の例はRobert Harperによって提案された。前順序によって定義された圏に米田埋め込みを適用するものだ。前順序は要素間に順序関係がある集合であり、この順序関係は伝統的に\leqslant(小なりイコール)で記述される。前順序に「前」が付いているのは、関係が推移律 (transitive law) と反射律 (reflexive law) を満たす必要があるだけで、必ずしも反対称律 (antisymmetric law) を満たす必要はないからだ(すなわち、循環してもよい)95

前順序関係を持つ集合は圏をなす。対象となるのはその集合の各要素だ。対象aaからbbへの射は、対象が比較できない場合やaba \leqslant bが真でない場合には存在せず、aba \leqslant bの場合にはaaからbbへの向きに存在する。ある対象から別の対象への射が2つ以上存在することはない。したがって、このような圏のhom集合はすべて、空集合または単元集合だ。このような圏は細い圏と呼ばれる。

この構成が実際に圏であることは簡単に納得できる。まず、射は合成可能だ。なぜなら、aba \leqslant bかつbcb \leqslant cならばaca \leqslant cだからだ。そして、合成は結合性を持つ。恒等射も存在する。なぜなら、すべての要素がそれ自身(以下)となる(もとになっている順序の反射律)からだ。

これで前順序圏に余米田埋め込みを適用できるようになった。特に興味があるのは射に対する作用だ: [𝐂,𝐒𝐞𝐭](𝐂(,a),𝐂(,b))𝐂(a,b)[\mathbf{C}, \mathbf{Set}](\mathbf{C}(-, a), \mathbf{C}(-, b)) \cong \mathbf{C}(a, b) 右辺のhom集合が空集合でないのはaba \leqslant bのときだけだ。その場合は単元集合となる。したがって、aba \leqslant bの場合、左辺には自然変換が1つだけ存在する。それ以外の場合は自然変換はない。

では、前順序のhom関手間の自然変換とは何だろうか? それは2つの集合𝐂(,a)\mathbf{C}(-, a)𝐂(,b)\mathbf{C}(-, b) の間の関数の族でなければならない。前順序集合では、2つの集合はそれぞれ空集合か単元集合だ。どんな関数があり得るか見てみよう。

空集合からそれ自身への関数(空集合に作用する恒等射)、空集合から単元集合へのabsurd関数(関数の値を定義すべき要素が空集合には1つもないので、何もしない)、そして単元集合からそれ自身への関数 (要素が1つの集合に作用する恒等射)がある。単元集合から空集合への組み合わせだけは禁じられている(そのような関数が単元集合の要素に作用したとして、どんな値を返せばよいだろう?)。

つまり、この自然変換は決してhom単元集合をhom空集合に接続しない。言い換えると、xax \leqslant a (hom単元集合𝐂(x,a)\mathbf{C}(x, a)) ならば𝐂(x,b)\mathbf{C}(x, b) は空集合ではない。空でない𝐂(x,b)\mathbf{C}(x, b)xxbb以下であることを意味する。したがって、ここでの自然変換が存在するためには、すべてのxxについて、xax \leqslant aならばxbx \leqslant bが成り立つ必要がある: 任意の x について xaxb\text{任意の } x \text{ について } x \leqslant a \Rightarrow x \leqslant b 一方、余米田の補題によると、この自然変換の存在は𝐂(a,b)\mathbf{C}(a, b) が空でないこと、つまりaba \leqslant bであることと等価だ。まとめると、次の結果が得られる: ab ならばそのときに限り任意の x について xaxba \leqslant b \text{ ならばそのときに限り任意の } x \text{ について } x \leqslant a \Rightarrow x \leqslant b この結果に直接到達することもできただろう。直観的には、aba \leqslant bならばaa以下のすべての要素もbb以下である必要がある。逆に、aaを右辺のxxに代入すると、aba \leqslant bとなる。しかし、米田埋め込みを通じてこの結果に到達する方がはるかに刺激的なのは認めなければならない。

16.4 自然性

米田の補題は自然変換の集合と𝐒𝐞𝐭\mathbf{Set}の対象との間に同型射を設ける。いま扱っている自然変換たちは関手圏[𝐂,𝐒𝐞𝐭][\mathbf{C}, \mathbf{Set}]内の射だ。任意の2つの関手間の自然変換の集合は、関手圏におけるhom集合となる。米田の補題とは次の同型だ: [𝐂,𝐒𝐞𝐭](𝐂(a,),F)Fa[\mathbf{C}, \mathbf{Set}](\mathbf{C}(a, -), F) \cong F a この同型は実はFFについてもaaについても自然だ。言い換えれば、積圏[𝐂,𝐒𝐞𝐭]×𝐂[\mathbf{C}, \mathbf{Set}] \times \mathbf{C}から取られたペア (F,a)(F, a) について自然だ。ここではFFを関手圏の対象として扱っていることに注意してほしい。

これが何を意味するのか少し考えてみよう。自然同型は2つの関手の間の可逆な自然変換だ。そして実際、前述の同型の右辺は関手だ。具体的には[𝐂,𝐒𝐞𝐭]×𝐂[\mathbf{C}, \mathbf{Set}]\times \mathbf{C}から𝐒𝐞𝐭\mathbf{Set}への関手だ。ペア (F,a)(F, a) に対するその作用は集合――関手FFを対象aaにおいて評価した結果――となる。この関手は評価関手 (evaluation functor) と呼ばれる。

左辺も関手であり、(F,a)(F, a) を自然変換の集合[𝐂,𝐒𝐞𝐭](𝐂(a,),F)[\mathbf{C}, \mathbf{Set}](\mathbf{C}(a, -), F) に変換する。

これらが本当に関手だと示すには、射に対する作用も定義しなければならない。しかし、ペア (F,a)(F, a)(G,b)(G, b) の間の射とは何だろうか? それは射のペア (Φ,f)(\Phi, f) だ。1番目は関手間の射――自然変換――であり、2番目は𝐂\mathbf{C}内の通常の射だ。

評価関手はこのペア (Φ,f)(\Phi, f) を取り、2つの集合FaF aGbG bの間の関数に写す。このような関数は、aaにおけるΦ\Phiの成分(FaF aGaG aに写す)とGGによってリフトされた射ffから、簡単に構築できる: (Gf)Φa(G f) \cdot \Phi_a Φ\Phiの自然性により、これは次と同じであることに注意してほしい: Φb(Ff)\Phi_b \cdot (F f) この同型全体の自然性を証明するつもりはない――関手とは何かが掴めれば、ごく機械的に証明できる。それはこの同型が関手と自然変換から成り立っているという事実から導かれる。うまくいかないはずがない。

16.5 課題

  1. 余米田埋め込みをHaskellで表現せよ。
  2. fromYbtoaの間に確立された全単射が同型である(2つの写像が互いに逆である)ことを示せ。
  3. 与えられたモノイドに対し、米田埋め込みを行え。そのモノイドの単一の対象に対応する関手は何か96? どのような自然変換がモノイド射に対応するか?
  4. 共変米田埋め込みを前順序に適用したものは何か?(この問いはGershom Bazermanによって提案された。)
  5. 米田埋め込みを使えば、任意の関手圏[𝐂,𝐃][\mathbf{C}, \mathbf{D}]を関手圏[[𝐂,𝐃],𝐒𝐞𝐭][[\mathbf{C}, \mathbf{D}], \mathbf{Set}]に埋め込める。それが射(この場合は自然変換)にどう作用するか説明せよ。

17 射こそすべて

まだ圏論の勘所は射だと確信してもらえていないなら、私が務めをきちんと果たせなかったということになる。次章の話題である随伴 (adjunction) は、hom集合間の同型によって定義されるため、hom集合の構成要素についての直観を見直すのが理にかなっている。また、随伴はこれまで見てきた多くの構成を記述するさらに汎用的な語彙を提供するため、それらの構成を復習しておくのも有意義だろう。

17.1 関手

まず始めに、関手を射の写像だと真剣に考えるべきだ。これはHaskellのFunctor型クラスの定義において強調される考え方だ。そこではfmapが中心だった。もちろん、関手は対象――射の両端――も写す。そうでなければ、合成の保存について語れない。対象は射のどのペアが合成可能か教えてくれる。一方の射の終点がもう一方の始点と等しくなければ合成できない。したがって、射の合成をリフトされた射の合成に写すなら、それらの両端の写像もほぼ決まる。

17.2 可換図式

射についての性質の多くは可換図式によって表される。ある特定の射が他の射の合成として複数の方法で記述できるなら、可換図式があることになる。

特に、可換図式はほぼすべての普遍的構成の基礎となっている(始対象と終対象という重要な例外はある)。このことはすでに、積、余積、その他さまざまな(余)極限、冪対象、自由モノイドなどの定義で見てきた。

積は普遍的構成の簡単な例だ。2つの対象aabbを選び、それらの積となる普遍性を持つような、射ppqqのペアを伴う対象ccが存在するかを見る。

積は極限の特別な場合でもある。極限は錐によって定義される。一般的な錐は可換図式から作成される。それらの図式の可換性は、関手間の写像についての適切な自然性条件で置き換えられる97。このようにして、可換性は自然変換という高水準言語に対するアセンブリー言語の役割に降格される。

17.3 自然変換

一般に、自然変換は射から四角い可換図式への写像が必要なときに非常に便利だ。自然性の四角い図式で向かい合う辺のうち2つは、ある射ffを2つの関手FFGGでそれぞれ写したものだ。残りの2辺は自然変換の成分(これらも射)だ。

自然性は「隣接する」(つまり射でつながった)成分に移っても圏や関手の構造に反しないことを意味している。自然変換の成分を使って対象間のギャップをまず埋めてから関手を使って隣の対象にジャンプするのでも、その逆でも関係ない。すなわち、2つの方向は直交している。いうなれば、自然変換は左右に移動させ、関手は上下や前後に移動させる。関手のは、その行き先となる圏でのシートとして視覚化できる。この見方では、自然変換はFFに対応するシートをGGに対応する別のシートに写す。

この直交性のHaskellにおける例はすでに見ている。そこでは、関手の作用はコンテナーの形状を変更せずに内容を変更し、一方で自然変換は内容を変更せずに別のコンテナーに詰め直すものだった。これらの操作の順序は関係なかった。

極限の定義の中で錐が自然変換に置き換えられるのを見た。自然性はすべての錐の側面が可換だと保証する。しかし、極限は錐の間の写像によって定義されていた。これらの写像は可換性条件も満たす必要があった。(たとえば、積の定義における三角形は可換である必要がある。)

これらの条件も自然性によって置き換えられる。普遍な錐、すなわち極限が、(反変)hom関手: Fc𝐂(c,𝐋𝐢𝐦D)F \Colon c \to \mathbf{C}(c, % \mathbf{Lim}{}% {D}) と、𝐂\mathbf{C}内の対象を、それ自体が自然変換である錐に写すような(反変)関手: Gc𝑁𝑎𝑡(Δc,D)G \Colon c \to \mathit{Nat}(\Delta_c, D) の自然変換として定義されていたのを思い出してほしい。ここで、Δc\Delta_cは定関手、DD𝐂\mathbf{C}での図式を定義する関手だ。関手FFGGは両方とも、𝐂\mathbf{C}の射に対する作用が明確に定義されている。奇遇にもFFGGの間の特定の自然変換は同型だ。

17.4 自然同型

自然同型――すべての成分が可逆な自然変換――は、圏論で「2つのものは同じである」と言うときの言い方だ。そのような変換の成分は対象間の同型射――逆が存在する射――でなければならない。関手の像をシートとして表すなら、自然同型はシート間の1対1の可逆な写像だと言える。

17.5 Hom集合

それにしても、射とは何だろう? 射は対象よりも構造が豊かだ。対象とは違って、射には2つの端がある。しかし、始点となる対象と終点となる対象を固定すると、それら2つの間の射たちは(少なくとも局所的に小さい圏では)単に集合をなしてしまう。この集合の要素を区別するためにffggのような名前を付けることはできるが、一体何がこれらの射を区別しているのだろう?

与えられたhom集合内の射同士の本質的な差異は、(隣接するhom集合からの)他の射とどのように合成されるかにある。ffとの合成(前合成でも後合成でもよい)がggとの合成と異なるような射hhが存在する場合、つまり、たとえば: hfhgh \circ f \neq h \circ g ならffggの違いを直接「観察」できる。しかし、違いが直接観察できない場合でも、関手を使えばそのhom集合にズームインできる。関手FFは2つの射を、より豊かな圏における別々の射: FfFgF f \neq F g に写せる。そこでは、隣接するhom集合による分解能がより高い場合がある。たとえば、FFの像に含まれないhh'によって: hFfhFgh' \circ F f \neq h' \circ F g のように区別できる場合がある。

17.6 Hom集合同型

圏論では多くの構成がhom集合間の同型に依存している。もっとも、hom集合はただの集合なので、それらの間の同型から分かることはあまりない。有限集合の場合は、同型は要素数が同じだと示すだけだ。無限集合の場合は、同型が存在するならばそれらの濃度が同じでなければならない。しかし、hom集合の意味のある同型はすべて、合成も考慮しなければならない。合成に関わるhom集合はひとつだけではない。あらゆるhom集合にまたがる同型を定義する必要があり、合成と相互運用できるような何らかの互換性の条件を課す必要がある。そして、自然同型はその条件にぴったり合う。

だが、hom集合同士の自然同型とは何だろう? 自然性は、関手間の写像の性質であり、集合間の写像についてのものではない。つまり、いま話しているのは実際にはhom集合値関手の間の自然同型についてだ。それらの関手は単なる集合値関手ではない。射に対するその作用は、適切なhom関手によって導かれる。射はhom関手によって、(合成の変性に依存して)前合成か後合成のどちらかで正準的に写される。

米田埋め込みはそのような同型の一例だ。それは𝐂\mathbf{C}内のhom集合を関手圏内のhom集合に写す。そしてそれは自然だ。米田埋め込みにおける関手のひとつは𝐂\mathbf{C}のhom関手で、もうひとつは対象をhom集合間の自然変換の集合に写すような関手だ。

極限の定義もhom集合間の自然同型だ(ここでも2番目は関手圏内のhom集合だ): 𝐂(c,𝐋𝐢𝐦D)𝑁𝑎𝑡(Δc,D)\mathbf{C}(c, % \mathbf{Lim}{}% {D}) \simeq \mathit{Nat}(\Delta_c, D) 実は冪対象や自由モノイドの構成もhom集合間の自然同型として書き直せる。

これは偶然の一致ではない――次章で見るように、これらはhom集合の自然同型として定義される随伴の様々な例にすぎない。

17.7 Hom集合の非対称性

随伴を理解するのに役立つ観察結果はもう1つある。Hom集合は一般に対称ではない。Hom集合𝐂(a,b)\mathbf{C}(a, b) は、hom集合𝐂(b,a)\mathbf{C}(b, a) と大きく異なることがよくある。この非対称性の究極の例は、半順序を圏と見なすことだ。半順序では、aabb以下の場合、かつその場合に限ってaaからbbへの射が存在する。さらにaabbが異なる場合は、bbからaaへと逆方向に進む射は存在しない。Hom集合𝐂(a,b)\mathbf{C}(a, b) が空集合でない(ここでは単元集合であることを意味する)なら、a=ba = bでない限り、𝐂(b,a)\mathbf{C}(b, a) は空でなければならない。この圏の射には明確な一方向の流れが存在する。

関係が反対称でなくてもよい前順序も、たまにある循環を除けば「ほとんど」方向付けられている。任意の圏を前順序の一般化と見なすと便利だ。

前順序は細い圏だ――すべてのhom集合が単元集合か空集合のどちらかだ。一般の圏は「太い」(thick) 前順序として描写できる。

17.8 課題

  1. 退化 (degenerate) した自然性条件の例を考え、適切な図を描け。たとえば、関手FFまたはGGのどちらかが対象aabbfabf \Colon a \to bの両端)の両方を同じ対象に、Fa=FbF a = F bまたはGa=GbG a = G bのように写したらどうなるか? (この方法で錐や余錐が得られることに注目してほしい。)次に、Fa=GaF a = G aまたはFb=GbF b = G bのどちらかの場合について考えよ。最後に、自分自身へループする射faaf \Colon a \to aから始めた場合はどうか?

18 随伴

数学では、あるものが別のものに似ているという言い方はいろいろある。最も厳密なのは等しさだ。互いを区別する方法がなければ、2つのものは等しい。想像できるあらゆる状況において、一方を他方の代わりにできる。たとえば、可換図式について話すときはいつも射の等しさ (equality) を使っていることに気付いただろうか? それは、射が集合(hom集合)を形成し、集合の要素は等しさを確認できるからだ。

しかし、等しさは強すぎることが多い。実際には等価ではないのに、あらゆる意図と目的に照らして同じであるという例はたくさんある。たとえば、ペアの型(Bool, Char)(Char, Bool)と厳密に等しいわけではないが、含んでいる情報が同じなのは分かっている。この概念を最もうまく捉えたものは、2つの型の間の同型射――反転可能な射だ。これは射なので、構造を保存する。そして同型射 (isomorphism) の “iso” は、もとの場所に帰り着く往復旅行の一部であることを意味する。どちら側から出発するかは関係ない。ペアにおいては、この同型射はswapと呼ばれる:

swap :: (a,b) -> (b,a)
swap (a,b) = (b,a)

swapはそれ自身の逆だ。

18.1 随伴と単位/余単位ペア

圏が同型だと述べるときは、これを圏間の写像、すなわち関手によって表す。圏𝐂\mathbf{C}から圏𝐃\mathbf{D}への逆変換可能な関手RR(“right”)が存在する場合に、𝐂\mathbf{C}𝐃\mathbf{D}は同型だと言えるようにしたい。言い換えると、𝐃\mathbf{D}から𝐂\mathbf{C}に戻る別の関手LL(“left”)が存在し、RRと合成することで恒等関手IIに等しくなる。合成はRLR \circ LLRL \circ Rの2種類があるので、恒等関手も𝐂\mathbf{C}内と𝐃\mathbf{D}内の2種類がある。

しかし、ここがややこしいところだ:2つの関手が等しいとは何を意味するのだろうか? 次の等しさは何を意味するだろう: RL=I𝐃R \circ L = I_{\mathbf{D}} あるいは、次のものは: LR=I𝐂L \circ R = I_{\mathbf{C}} 関手の等しさを対象の等しさとして定義するのは合理的に思える。2つの関手が同じ対象に作用するなら、同じ対象が生成されるはずだ。しかし、一般に、対象の等しさの概念は任意の圏においては存在しない。それは単に定義の一部ではない。(この「等しさとは本当は何なのか」といううさぎの穴を深く掘り下げると、ホモトピー型理論に辿り着く。)

関手は圏の圏における射であるから、それらは等しさで比較可能なはずだ、と主張したくなるかもしれない。実際、対象が集合を形成するような小さい圏を扱う限り、集合の要素の等しさを使って対象を等値比較できる。

ただし、覚えておいてほしい。𝐂𝐚𝐭\mathbf{Cat}は実際には𝟐\mathbf{2}-圏だ。𝟐\mathbf{2}-圏のhom集合には追加の構造がある――𝟏\mathbf{1}-射の間に作用する𝟐\mathbf{2}-射がある。𝐂𝐚𝐭\mathbf{Cat}では、𝟏\mathbf{1}-射は関手であり、𝟐\mathbf{2}-射は自然変換だ。だから、関手について述べるときに自然同型を等しさの代わりと考えるのは、より自然なのだ(この駄洒落は避けようがない!)。

したがって、圏の同型の代わりに同値性 (equivalence) という、より一般的な概念を考えるのが理にかなっている。2つの圏𝐂\mathbf{C}𝐃\mathbf{D}同値 (equivalent) であるとは、それらの間を行き来する2つの関手があり、(どちらかの向きの)合成が恒等関手と自然同型 (naturally isomorphic) であることだ。言い換えると、合成RLR \circ Lと恒等関手I𝐃I_{\mathbf{D}}の間には双方向の自然変換があり、LRL \circ Rと恒等関手I𝐂I_{\mathbf{C}}の間にも別の双方向の自然変換がある。

随伴は同値性よりもさらに弱い。2つの関手の合成が恒等関手と同型 (isomorphic) でなくてもよいからだ。その代わり、I𝐃I_{\mathbf{D}}からRLR \circ Lへの一方向の自然変換と、LRL \circ RからI𝐂I_{\mathbf{C}}への別の一方向の自然変換が存在しなくてはならない。これら2つの自然変換の表記を以下に示す: ηI𝐃RLεLRI𝐂 \begin{gathered} \eta \Colon I_{\mathbf{D}} \to R \circ L \\ \varepsilon \Colon L \circ R \to I_{\mathbf{C}} \end{gathered} η\etaは随伴の単位 (unit) と呼ばれ、ε\varepsilonは余単位 (counit) と呼ばれる。

これら2つの定義の非対称性に注目してほしい。一般には、残りの2つの写像は存在しない: RLI𝐃必須ではないI𝐂LR必須ではない \begin{gathered} R \circ L \to I_{\mathbf{D}} \quad\quad\text{必須ではない} \\ I_{\mathbf{C}} \to L \circ R \quad\quad\text{必須ではない} \end{gathered} この非対称性のため、関手LLは関手RRに対する左随伴 (left adjoint) と呼ばれ、関手RRLLに対する右随伴 (right adjoint) と呼ばれる。(当然、左と右に意味があるのは図を特定の向きに描いた場合だけだ)。

随伴は次のように略記される: LRL \dashv R 随伴をよりよく理解するために、単位と余単位をさらに詳しく分析してみよう。

まずは単位から始めよう。これは自然変換なので、射の族だ。𝐃\mathbf{D}内の対象ddについて、η\etaの成分は、IdI dddに等しい)と (RL)d(R \circ L) d(図中のdd')の間の射となる: ηdd(RL)d\eta_d \Colon d \to (R \circ L) d 合成RLR \circ Lは、𝐃\mathbf{D}の自己関手であることに注意してほしい。

この等式は、𝐃\mathbf{D}内の任意の対象ddを始点として選択でき、往復旅行する関手RLR \circ Lを使って終点dd'を選択できることを示している。そして、射ηd\eta_dという矢が終点に向けて放たれる。

同様に、余単位ε\varepsilonの成分は次のように記述できる: εc(LR)cc\varepsilon_{c} \Colon (L \circ R) c \to c これは、𝐂\mathbf{C}内の任意の対象ccを終点として選択でき、ラウンドトリップ関手LRL \circ Rを使って始点c=(LR)cc' = (L \circ R) cを選択できることを示している。そして、射εc\varepsilon_{c}という矢が始点から終点に向けて放たれる。

単位と余単位について別の見方をすると、単位は𝐃\mathbf{D}に恒等関手を挿入できる場所ならどこでも合成RLR \circ L導入 (introduce) でき、余単位は𝐂\mathbf{C}の恒等射で置き換えることで合成LRL \circ R除去 (eliminate) できる。これにより、導入した後で除去すれば何も変更されないことを保証するいくつかの「自明な」整合性条件が導かれる: L=LI𝐃LRLI𝐂L=LR=I𝐃RRLRRI𝐂=R \begin{gathered} L = L \circ I_{\mathbf{D}} \to L \circ R \circ L \to I_{\mathbf{C}} \circ L = L \\ R = I_{\mathbf{D}} \circ R \to R \circ L \circ R \to R \circ I_{\mathbf{C}} = R \end{gathered} これらは、次の図式を可換にするので、三角恒等式 (triangular identity) と呼ばれる:

これらは関手圏の図式だ。つまり、矢は自然変換であり、それらの合成は自然変換の水平合成だ。成分で表すと、これらの恒等式は次のようになる: εLdLηd=𝐢𝐝LdRεcηRc=𝐢𝐝Rc \begin{gathered} \varepsilon_{L d} \circ L \eta_d = \mathbf{id}_{L d} \\ R \varepsilon_{c} \circ \eta_{R c} = \mathbf{id}_{R c} \end{gathered} Haskellでは単位と余単位を別の名前でよく見かける。単位はreturn(あるいはApplicativeの定義ではpure)として知られている:

return :: d -> m d

また、余単位はextractとして知られている:

extract :: w c -> c

ここで、mRLR \circ Lに対応する(自己)関手であり、wLRL \circ Rに対応する(自己)関手だ。後で述べるように、これらはそれぞれモナドとコモナドの定義の一部だ。

自己関手をコンテナーと見なすなら、単位(return)は任意の型の値に対して既定の箱を生成する多相関数だ。余単位(extract)はその逆を行い、コンテナーから単一の値を取得または生成する。

後で述べるように、随伴関手の各ペアはモナドとコモナドを定義する。逆に、すべてのモナドやコモナドは随伴関手のペアに分解され得る――ただし、その分解は一意ではない。

Haskellではモナドをよく使うが、それらを随伴関手のペアに分解することはめったにない。その主な理由は、それらの関手によって通常は𝐇𝐚𝐬𝐤\mathbf{Hask}の外へ追い出されるからだ。

しかし、Haskellでは自己関手の随伴を定義できる。以下はData.Functor.Adjunctionから引用した定義の一部だ:

class (Functor f, Representable u) =>
      Adjunction f u | f -> u, u -> f where
    unit :: a -> u (f a)
    counit :: f (u a) -> a

この定義には説明が必要だ。まず、これは多パラメーター型クラスを記述している――2つのパラメーターはfuだ。その2つの型構成子の間にAdjunctionという関係を確立している。

バーティカルバーの後の追加条件は、関数の依存関係を指定している。たとえば、f -> ufによってuが決定されることを意味する(fuの関係は関数で、ここでは型構成子についての関数だ)。逆に、u -> fは、uが分かればfが一意に決まることを意味する。

なぜHaskellでは右随伴u表現可能関手であるという条件を課せるのかについては、すぐ後で説明する。

18.2 随伴とhom集合

随伴の等価な定義として、hom集合の自然同型によるものがある。その定義はこれまで学んだ普遍的構成とうまく結びついている。ある一意な射がある構成を分解しているという話を聞いたら毎回、それはある集合からhom集合への写像だと見なすべきだ。それが「一意な射を選択する」ということの意味だ。

さらに言うと、分解は自然変換によって記述されることが多い。分解には可換図式が関わる――ある射は2つの射(因子)の合成に等しい。自然変換は射を可換図式に写す。したがって、普遍的構成では、射から可換図式へ、そして一意な射へ向かう。最終的には、射から射への写像、あるいはあるhom集合から別の(通常は異なる圏の)hom集合への写像が得られる。もしこの写像が可逆で、すべてのhom集合に自然に拡張できるならば、随伴が存在する。

普遍的構成と随伴の主な違いは、後者がすべてのhom集合に対して大域的に定義されていることだ。たとえば、普遍的構成を使えば選択した2つの対象の積を定義できる。これはその圏内の他の対象のペアに対して積が存在しない場合でも同様だ。すぐ後で説明するように、対象の任意のペアの積が圏に存在する場合は、随伴によっても定義できる。

これがhom集合を使った随伴の別の定義だ。前と同じように、2つの関手L𝐃𝐂L \Colon \mathbf{D} \to \mathbf{C}R𝐂𝐃R \Colon \mathbf{C} \to \mathbf{D}がある。任意の2つの対象として𝐃\mathbf{D}内で始点となる対象dd𝐂\mathbf{C}内で終点となる対象ccを選択しよう。LLを使えば始点となる対象dd𝐂\mathbf{C}に写せる。これで𝐂\mathbf{C}内の2つの対象LdL dccが得られた。これらはhom集合を定義する: 𝐂(Ld,c)\mathbf{C}(L d, c) 同様に、RRを使えば終点となる対象ccを写せる。これで𝐃\mathbf{D}内の2つの対象ddRcR cが得られた。これらもhom集合を定義する: 𝐃(d,Rc)\mathbf{D}(d, R c) LLRRに対する左随伴となるのは、ddccの両方で自然なhom集合の同型: 𝐂(Ld,c)𝐃(d,Rc)\mathbf{C}(L d, c) \cong \mathbf{D}(d, R c) が存在する場合、かつその場合に限ると述べた。自然性は、始点となる対象dd𝐃\mathbf{D}上でスムーズに変化させられ、終点となる対象cc𝐂\mathbf{C}上でスムーズに変化させられることを意味する。より正確には、𝐂\mathbf{C}から𝐒𝐞𝐭\mathbf{Set}への次の2つの(共変)関手間に自然変換φ\varphiが存在する。それらの関手は対象に対してこう作用する: c𝐂(Ld,c)c𝐃(d,Rc) \begin{gathered} c \to \mathbf{C}(L d, c) \\ c \to \mathbf{D}(d, R c) \end{gathered} もう一方の自然変換ψ\psiは次の(反変)関手間に作用する: d𝐂(Ld,c)d𝐃(d,Rc) \begin{gathered} d \to \mathbf{C}(L d, c) \\ d \to \mathbf{D}(d, R c) \end{gathered} 自然変換は両方とも可逆でなければならない。

随伴の2つの定義が等しいことは簡単に示せる。例として、単位変換の導出を、hom集合の同型から始めよう: 𝐂(Ld,c)𝐃(d,Rc)\mathbf{C}(L d, c) \cong \mathbf{D}(d, R c) この同型は任意の対象ccで成り立つので、c=Ldc = L dでも成り立つ必要がある: 𝐂(Ld,Ld)𝐃(d,(RL)d)\mathbf{C}(L d, L d) \cong \mathbf{D}(d, (R \circ L) d) 左辺は少なくとも1つの射、つまり恒等射を必ず含むのが分かっている。自然変換はこの射を𝐃(d,(RL)d)\mathbf{D}(d, (R \circ L) d) の要素に写す。恒等関手IIを挿入すれば、次の圏内の射に写すとも言える: 𝐃(Id,(RL)d)\mathbf{D}(I d, (R \circ L) d) ddでパラメーター化された射の族が得られた。それらは関手IIと関手RLR \circ Lの間に自然変換を形成する(自然性条件は容易に確認できる)。これはまさに単位η\etaだ。

逆に、単位と余単位の存在から始めれば、hom集合間の変換を定義できる。例として、hom集合𝐂(Ld,c)\mathbf{C}(L d, c) 内の任意の射ffを選択しよう。ffに作用して𝐃(d,Rc)\mathbf{D}(d, R c) 内に射を生成するφ\varphiを定義したい。

選択肢はあまりない。試せる方法の1つはRRを使ってffをリフトすることだ。これにより、R(Ld)R (L d) からRcR cへの射RfR f――𝐃((RL)d,Rc)\mathbf{D}((R \circ L) d, R c) の要素である射が生成される。

φ\varphiの成分に必要なのはddからRcR cへの射だ。これは問題ない。ηd\eta_dの成分を使えばddから (RL)d(R \circ L) dを得られるからだ。すると、次の結果が得られる: φf=Rfηd\varphi_f = R f \circ \eta_d 他の方向についても同様で、ψ\psiを導出できる。

HaskellでのAdjunctionの定義に戻ると、自然変換φ\varphiψ\psiはそれぞれ(abについての)多相関数leftAdjunctrightAdjunctに置き換えられる。関手LLRRfuと呼ばれる。

class (Functor f, Representable u) =>
  Adjunction f u | f -> u, u -> f where
    leftAdjunct  :: (f a -> b) -> (a -> u b)
    rightAdjunct :: (a -> u b) -> (f a -> b)

unit/counitの構成とleftAdjunct/rightAdjunctの構成の同値性は、次の対応によって示される:

  unit           = leftAdjunct id
  counit         = rightAdjunct id
  leftAdjunct f  = fmap f . unit
  rightAdjunct f = counit . fmap f

随伴について圏論での記述からHaskellのコードへの翻案をなぞるのは非常に有益だ。演習として大いに推奨したい。

以上で、Haskellで右随伴が自動的に表現可能関手になる理由を説明する準備ができた。その理由は、第1近似としては、Haskellの型の圏を集合の圏として扱えるからだ。

右圏𝐃\mathbf{D}𝐒𝐞𝐭\mathbf{Set}であるとき、右随伴RR𝐂\mathbf{C}から𝐒𝐞𝐭\mathbf{Set}への関手だ。そのような関手が表現可能なのは、hom関手𝐂(𝑟𝑒𝑝,_)\mathbf{C}(\mathit{rep}, \_)RRに対して自然同型であるような関手𝑟𝑒𝑝\mathit{rep}𝐂\mathbf{C}内にある場合だ。𝐒𝐞𝐭\mathbf{Set}から𝐂\mathbf{C}へのある関手LLに対してRRが右随伴である場合、そのような対象は常に存在する――それはLLの下の単元集合 ()() の像だ: 𝑟𝑒𝑝=L()\mathit{rep} = L () 実際、随伴は次の2つのhom集合が自然同型だと教えてくれる: 𝐂(L(),c)𝐒𝐞𝐭((),Rc)\mathbf{C}(L (), c) \cong \mathbf{Set}((), R c) 任意のccについて、右辺は単元集合 ()() からRcR cへの関数の集合だ。そのような関数が集合RcR cからそれぞれ1つの要素を選択することはすでに説明した。そのような関数の集合は、集合RcR cの同型だ。したがって、次が成り立つ: 𝐂(L(),)R\mathbf{C}(L (), -) \cong R これはRRが実際に表現可能であることを示している。

18.3 随伴に基づく積

これまでに、普遍的構成を用いた概念をいくつか紹介した。これらの概念の多くは、大域的に定義されている場合、随伴を使って表現する方が簡単だ。非自明な例のうち最も単純なのは積だ。積の普遍的構成の要点は、普遍的構成を通じて積に似た候補を分解できることだ。

より正確には、2つの対象aabbの積は2つの射𝑓𝑠𝑡\mathit{fst}𝑠𝑛𝑑\mathit{snd}を伴う対象 (a×b)(a \times b)(すなわちHaskell表記の(a, b))であり、かつ2つの射pcap \Colon c \to aqcbq \Colon c \to bを伴う他の候補ccに対して、ppqq𝑓𝑠𝑡\mathit{fst}𝑠𝑛𝑑\mathit{snd}を通じて分解する一意な射mc(a,b)m \Colon c \to (a, b) が存在する。

すでに見たように、Haskellで2つの射影からこの射を生成するfactorizerを実装できる。

factorizer :: (c -> a) -> (c -> b) -> (c -> (a, b))
factorizer p q = \x -> (p x, q x)

分解条件が成立することは簡単に確認できる:

fst . factorizer p q = p
snd . factorizer p q = q

射のペアpqをとり、もう1つの射m = factorizer p qを生成する写像が存在する。

これをどう変換すれば、随伴を定義するために必要な2つのhom集合間の写像にできるだろう? 秘訣は、𝐇𝐚𝐬𝐤\mathbf{Hask}の外に出て、射のペアを積圏内の単一の射として扱うことだ。

積圏とは何か思い出してほしい。2つの任意の圏𝐂\mathbf{C}𝐃\mathbf{D}を取り上げよう。積圏内の対象𝐂×𝐃\mathbf{C}\times{}\mathbf{D}は、𝐂\mathbf{C}からの1つの対象と、𝐃\mathbf{D}からのもう1つの対象のペアだ。射は、𝐂\mathbf{C}からの1つの射と、𝐃\mathbf{D}からのもう1つの射のペアだ。

ある圏𝐂\mathbf{C}に積を定義するには、積圏𝐂×𝐂\mathbf{C}\times{}\mathbf{C}から始める必要がある。𝐂\mathbf{C}内の射のペアは、積圏𝐂×𝐂\mathbf{C}\times{}\mathbf{C}内の単一の射だ。

積を定義するために積圏を使うのは、最初は少し紛らわしいかもしれない。しかし、それらの積は全く異なる。普遍的構成は積圏を定義するのに必要ない。必要なのは対象のペアと射のペアという概念だけだ。

ただし、𝐂\mathbf{C}からの対象のペアは、𝐂\mathbf{C}の対象ではない。それは別の圏𝐂×𝐂\mathbf{C}\times{}\mathbf{C}の対象だ。このペアは形式的にa,b\langle a, b \rangleと書ける。ここで、aabb𝐂\mathbf{C}の対象だ。一方、普遍的構成は、同じ𝐂\mathbf{C}内の対象a×ba\times{}b(すなわちHaskellでの(a, b))を定義するために必要だ。この対象は、普遍的構成によって指定された方法でペアa,b\langle a, b \rangleを表すはずだ。これは常に存在するわけではなく、たとえ𝐂\mathbf{C}内の一部の対象のペアに対して存在しても、他の対象のペアに対しては存在しない場合もある。

さて、factorizerをhom集合の写像として見てみよう。1つ目のhom集合は積圏𝐂×𝐂\mathbf{C}\times{}\mathbf{C}内にあり、2つ目は𝐂\mathbf{C}内にある。𝐂×𝐂\mathbf{C}\times{}\mathbf{C}内の一般の射は、射のペアf,g\langle f, g \ranglefcagcb \begin{gathered} f \Colon c' \to a \\ g \Colon c'' \to b \end{gathered} となり、cc''cc'と異なる可能性がある。しかし、積を定義するために関心があるのは、𝐂×𝐂\mathbf{C}\times{}\mathbf{C}内で同じ始点となる対象ccを共有する特別な射のペアppqqだ。それは構わない。随伴の定義では、左hom集合の始点は任意の対象ではない――左関手LLが右圏内の対象に作用した結果だ。要求に合う関手を推測するのは簡単だ――それは𝐂\mathbf{C}から𝐂×𝐂\mathbf{C}\times{}\mathbf{C}への対角関手 (diagonal functor) Δ\Deltaで、対象への作用は次のようになる: Δc=c,c\Delta c = \langle c, c \rangle したがって、ここでの随伴の左hom集合はこうなる: (𝐂×𝐂)(Δc,a,b)(\mathbf{C}\times{}\mathbf{C})(\Delta c, \langle a, b \rangle) これは積圏のhom集合だ。その要素は、factorizerの引数として知っている射のペアだ: (ca)(cb)(c \to a) \to (c \to b) \ldots{} 右側のhom集合は𝐂\mathbf{C}内にあり、始点となる対象ccから、ある関手RR𝐂×𝐂\mathbf{C}\times{}\mathbf{C}内の終点となる対象に作用した結果へと向かう。これこそがペアa,b\langle a, b \rangleを積対象a×ba\times{}bに写す関手だ。hom集合のこの要素はfactorizer結果として認識される: (c(a,b))\ldots{} \to (c \to (a, b))

まだ完全な随伴は得られていない。まずfactorizerが反転可能である必要がある――hom集合の間に同型射を構築しているからだ。factorizerの逆は、ある対象ccから積対象a×ba\times{}bへの射mmで始まる必要がある。言い換えると、mmは次のものの要素でなければならない: 𝐂(c,a×b)\mathbf{C}(c, a\times{}b) 逆factorizerはmmc,c\langle c, c \rangleからa,b\langle a, b \rangleへ向かう𝐂×𝐂\mathbf{C}\times{}\mathbf{C}内の射p,q\langle p, q \rangleに写す。言い換えると、次のものの要素である射に写す: (𝐂×𝐂)(Δc,a,b)(\mathbf{C}\times{}\mathbf{C})(\Delta c, \langle a, b \rangle) この写像が存在するならば、対角関手に対して右随伴が存在すると結論できる。この関手は積を定義する。

Haskellでは、mm𝑓𝑠𝑡\mathit{fst}𝑠𝑛𝑑\mathit{snd}でそれぞれ合成することで、常にfactorizerの逆を合成できる。

p = fst . m
q = snd . m

積を定義する2つの方法の同値性の証明を完成するには、hom集合間の写像がaabbccで自然であることも示す必要がある。これは熱心な読者のための練習として残しておこう。

ここまでで行ったことを要約しよう:圏論的な積は対角関手の右随伴として大域的に定義できる: (𝐂×𝐂)(Δc,a,b)𝐂(c,a×b)(\mathbf{C} \times{} \mathbf{C})(\Delta c, \langle a, b \rangle) \cong \mathbf{C}(c, a\times{}b) ここで、a×ba\times{}bは、ペアa,b\langle a, b \rangleに対する右随伴関手𝑃𝑟𝑜𝑑𝑢𝑐𝑡\mathit{Product}の作用の結果だ。𝐂×𝐂\mathbf{C}\times{}\mathbf{C}からの関手はすべて双関手であるため、𝑃𝑟𝑜𝑑𝑢𝑐𝑡\mathit{Product}は双関手であることに注意してほしい。Haskellでは、𝑃𝑟𝑜𝑑𝑢𝑐𝑡\mathit{Product}双関手は単に(,)と書かれる。次の例のように、2つの型にこれを適用すれば直積型を得られる:

(,) Int Bool ~ (Int, Bool)

18.4 随伴に基づく冪

bab^a、すなわち関数対象aba \Rightarrow bは、普遍的構成を使って定義できる。この構成は、対象のすべてのペアに対して存在するなら、随伴と見なせる。繰り返すが、秘訣は言明に集中することだ:

他の任意の対象zzのうち、射gz×abg \Colon z\times{}a \to bを伴うものについて、一意な射hz(ab)h \Colon z \to (a \Rightarrow b) が存在する。

この言明によりhom集合間の写像が確立される。

この例では、同じ圏内の対象を扱っているので、随伴関手は2つとも自己関手だ。左(自己)関手LLは、対象zzに作用すると、z×az\times{}aを生成する。ある固定されたaaについて積を求めることに相当する関手だ。

右(自己)関手RRは、bbに作用すると、関数対象aba \Rightarrow b(すなわちbab^a)を生成する。ここでも、aaは固定されている。この2つの関手間の随伴は次のように記述されることがよくある: ×a()a-\times{}a \dashv (-)^a この随伴の基礎となるhom集合の写像を極力理解しやすくするには、普遍的構成で用いた図式を描き直せばよい。

𝑒𝑣𝑎𝑙\mathit{eval}98はこの随伴の余単位に他ならないことに注目してほしい: (ab)×ab(a \Rightarrow b)\times{}a \to b ここで: (ab)×a=(LR)b(a \Rightarrow b)\times{}a = (L \circ R) b 前に述べたとおり、普遍的構成は同型を除いて一意な対象を定義する。それが積を “the” product、冪を “the” exponentialと書く理由だ。この特性は随伴にも及ぶ。つまり、ある関手に随伴があるなら、その随伴は同型を除いて一意だ。

18.5 課題

  1. 次の2つの(反変)関手間の変換ψ\psiについて、自然性の四角い図式を導出せよ: a𝐂(La,b)a𝐃(a,Rb) \begin{gathered} a \to \mathbf{C}(L a, b) \\ a \to \mathbf{D}(a, R b) \end{gathered}
  2. 随伴の第2の定義におけるhom集合同型から始めて、余単位ε\varepsilonを導出せよ。
  3. 随伴の2つの定義について、同値性の証明を完成せよ。
  4. 余積が随伴によって定義できることを示せ。余積のfactorizerの定義から始めよ。
  5. 余積が対角関手の左随伴であることを示せ。
  6. 積と関数対象の間の随伴をHaskellで定義せよ。

19 自由/忘却随伴

自由構成は随伴の強力な応用例だ。自由関手忘却関手への左随伴として定義される。忘却関手は構造を忘れた関手で、通常は非常に単純だ。たとえば、多くの興味深い圏は集合の上に構築されている。しかし、それらの集合を抽象化した圏論的な対象は、内部構造 すなわち要素を持たない。それでも、これらの対象がある意味で集合の記憶を保ち、ある圏𝐂\mathbf{C}から𝐒𝐞𝐭\mathbf{Set}への写像――関手――が存在することはよくある。𝐂\mathbf{C}内のある対象に対応する集合は、その対象の台集合 (underlying set) と呼ばれる。

モノイドは、台集合――要素の集合――を持つ対象だ。モノイドの圏𝐌𝐨𝐧\mathbf{Mon}から集合の圏へは忘却関手UUが存在し、モノイドをその台集合に写す。それはさらにモノイド射(準同型)を集合間の関数に写す。

私は𝐌𝐨𝐧\mathbf{Mon}が分裂した性格を持っていると見なすのが好きだ。一方では、それは乗算と単位元を持つたくさんの集合からなる。他方では、それは特徴のない対象を持つ圏であり、その唯一の構造は対象間の射として表されている。乗算と単位元を保存するすべての集合関数は𝐌𝐨𝐧\mathbf{Mon}に射を呼び起こす。

注意事項:

  • 同じ集合に写されるモノイドは多数存在する可能性がある。
  • モノイド射は、それらの台集合の間に存在する関数より少ない(多くても同数だ)。

忘却関手UUの左随伴である関手FFは、生成元の集合から自由モノイドを構築する自由関手だ。この随伴は、前に議論した自由モノイドの普遍的構成から導かれる。

モノイドm_1とm_2は同じ台集合を持つ。m_2とm_3の台集合の間の関数は、その間の射よりも多い。

Hom集合を使って、この随伴を次のように書ける: 𝐌𝐨𝐧(Fx,m)𝐒𝐞𝐭(x,Um)\mathbf{Mon}(F x, m) \cong \mathbf{Set}(x, U m) この(xxmmについての自然)同型によって以下のことが分かる:

  • xxによって生成された自由モノイドFxF xと任意のモノイドmmとの間のモノイド準同型に対し、生成元xxの集合をmmの台集合に埋め込む一意な関数が存在する。これは𝐒𝐞𝐭(x,Um)\mathbf{Set}(x, U m) 内の関数だ。
  • あるmmの台集合にxxを埋め込むすべての関数に対し、xxによって生成された自由モノイドとモノイドmmの間に一意なモノイド射が存在する。(これは普遍的構成でhhと呼んでいた射だ。)

直観では、FxF xxxに基づいて構築できる「最大の」モノイドだ。もしモノイドの内部を見られたら、𝐌𝐨𝐧(Fx,m)\mathbf{Mon}(F x, m) に属するすべての射がこの自由モノイドを他のモノイドmm埋め込むことが分かるだろう。それは、いくつかの要素をできるだけ同一視することで行われる。具体的には、FxF xの生成元(たとえばxxの要素)をmmに埋め込む。この随伴は、𝐒𝐞𝐭(x,Um)\mathbf{Set}(x, U m) からの関数で与えられる右辺のxxの埋め込みが、左辺のモノイドの埋め込みを一意に決定し、またその逆も成り立つことを示している。

Haskellでは、リストのデータ構造は自由モノイドだ (ただし、注意点がいくつかある:Dan Doelのブログ記事99を参照)。リスト型[a]は自由モノイドであり、型aは生成元の集合を表す。たとえば、型[Char]には、単位元――空リスト[]――と、['a']['b']のような単要素リスト――自由モノイドの生成元が含まれている。残りは「積」を適用して生成される。ここでは、2つのリストの積は単に片方をもう片方に連接するだけだ。連接は結合的で単位元的 (unital) だ(つまり、中立元が存在する――ここでは空リストだ)。Charによって生成される自由モノイドは、Charからなるすべての文字列の集合に他ならない。これはHaskellではStringと呼ばれる:

type String = [Char]

typeは型シノニム――既存の型への別名――を定義する。)

もう1つの興味深い例として、単一の生成元から作られた自由モノイドが挙げられる。これはunitのリスト[()]の型だ。その要素は[][()][(), ()]などだ。そのようなリストはすべて、1つの自然数――長さで記述できる。それ以外にunitのリストとして表された情報はない。このようなリスト2つを連接すると、長さが構成要素の長さの合計である新しいリストが生成される。型[()]が、(0を含む)自然数の加算モノイドと同型であることは容易に理解できる。以下の2つの関数は互いに逆であり、この同型を表している:

toNat :: [()] -> Int
toNat = length

toLst :: Int -> [()]
toLst n = replicate n ()

簡単のためNatural型ではなくInt型を使ったが、考え方は同じだ。関数replicateは、任意の値――ここでは単位元――で埋められた長さnのリストを作成する。

19.1 いくつかの直観

以下では、身振り手振りの議論をいくつか挙げる。この種の議論は厳密ではないが、直観を形成するのに役立つ。

自由/忘却随伴についての直観を得るには、関手や関数が本質的に情報を損失することを心に留めておくのがよい。関数は複数の対象や射を潰すことがあり、関数は集合の複数の要素をまとめることがある。また、像が終域の一部しかカバーしていないこともある。

𝐒𝐞𝐭\mathbf{Set}内の「平均的な」hom集合は関数の全スペクトルを含み、最も損失の少ないもの(たとえば、単射、または、おそらく同型)に始まり、始域全体を単一要素(もしあれば)に潰す定数関数で終わる。

私はよく、任意の圏における射も損失があると見なす。これは単なるメンタルモデルだが、特に随伴について考えるときは有用だ――典型的には、圏の1つが𝐒𝐞𝐭\mathbf{Set}である随伴の場合だ。

形式的には、反転可能な射(同型射)または反転不可能な射についてのみ語れる。損失があると見なせるのは後者だ。また、単射関数(潰さない関数)と全射関数(終域全体をカバーする関数)という概念を一般化した、モノ (mono-) 射とエピ (epi-) 射という概念もある。ただし、モノとエピの両方でありながら非可逆な射も存在可能だ。

自由\dashv忘却随伴では、左側に制約の多い圏𝐂\mathbf{C}があり、右側に制約の少ない圏𝐃\mathbf{D}がある。𝐂\mathbf{C}の射が「より少ない」のは、何らかの追加構造を保存しなければならないからだ。𝐌𝐨𝐧\mathbf{Mon}の場合は乗算と単位元を保存しなければならない。𝐃\mathbf{D}の射はそれほど多くの構造を保存しなくてよいので、「より多くの」射がある。

忘却関手UU𝐂\mathbf{C}内の対象ccに適用するとき、ccの「内部構造」を暴いていると見なせる。実際、𝐃\mathbf{D}𝐒𝐞𝐭\mathbf{Set}なら、UUccの内部構造――台集合――を定義していると見なせる。(任意の圏では、対象の内部については他の対象との接続を通じてしか述べられないが、ここでは単に身振り手振りで議論しているだけだ。)

UUを使って2つの対象cc'ccを写す場合、一般に、hom集合𝐂(c,c)\mathbf{C}(c', c) の写像は𝐃(Uc,Uc)\mathbf{D}(U c', U c) の部分集合のみをカバーすると予想される。𝐂(c,c)\mathbf{C}(c', c) 内の射が追加構造を保存しなければならないのに対して、𝐃(Uc,Uc)\mathbf{D}(U c', U c) の射はそうではないからだ。

しかし、随伴は特定のhom集合の同型として定義されるので、cc'は非常に慎重に選択しなければならない。随伴では、cc'𝐂\mathbf{C}内のどこから選択してもよいわけではなく、自由関手FFの(より小さいと推察される)像から選択される: 𝐂(Fd,c)𝐃(d,Uc)\mathbf{C}(F d, c) \cong \mathbf{D}(d, U c) したがって、FFの像は任意のccに向かう多くの射を持つ対象で構成されていなければならない。実際、構造を保存するFdF dからccへの射は、構造を保存しないddからUcU cへの射と同数存在する必要がある。これは、FFの像が本質的に構造のない対象で構成されている必要がある(射が保存すべき構造がない)ことを意味する。そのような「構造のない」対象は自由対象 (free object) と呼ばれる。

モノイドの例では、自由モノイドは単位律と結合律によって生成される構造以外には何の構造も持たない。それ以外では、すべての乗算は全く新しい要素を生成する。

自由モノイドでは、2*32 * 366ではない――新しい要素[2,3]{[}2, 3{]}だ。[2,3]{[}2, 3{]}66は同一視されないので、この自由モノイドから他の任意のモノイドmmへの射は、それらを別々に写すことが許される。ただし、[2,3]{[}2, 3{]}66(それらの積)の両方をmmの同じ要素に写しても構わない。また、加算モノイドで[2,3]{[}2, 3{]}55(それらの和)を同一視することなども同様だ。同一視が異なれば、得られるモノイドも異なる。

これは別の興味深い直観を導く:自由モノイドは、モノイダル演算を実行する代わりに、渡された引数を累積 (accumulate) する。たとえば、2233を掛ける代わりに、2233をリストに記憶する。この手法の利点は、どんなモノイダル操作を使うか指定する必要がないことだ。引数を累積し続けて、その結果に演算子を最後だけ適用すればよい。そしてその最後の時点で、どんな演算子を適用するか選択できる。数値を加算したり、乗算したり、モジュロ2加算したりできる。つまり、自由モノイドは式の作成と評価を分離する。この考え方は代数について話すときにもう一度見ることになる。

この直観は他のもっと複雑な自由構成にも一般化できる。たとえば、評価する前に式木 (expression tree) 全体を累積できる。このアプローチの利点は、そのような木を変換して、評価を高速にしたり、メモリー消費を減らしたりできることだ。これはたとえば、行列計算の実装に使われる。先行評価 (eager evaluation) で行列計算を行うと、中間結果を保存するための一時的な配列が大量に割り当てられてしまうからだ。

19.2 課題

  1. 単元集合を生成元として構築された自由モノイドを考える。その自由モノイドから任意のモノイドmmへの射と、その単元集合からmmの台集合への関数との間に1対1の対応があることを示せ。

20 モナド:プログラマーの定義

プログラマーたちは、モナドにまつわる神話を発展させてきた。これはプログラミングにおける極めて抽象的で難しい概念の1つだと考えられている。「分かっている」人とそうでない人がいる。多くの人にとって、モナドの概念を理解した瞬間はあたかも神秘的な体験のようだ。モナドは非常に多様な構成の本質を抽象化しているので、日常生活でうまく比喩できるものがない。そして我々は暗闇の中で手探りするようになった。まるで盲人たちが象の様々な部分に触れて「ロープだ」「木の幹だ」「ブリトーだ!」と勝ち誇って叫ぶように。

はっきり言っておこう。モナドを取り巻く神秘主義はすべて誤解に基づいている。モナドはとてもシンプルな概念だ。混乱を引き起こしているのは、モナドの応用先の多様さだ。

この記事のための調査の一環として、私はダクトテープ(別名ダックテープ)とその応用先について調べた。それを使ってできることのほんの一例を紹介しよう:

  • ダクトをシーリングする
  • アポロ13号に搭載された二酸化炭素除去装置を修理する
  • いぼを治療する
  • アップルのiPhone 4の通話切断問題を修正する
  • プロムで着るドレスを作る
  • 吊り橋を建設する

ダクトテープが何か知らずに、このリストに基づいて理解しようとしているのを想像してほしい。幸運を祈る!

そういうわけで、「モナドは……のようなものだ」という常套句のコレクションにもう1つ追加しようと思う:モナドはダクトテープのようなものだ。応用は幅広く多様だが、原理はごく単純だ:モナドは物同士をくっつける。より正確には、物同士を合成する。

このことは、多くのプログラマー、特に命令型言語をバックグラウンドに持つプログラマーがモナドを理解するのに困難が伴う理由の一部を説明している。問題は、プログラミングを関数合成の観点から考えるのに慣れていないことにある。これは理解できる。関数から関数へ値を直接渡さずに、中間値に名前を付けるのはよくあることだ。グルーコードの短い断片を、ヘルパー関数に抽象化せず、インライン化することもよくある。以下は、ベクトルの長さを求める関数をC言語で命令型スタイルで実装したものだ:

double vlen(double *v) {
    double d = 0.0;
    int n;
    for (n = 0; n < 3; ++n)
        d += v[n]* v[n];
    return sqrt(d);
}

これを、明示的な関数合成を用いた(様式化された)Haskell版と比較してほしい:

vlen = sqrt . sum . fmap  (flip (^) 2)

(ここでは、より謎めかせるために、指数演算子(^)の2番目の引数を2に設定して部分適用した。)

Haskellのポイントフリースタイルが常に優れていると主張しているのではなく、プログラミングで行うことのすべての基礎に関数合成があると主張しているだけだ。そして実質的には関数を合成しているにもかかわらず、Haskellでは多大な労力を費やしてdo記法と呼ばれる命令型構文をモナディック合成のために提供している。その使い方については後で説明する。まずは、なぜモナディック合成が必要なのか説明しよう。

20.1 クライスリ圏

以前、通常の関数を装飾することでWriterモナドに到達した。装飾するために、典型的には、戻り値を文字列とペアにした。あるいはもっと一般には、モノイドの要素とペアにした。いまではそのような装飾が関手だと気付ける:

newtype Writer w a = Writer (a, w)

instance Functor (Writer w) where
    fmap f (Writer (a, w)) = Writer (f a, w)

続いて、以下の形式の装飾された関数すなわちクライスリ射 (Kleisli arrow) を合成する方法を発見した:

a -> Writer w b

ログの累積は合成の内部で実装した。

これでクライスリ圏をより一般的に定義する準備ができた。まずは圏𝐂\mathbf{C}と自己関手mmから始める。対応するクライスリ圏𝐊\mathbf{K}𝐂\mathbf{C}と同じ対象を持つが、射は異なる。𝐊\mathbf{K}内の2つの対象aabbの間の射は次のように実装される: amba \to m\ b これはもとの圏𝐂\mathbf{C}内の射だ。𝐊\mathbf{K}内のクライスリ射はaabbの間の射として扱い、aambm\ bの間の射としては扱わないことを覚えておくのが重要だ。

この例では、mmWriter wに特化されており、ある決まったモノイドwに対応している。

クライスリ射は、それに適した合成を定義できるときだけ圏を形成する。すべての対象に対して恒等射を持ち結合的な合成がある場合、関手mmモナド (monad) と呼ばれ、その結果形成される圏はクライスリ圏と呼ばれる。

Haskellでは、クライスリ合成はfish演算子>=>を用いて定義され、その恒等射はreturnと呼ばれる多相関数だ。クライスリ合成を使ったモナドの定義は次のとおりだ:

class Monad m where
    (>=>) :: (a -> m b) -> (b -> m c) -> (a -> m c)
    return :: a -> m a

モナドを定義する方法には同等のものがいくつもあり、これがHaskellエコシステムでの主要な方法ではないことを覚えておいてほしい。この方法の概念の簡潔さと得られる直観は気に入っているが、プログラミングの際に便利な定義は他にもある。それらについて少し話そう。

この定式化ではモナド則を非常に簡単に表現できる。Haskellではモナド則を強制できないが、等式による推論には使える。それらは単にクライスリ圏の標準的な合成律だ:

(f >=> g) >=> h = f >=> (g >=> h) -- 結合性
return >=> f = f                  -- 左単位元
f >=> return = f                  -- 右単位元

この種の定義はモナドが本当は何なのかも表している。つまり、モナドは装飾された関数を合成する方法なのだ。副作用や状態は関係ない。関係あるのは合成だ。後で見るように、装飾された関数はさまざまな作用や状態を表現するために使われることがあるが、モナドはそのためのものではない。モナドは粘着力のあるダクトテープで、装飾された関数の一端を別の装飾された関数の一端につなぐ。

Writerの例に戻ろう:ログ生成関数(Writer関手でのクライスリ射)は圏を形成する。なぜなら、Writerはモナドだからだ。

instance Monoid w => Monad (Writer w) where
    f >=> g = \a ->
        let Writer (b, s)  = f a
            Writer (c, s') = g b
        in Writer (c, s `mappend` s')
    return a = Writer (a, mempty)

Writer wのモナド則は、wのモノイド則が満たされている限り満たされる(これらもHaskellでは強制できない)。

Writerモナド用に便利なクライスリ射が定義されており、tellと呼ばれる。その唯一の目的は引数をログに追加することだ:

tell :: w -> Writer w ()
tell s = Writer ((), s)

これは後で他のモナディック関数の構成要素として使うことになる。

20.2 Fishの解剖

さまざまなモナドに対してfish演算子を実装するとすぐ、コードに重複がたくさんあり、簡単に括り出せることに気付くだろう。まず、2つの関数のクライスリ合成は1つの関数を返す必要があるので、その実装も型aの引数を1つ取るラムダから始められる:

(>=>) :: (a -> m b) -> (b -> m c) -> (a -> m c)
f >=> g = \a -> ...

この引数はfに渡す以外のことはできない:

f >=> g = \a -> let mb = f a
                in ...

この時点で、型m cの結果を生成し、型m bの対象と関数g :: b -> m cを自由に使える必要がある。それを行う関数を定義しよう。この関数はbindと呼ばれ、通常は中置演算子の形式で記される:

(>>=) :: m a -> (a -> m b) -> m b

モナドごとに、fish演算子の代わりにbindを定義できる。実際、標準的なHaskellのモナドの定義ではbindが使われている:

class Monad m where
    (>>=) :: m a -> (a -> m b) -> m b
    return :: a -> m a

以下にWriterモナドのバインドの定義を示す:

(Writer (a, w)) >>= f = let Writer (b, w') = f a
                        in  Writer (b, w `mappend` w')

これは確かにfish演算子の定義より短い。

mが関手であるという事実を利用すればbindをさらに詳しく分析できる。fmapを使えばm aの内容に関数a -> m bを適用できる。それによってam bに変換される。したがって、適用結果は型m (m b)となる。これは必要な結果の型m bそのものではない。しかし、近付いてはいる。mの2重適用を潰す、つまりフラット化する関数さえあればよい。そのような関数はjoinと呼ばれる。

join :: m (m a) -> m a

joinを使ってbindを次のように書き直せる:

ma >>= f = join (fmap f ma)

これはモナドの定義の3番目の選択肢につながる:

class Functor m => Monad m where
    join :: m (m a) -> m a
    return :: a -> m a

ここではmFunctorであることを明示的に要求した。これまでの2つのモナドの定義ではその必要はなかった。型構成子mは、fish演算子とbind演算子のどちらかをサポートするなら自動的に関手になるからだ。たとえば、fmapはbindとreturnで定義できる:

fmap f ma = ma >>= \a -> return (f a)

完全を期すため、Writerモナドのjoinを以下に示す:

join :: Monoid w => Writer w (Writer w a) -> Writer w a
join (Writer ((Writer (a, w')), w)) = Writer (a, w `mappend` w')

20.3 do記法

モナドを使ってコードを書く方法の1つは、クライスリ射の使用――fish演算子によるそれらの合成だ。このプログラミング手法はポイントフリースタイルを一般化したものだ。ポイントフリーのコードはコンパクトで、実にエレガントなことが多い。しかし、一般には、理解するのが難しく暗号すれすれだ。だから、ほとんどのプログラマーは関数の引数や中間値に名前を付ける方を好む。

それはつまり、モナドを扱う場合にはfish演算子よりもbind演算子を優先するのを意味する。Bindはモナディックな値を取ってモナディックな値を返す。プログラマーはそれらの値に名前を付けても構わない。しかし、これは改善とは言えない。本当は、あたかも通常の値を扱っているかのようなふりがしたい。値をカプセル化するモナディックなコンテナーが欲しいのではない。命令型コードのように、グローバルなログの更新などの副作用のほとんどが視界から隠れていてほしい。そしてそれこそHaskellがdo記法でエミュレートするものだ。

ここで疑問に思うだろう。一体なぜモナドを使うのか? 副作用を見えなくしたいなら、命令型言語に留まればよいのではないか? その答えは、モナドなら副作用をはるかにうまくコントロールできる、というものだ。たとえば、Writerモナドのログは関数から関数へ渡され、グローバルに公開されることはない。ログを改竄したりデータ競合を引き起したりする可能性はない。また、モナディックなコードはプログラムの他の部分から明確に区分され隔離される。

do記法はモナディック合成のための単なる糖衣構文だ。表面上は命令型コードによく似ているが、bindとラムダ式のシーケンスに直接変換できる。

一例として、前にWriterモナドでのクライスリ射の合成を説明するために使った例を取り上げる。現在の定義を使うと、それは次のように書き直せる:

process :: String -> Writer String [String]
process = upCase >=> toWords

この関数は、動作ログを生成しつつ、入力文字列内のすべての文字を大文字に変換して単語に分割する。

do記法ならこうなる:

process s = do
    upStr <- upCase s
    toWords upStr

ここで、upCaseWriterを生成するのに、upStrは単なるStringだ:

upCase :: String -> Writer String String
upCase s = Writer (map toUpper s, "upCase ")

これはdoブロックがコンパイラーによって次のように脱糖されるためだ:

process s =
    upCase s >>= \upStr ->
        toWords upStr

upCaseのモナディックな結果はStringを引数とするラムダに束縛されている。その引数の名前はdoブロックに書かれていたものと同じだ。次の行:

upStr <- upCase s

は、upStr得るのはupCase sの結果だ、と読める。

擬似的な命令型スタイルはtoWordsをインライン化するとさらに顕著になる。2つの関数呼び出しに置き換えてインライン化しよう。まずtellを呼び出して、文字列"toWords"をログに記録する。続いてreturnを呼び出して、文字列upStrwordsによって分割した結果を返す。wordsは文字列を扱う通常の関数であることに注意してほしい。

process s = do
    upStr <- upCase s
    tell "toWords "
    return (words upStr)

ここで、doブロックの各行を脱糖すると、ネストされた新たなbindが導入される:

process s =
    upCase s >>= \upStr ->
      tell "toWords " >>= \() ->
        return (words upStr)

tellはunit値を生成するので、後続のラムダに渡す必要はないことに注意してほしい。モナディックな結果の内容を無視するのはよくあることだ(ただし、その作用――ここではログへの寄与――は無視しない)。そのため、そのような場合にbindを置き換える特別な演算子がある:

(>>) :: m a -> m b -> m b
m >> k = m >>= (\_ -> k)

実際に脱糖されたコードは次のようになる:

process s =
    upCase s >>= \upStr ->
      tell "toWords " >>
        return (words upStr)

一般に、doブロックは行(またはサブブロック)で構成され、左矢印によって新しい名前を導入してコードの残りの部分で使えるようにするか、あるいは純粋に副作用を目的として実行される。Bind演算子はコード行間で暗黙的に使われる。ちなみに、Haskellではdoブロックの書式を波括弧とセミコロンで置き換えられる。これが、モナドはセミコロンをオーバーロードする方法だ、と表現される理由になっている。

do記法を脱糖する際にラムダとbind演算子がネストされると、doブロックの残りを各行の結果に基づいて実行するのに影響を与えることに注目してほしい。この特性を使えば、例外をシミュレートするなど、複雑な制御構造を導入できる。

興味深いことに、do記法と同等のものが命令型言語に、典型的にはC++に応用されている。再開可能な関数 (resumable function) やコルーチン (coroutine) のことだ。C++のfutureがモナドを形成する100 ことは秘密ではない。それは継続モナドの一例だ。継続モナドについてはすぐ後で議論する。継続の問題点は、合成するのが非常に難しいことだ。Haskellではdoという表記法を使うことで、「こっちのハンドラーがそっちのハンドラーを呼ぶ」というスパゲッティを、逐次コードに非常によく似たものに変換している。再開可能な関数によって、同じ変換がC++でも可能になる。また、同じメカニズムを適用すれば、ネストされたループのスパゲッティ101 をリスト内包表記または「ジェネレーター」に変えられる。これは本質的にリストモナドのdo記法だ。モナドによる統一的な抽象化がなければ、これらの個々の問題は通常、言語にカスタム拡張を提供して対処されることになる。Haskellでは、すべてライブラリを通じて対処される。

21 モナドと作用

モナドが何のためにあるのかは理解できた。モナドは装飾された関数を合成できる。本当に興味深い疑問は、なぜ装飾された関数が関数プログラミングにおいてそんなに重要なのかということだ。すでに見た例として、Writerモナドでは、装飾によって複数の関数呼び出しにわたってログを作成し累積できた。通常は非純粋関数 (impure function) を使って(たとえば、何らかのグローバルな状態にアクセスし変更することで)解決するような問題を、純粋関数を使って解決した。

21.1 問題

よく似た問題を集めた短いリストを、エウジニオ・モッジの独創的な論文102から転載して以下に示した。どれも伝統的には関数の純粋さを放棄することで解決される:

  • 部分性:停止しない可能性のある計算
  • 非決定性:何種類もの結果を返す可能性のある計算
  • 副作用:状態にアクセス・変更する計算
    • 読み取り専用の状態、あるいは環境
    • 書き込み専用の状態、あるいはログ
    • 読み取り・書き込みの状態
  • 例外:失敗する可能性のある部分関数
  • 継続:プログラムの状態を保存でき、要求に応じて復元できること
  • インタラクティブな入力
  • インタラクティブな出力

本当に驚くべきことに、これらすべての問題は1つの巧妙なトリックで解決できる。装飾された関数にするというトリックだ。もちろん、装飾は各ケースで全く違うものになる。

装飾がモナディックだという条件は、この段階では必要ないことを認識しておかなくてはならない。合成――1つの装飾された関数をより小さな装飾された関数に分解できること――を主張するとき、初めてモナドが必要になる。また、それぞれの装飾が違うため、モナディック合成の実装方法もやはり違うものになるが、全体的なパターンは同じだ。非常に単純な、恒等射を伴った結合的な合成というパターンだ。

次節では、Haskellでの例について詳しく説明する。圏論に戻りたい人や、Haskellによるモナドの実装にすでに慣れている人は、遠慮なく斜め読みしたり飛ばしたりして構わない。

21.2 解決策

まず、Writerモナドの使用方法を分析してみよう。あるタスクを実行する純粋関数――引数を与えると、ある決まった出力が生成される関数――から始めた。この関数を、もとの出力を文字列とペアにすることで装飾する別の関数に置き換えた。これがログ生成の問題に対する解決策だった。

それで終わりにできなかったのは、一般に、モノリシックな解決策は扱いたくないからだ。1つのログ生成関数をより小さなログ生成関数に分解できる必要があった。こうした小さな関数の合成こそがモナドという概念への道標となった。

本当に驚くべきことは、関数の戻り値の型を装飾するという同一のパターンが、通常は純粋性を放棄する必要がある多種多様な問題に対して機能することだ。リストを見て、それぞれの問題に適用される装飾を順番に突き止めよう。

21.2.1 部分性

停止しない可能性のあるすべての関数について、戻り値の型を「リフトされた」型に変更する。その型はもとの型のすべての値および特別な「ボトム」値\botを含む。たとえば、集合としてのBool型はTrueFalseの2つの要素を含む。リフトされたBoolは3つの要素を含む。リフトされたBoolを返す関数は、TrueまたはFalseを生成するか、永久に実行され続ける。

面白いのは、Haskellのような遅延評価言語では、終わりのない関数が実際に値を返すことがあり、その値が次の関数に渡されうるということだ。この特別な値をボトムと呼ぶ。この値が(たとえば、パターンマッチングに使うため、あるいは出力として生成したため)明示的に必要とされているのでない限り、プログラムの実行を遅らせることなくこの値を渡せる。すべてのHaskellの関数は潜在的に停止しない関数であり得るため、Haskellのすべての型はリフトされることが想定されている。これが、単純な𝐒𝐞𝐭\mathbf{Set}ではなく、Haskellの(リフトされた)型と関数の圏𝐇𝐚𝐬𝐤\mathbf{Hask}について頻繁に議論する理由だ。もっとも、𝐇𝐚𝐬𝐤\mathbf{Hask}が本物の圏かどうかは議論がある (Andrej Bauerによる記事103を参照)。

21.2.2 非決定性

関数が多くの異なる結果を返すことがあるなら、一度にすべての結果を返す方がよいだろう。意味論的には、非決定論的関数は結果のリストを返す関数と等価だ。これはガベージコレクションのある遅延評価言語では実に理に適っている。たとえば、必要な値が1つだけの場合はリストのheadだけを取得すればよく、tailが評価されることはない。ランダムな値が必要な場合は乱数ジェネレーターを使ってリストのn番目の要素を選択する。遅延評価では結果の無限リストを返すことさえ可能になる。

リストモナド――Haskellでの非決定論的計算の実装――ではjoinconcatとして実装されている。joinの役割はコンテナーのコンテナーをフラット化することなのを思い出してほしい――concatはリストのリストを連接して1つのリストにする。また、returnは単要素リストを作成する:

instance Monad [] where
    join = concat
    return x = [x]

リストモナドのbind演算子は、fmapの後にjoinが続くという一般式で与えられ、ここでは次のようになる:

as >>= k = concat (fmap k as)

ここで、関数kはそれ自体がリストを生成する関数で、リストasのすべての要素に適用される。結果はリストのリストとなり、concatを使ってフラット化される。

プログラマーの観点では、リストの操作は比較的簡単であり、たとえば、非決定論的関数をループ内で呼び出したり、イテレーターを返す関数を実装したりする方が難しい (ただし、近年のC++104では、遅延評価するrangeを返すのはHaskellでリストを返すのとほぼ等価だ)。

非決定性が創造的に使われる好例はゲームプログラミングだ。たとえば、コンピューターが人間を相手にチェスをするとき、相手の次の動きは予測できない。しかし、すべての可能な移動のリストを生成して1つずつ分析できる。同様に、非決定論的な構文解析器は、与えられた式に対して可能なすべての構文解析のリストを生成できる。

リストを返す関数は非決定論的だと解釈してもよいものの、リストモナドの応用範囲はもっと広い。リストを生成する計算をつなぎ合わせることは、命令型プログラミングで使われる反復構文――ループ――を関数プログラミングで完全に代替する手段だからだ。単一のループは、多くの場合fmapを使って書き直せる。fmapはループの本体をリストの各要素に適用する。リストモナドでdo記法を使えば、複雑な多重ループを置き換えられる。

私のお気に入りの例は、ピタゴラス数 (Pythagorean triple) ――直角三角形の辺を形成できる3つの正の整数の組――を生成するプログラムだ。

triples = do
    z <- [1..]
    x <- [1..z]
    y <- [x..z]
    guard (x^2 + y^2 == z^2)
    return (x, y, z)

最初の行は、zが正の整数の無限リスト[1..]から要素を取得することを示している。次にxは、1からzまでの整数の(有限)リスト[1..z]から要素を取得する。最後にyは、xからzまでの整数のリストから要素を取得する。これで1xyz1 \leqslant x \leqslant y \leqslant zの3つの整数を自由に使える。関数guardは、Bool式を取り、unitのリストを返す。

guard :: Bool -> [()]
guard True  = [()]
guard False = []

この関数(MonadPlusというより大きなクラスのメンバー)は、ここでは非ピタゴラス数を除外するために使われる。実際、bind(または関連する演算子>>)の実装を見ると、空リストが与えられると空リストが生成されることに気付くだろう。一方、空でないリスト(ここではunitを含む単リスト[()])が与えられた場合、bindは継続を呼ぶ。ここではreturn(x, y, z)だ。これは検証済みのピタゴラス数を持つ単リストを生成する。これらの単リストはすべて、内包するbindによって連接され、最終的な(無限の)結果を生成する。当然、tripleの呼び出し側はリスト全体を消費できないが、Haskellは遅延評価言語なので問題ない。

通常は3重にネストされたループを必要とする問題が、リストモナドとdo記法のおかげで劇的に単純化された。それだけでは不十分と言わんばかりに、Haskellではこのコードをリスト内包表記を使ってさらに単純化する:

triples = [(x, y, z) | z <- [1..]
                     , x <- [1..z]
                     , y <- [x..z]
                     , x^2 + y^2 == z^2]

これはリストモナド(厳密に言えば、MonadPlus)のためのさらなる糖衣構文だ。

他の関数型言語や命令型言語でも、ジェネレーターやコルーチンという名のもとに同様の構造が見られることがある。

21.2.3 読み取り専用の状態

外部の状態すなわち環境に対して読み取り専用でアクセスする関数は、その環境を追加の引数として取る関数に常に置き換え可能だ。純粋な関数(a, e) -> b(ここで、eは環境の型)は、一見したところ、クライスリ射のようには見えない。しかし、それをa -> (e -> b)にカリー化すれば、その装飾がお馴染みのreader関手なのがすぐ分かる:

newtype Reader e a = Reader (e -> a)

Readerを返す関数は、ある環境を与えると望んだ結果を生成するような作用を持つ小さな実行可能コードを生成する、と解釈できる。ヘルパー関数runReaderはそのような作用を実行する:

runReader :: Reader e a -> e -> a
runReader (Reader f) e = f e

環境の値が異なれば生成される結果も異なりうる。

Readerを返す関数もReaderの作用自体も純粋であることに注意してほしい。

Readerモナド用にbindを実装するには、まず、環境eを取得してbを生成する関数を生成する必要がある:

ra >>= k = Reader (\e -> ...)

ラムダ内では、作用raを実行してaを生成できる:

ra >>= k = Reader (\e -> let a = runReader ra e
                         in ...)

次に、aを継続kに渡せば新しい作用rbが得られる:

ra >>= k = Reader (\e -> let a  = runReader ra e
                             rb = k a
                         in ...)

最後に、環境eで作用rbを実行できる:

ra >>= k = Reader (\e -> let a  = runReader ra e
                             rb = k a
                         in runReader rb e)

returnを実装するには、環境を無視して値をそのまま返す作用を作成すればよい。

いくつかの簡略化を経てまとめると、次の定義が得られる:

instance Monad (Reader e) where
    ra >>= k = Reader (\e -> runReader (k (runReader ra e)) e)
    return x = Reader (\e -> x)

21.2.4 書き込み専用の状態

これは最初に例にしたログ生成と同じだ。装飾はWriter関手によって与えられる:

newtype Writer w a = Writer (a, w)

完全を期すため、データ構成子をアンパックする小さなヘルパー関数runWriterも示そう:

runWriter :: Writer w a -> (a, w)
runWriter (Writer (a, w)) = (a, w)

すでに見たとおり、Writerを合成可能にするには、wはモノイドでなければならない。bind演算子で書かれたWriterMonadインスタンスを以下に示す:

instance (Monoid w) => Monad (Writer w) where
    (Writer (a, w)) >>= k = let (a', w') = runWriter (k a)
                            in Writer (a', w `mappend` w')
    return a = Writer (a, mempty)

21.2.5 状態

状態へ読み取り/書き込みアクセスする関数は、ReaderWriterの装飾を組み合わせる。それらは、追加の引数として状態を受け取り、結果として値/状態のペアを生成する純粋関数 (a, s) -> (b, s) と見なせる。カリー化すると、それらはクライスリ射a -> (s -> (b, s))の形になり、装飾はState関手へと抽象化される:

newtype State s a = State (s -> (a, s))

ここでも、クライスリ射は作用を返すものと見なせて、ヘルパー関数を使って実行できる:

runState :: State s a -> s -> (a, s)
runState (State f) s = f s

初期状態が異なると、結果が異なりうるだけでなく、最終状態も異なりうる。

Stateモナドに対するbindの実装はReaderモナドの実装と非常によく似ている。ただし、各ステップで正しい状態を渡すように注意する必要がある:

sa >>= k = State (\s -> let (a, s') = runState sa s
                            sb = k a
                        in runState sb s')

インスタンス全体は次のとおりだ:

instance Monad (State s) where
    sa >>= k = State (\s -> let (a, s') = runState sa s
                            in runState (k a) s')
    return a = State (\s -> (a, s))

ヘルパーとしてのクライスリ射も2つあり、状態を操作するために使える。そのうちの1つは状態を調べるために取得する:

get :: State s s
get = State (\s -> (s, s))

もう1つは状態を全く新しいものに置き換える:

put :: s -> State s ()
put s' = State (\s -> ((), s'))

21.2.6 例外

例外をスローする命令型関数は、実際には部分関数――引数の値のいくつかに対して定義されていない関数だ。例外を最もシンプルに純粋全域関数として実装するにはMaybe関手を使う。部分関数を全域関数へと拡張すればよい。つまり、意味があるならJust aを、そうでないならNothingを返すように拡張する。失敗の原因に関する何らかの情報も返したい場合は、代わりにEither関手を使える(第1の型は固定され、たとえばStringとされる)。

以下はMaybeMonadインスタンスだ:

instance Monad Maybe where
    Nothing >>= k = Nothing
    Just a  >>= k = k a
    return a = Just a

Maybeについてのモナディック合成では、エラーが検出されたときは計算がきちんと短絡評価される(継続kが決して呼ばれない)ことに注意してほしい。これは例外に期待される振る舞いだ。

21.2.7 継続

これは「電話はしないでください、こちらから電話しますので!」という、採用面接のあとで経験しそうな状況を表す105。回答を直接得る代わりに、ある関数をハンドラーとして提供することになっており、そのハンドラーが呼ばれると結果が得られる。このスタイルのプログラミングが特に役立つのは、呼び出し時点で結果が分からない場合だ。たとえば、別のスレッドで評価される場合や、リモートのウェブサイトから配信される場合などだ。この場合のクライスリ射が返す関数は「残りの計算」を表すハンドラーを受け取る:

data Cont r a = Cont ((a -> r) -> r)

このハンドラーa -> rは、最終的に呼び出されると型rの結果を生成し、その結果が最後に返される。継続は結果の型によってパラメーター化される。(実用上は、これはある種のステータスインジケーターである場合が多い。)

クライスリ射から返された作用を実行するためのヘルパー関数もある。それはハンドラーを取得して継続に渡す:

runCont :: Cont r a -> (a -> r) -> r
runCont (Cont k) h = k h

継続の合成は非常に難しいことで知られており、それゆえモナドや、特にdo記法による処理は非常に有利だ。

bindの実装について説明しよう。まず、本質だけに絞ったシグネチャーに注目する:

(>>=) :: ((a -> r) -> r) ->
         (a -> (b -> r) -> r) ->
         ((b -> r) -> r)

目標は、ハンドラー(b -> r)を受け取って結果rを生成する関数を作成することだ。そこで出発点はこうなる:

ka >>= kab = Cont (\hb -> ...)

このラムダの内部で、ka関数を、残りの計算を表す適切なハンドラーを添えて呼び出したい。そのハンドラーをラムダとして実装する:

runCont ka (\a -> ...)

この場合、残りの計算は、まずkabaで呼び出し、次にhbを結果の作用kbに渡すことを含む:

runCont ka (\a -> let kb = kab a
                  in runCont kb hb)

ご覧のとおり、継続は裏返しに合成されている。最終的なハンドラーhbは、計算の最も内側の層から呼び出される。インスタンス全体は次のとおりだ:

instance Monad (Cont r) where
    ka >>= kab = Cont (\hb -> runCont ka (\a -> runCont (kab a) hb))
    return a = Cont (\ha -> ha a)

21.2.8 インタラクティブな入力

これは最も厄介な問題であり、多くの混乱の源だ。明らかに、getCharのような関数は、キーボードから入力された文字を返すとしたら、純粋ではあり得ない。しかし、コンテナー内の文字を返すとしたらどうだろう? そのコンテナーから文字を除去する方法がない限り、この関数は純粋であると主張できる。getCharを呼び出すたびに、全く同じコンテナーが返される。概念としては、このコンテナーには可能な文字すべての重ね合わせが含まれる。

量子力学に馴染みがあれば、この比喩を問題なく理解できるだろう。これはシュレーディンガーの猫が入った箱のようなものだ――ただし、箱を開けたり中を覗いたりする方法はない。この箱は、組み込みの特別なIO関手を使って定義される。この例ではgetCharをクライスリ射として宣言できる:

getChar :: () -> IO Char

(実際には、unit型を取る関数は戻り値の型の値を選択するのと等価なので、getCharの宣言は単純にgetChar :: IO Charとなる。)

IOは関手なのでfmapを使って内容を操作できる。また、関手として、文字だけでなく任意の型を内容として保持できる。このアプローチの真の有用性が明らかになるのは、HaskellでのIOがモナドなのを考慮したときだ。これはIOオブジェクトを生成するクライスリ射を合成できることを意味する。

クライスリ合成を使えば、IOオブジェクトの内容を覗き見できると思うかもしれない(またも量子力学で比喩すると「波動関数を収縮させる」ことにあたる)。実際、getCharは、文字を受け取る別のクライスリ射と合成できる。たとえば、受け取った文字を整数に変換するようなものと合成できる。注意点として、そのような2番目のクライスリ射は整数を(IO Int)としてしか返せない。ここでも、すべての可能な整数を重ね合わせることになる。以下同様だ。シュレーディンガーの猫は決して外に出ない。IOモナドに入ると、抜け出す方法はない。IOモナドについてrunStaterunReaderと同等のものはない。runIOはないのだ!

では、クライスリ射の結果であるIOオブジェクトを、他のクライスリ射と合成する以外に何ができるだろう? そう、mainから返せる。Haskellではmainは次のシグネチャーを持つ:

main :: IO ()

そしてこれはクライスリ射と見なして構わない:

main :: () -> IO ()

この観点からは、HaskellのプログラムはIOモナドを使った1つの大きなクライスリ射にすぎない。モナディック合成を使えば複数の小さなクライスリ射から合成できる。生成されたIOオブジェクト(IOアクションとも呼ばれる)で何を行うかはランタイムシステム次第だ。

矢自体が純粋関数であることに注目してほしい――それは一貫して純粋関数だ。純粋でない仕事はシステムへと追いやられている。mainから返されたIOアクションを最終的に実行するとき、あらゆる種類の汚れ仕事が行われる。たとえば、ユーザー入力の読み取り、ファイルの変更、不愉快なメッセージの表示、ディスクのフォーマット、などなどだ。Haskellのプログラムは決して手を汚すことはない(ただし、unsafePerformIOを呼び出す場合は除くが、それは別の話だ)。

当然、Haskellは遅延評価なのでmainはほとんど即座に戻り、純粋でない仕事はすぐに始まる。IOアクションの実行中に、純粋な計算の結果が要求され評価される。そのため、実際には、プログラムの実行では純粋な(Haskellの)コードと純粋でない(システムの)コードが折り重なっている。

IOモナドには別の解釈がある。さらに奇妙な解釈だが、数学モデルとしては完全な意味をなす。それは宇宙全体をプログラム中のオブジェクトとして扱う。概念として、命令型モデルは宇宙を外部のグローバルオブジェクトとして扱うため、入出力を実行する手続きにはそのオブジェクトとの相互作用による副作用が伴うことに着目してほしい。それらは宇宙の状態を読み取ることも変更することもできる。

関数プログラミングで状態を扱う方法はすでに知っている――Stateモナドを使えばよい。だが、宇宙の状態は単純ではないので、標準的なデータ構造では簡単に記述できない。しかし、直接扱わない限り、その必要はない。型RealWorldが存在し、宇宙工学の奇跡によってランタイムがこの型のオブジェクトを提供できる、と仮定すれば十分だ。IO作用はただの関数だ:

type IO a  =  RealWorld -> (a, RealWorld)

あるいは、Stateモナドとしてはこうなる:

type IO = State RealWorld

ただし、IOモナドの>=>returnは言語に組み込まれている必要がある。

21.2.9 インタラクティブな出力

同じIOモナドが、インタラクティブな出力をカプセル化するのにも使われる。RealWorldはすべての出力デバイスを含むと見なされる。なぜ単にHaskellから出力関数を呼び出して、何もしないふりをすることができないのか疑問に思うだろう。たとえば、なぜ次のように書くのだろう:

putStr :: String -> IO ()

単純に次のように書くのではいけないのだろうか:

putStr :: String -> ()

理由は2つある:Haskellは遅延評価なので、出力――ここではunitオブジェクト――が全く使われない関数は呼び出されない。また、遅延評価でなかったとしても、そのような呼び出しの順序は自由に変更でき、出力を混乱させうる。Haskellで2つの関数を強制的に逐次実行する唯一の方法はデータ依存性を使うことだ。ある関数の入力を別の関数の出力に依存させる必要がある。IOアクション間でRealWorldを受け渡せば逐次処理を強制できる。

概念としては、以下のプログラム:

main :: IO ()
main = do
    putStr "Hello "
    putStr "World!"

では、「World!」と表示するアクションは「Hello」がすでに画面に表示されている宇宙を入力として受け取る。そして画面に「Hello World!」と表示された新しい宇宙を出力する。

21.3 結論

当然ながらここではモナドを使うプログラミングのほんの表面を撫でたにすぎない。モナドは、命令型プログラミングで副作用を使って通常行われることを純粋関数で行うだけでなく、高度な制御と型安全性を備えて実現している。だが、欠点がないわけではない。モナドへのよくある不満は、互いに簡単に合成できないことだ。だとしても、基本的なモナドのほとんどはモナド変換子 (Monad transformer) ライブラリを使って結合できる。たとえば状態と例外を組み合わせたモナドスタックを作成するのは比較的簡単だ。ただし、任意のモナドをスタックするためのレシピはない。

22 圏論から見たモナド

モナドについてプログラマーに話すと、おそらく作用について話すことになるだろう。だが、数学者にとってはモナドは代数に関するものだ。代数については後で話そう――代数はプログラミングにおいて重要な役割を果たす――まずは、代数とモナドとの関係について少し直観を得てもらおうと思う。さしあたっては、やや身振り手振りでの議論になるが、我慢してほしい。

代数とは式を作成・操作・評価することだ。式は演算子を使って組み立てられる。次の単純な式を考えてみよう: x2+2x+1x^2 + 2 x + 1 この式はxxのような変数と1122のような定数とを加算や乗算のような演算子で組み合わせて構成されている。プログラマーはよく式を木 (tree) と見なす。

木はコンテナーなので、より一般化すると、式は変数を格納するコンテナーだと言える。圏論ではコンテナーを自己関手として表す。型aaを変数xxに代入すると、式は型mam\ aを持つことになる。ここで、mmは式木を構成する自己関手だ。(非自明な分岐式は通常、再帰的に定義された自己関手を使って作成される。)

式に対して実行できる最もありふりた操作は何だろう? それは置換だ。つまり、変数を式に置き換えることだ。たとえば、この例ではxxy1y-1に置き換えて次のようにできる: (y1)2+2(y1)+1(y - 1)^2 + 2 (y - 1) + 1 何が起きたかというと、型mam\ aの式を取って型amba \to m\ bへの変換を適用した(bbyyの型を表す)。結果は型mbm\ bの式になる。はっきり書いてみよう: ma(amb)mbm\ a \to (a \to m\ b) \to m\ b そう、これはモナドにおけるbindのシグネチャーだ。

ちょっとしたモチベーションが得られた。さあ、モナドの計算に移ろう。数学者はプログラマーとは異なる表記法を用いる。自己関手には文字TTを使い、joinにはμ\mureturnにはη\etaというギリシャ文字を使うのが好みだ。joinreturnも多相関数なので、自然変換に対応すると推測できる。

それゆえ、圏論では、モナドは自然変換μ\muη\etaのペアを伴った自己関手TTとして定義される。

μ\muは関手の平方T2T^2からTTへ戻る自然変換だ。この平方は単に関手をそれ自身とTTT \circ Tのように合成したものだ(このような平方は自己関手でしか行えない): μT2T\mu \Colon T^2 \to T この自然変換の対象aaにおける成分は次の射だ: μaT(Ta)Ta\mu_a \Colon T (T a) \to T a これは、𝐇𝐚𝐬𝐤\mathbf{Hask}では、joinの定義に直接変換される。

η\etaは恒等関手IITTの間の自然変換だ: ηIT\eta \Colon I \to T 対象aaに対しIIを作用させると単にaaになるのを考慮すると、η\etaの成分は次の射によって与えられる: ηaaTa\eta_a \Colon a \to T a これはreturnの定義に直接変換される。

これらの自然変換にはいくつか追加で満たすべき規則がある。1つの観点としては、これらの規則によって自己関手TTに対してクライスリ圏を定義できる。aabbの間のクライスリ射は射aTba \to T bとして定義されることを思い出そう。2つのそのような射の合成(ここでは下付き文字TTを添えた円で記す)はμ\muを使って実装できる: gTf=μc(Tg)fg \circ_T f = \mu_c \circ (T g) \circ f ただし: faTbgbTc \begin{gathered} f \Colon a \to T b \\ g \Colon b \to T c \end{gathered} ここでTTは関手であり、射ggに適用できる。この式はHaskellの記法で理解した方が簡単だろう:

f >=> g = join . fmap g . f

あるいは、成分で表すとこうなる:

(f >=> g) a = join (fmap g (f a))

代数的に解釈すると、単に2つの連続した置換を合成しているだけだ。

クライスリ射が圏を形成するには、それらの合成が結合性を持っていて、かつηa\eta_aaaにおける恒等クライスリ射であってほしい。この要件はμ\muη\etaのモナド則に変換できる。しかし、これらの規則は、別の方法で導出すればもっとモノイド則に似て見えてくる。実際、μ\muはしばしば乗算 (multiplication) と呼ばれ、η\eta単位 (unit) と呼ばれる。

大まかに言うと、結合律は、TTの立方T3T^3TTに縮約するための2つの方法が同じ結果を与えなければならない、と述べている。また、左単位律と右単位律は、η\etaTTに適用された後にμ\muで縮約すると再びTTが得られる、と述べている。

少しトリッキーなのは自然変換と関手を合成しているからだ。そこで水平合成について少し復習するのがよいだろう。たとえば、T3T^3TTT2T^2の後に合成したものと見なせる。これに、次の2つの自然変換の水平合成を適用できる: ITμI_T \circ \mu

するとTTT \circ Tが得られ、μ\muを適用するとさらにTTに縮約できる。ITI_TTTからTTへの恒等自然変換だ。このような水平合成ITμI_T \circ \muはよくTμT \circ \muと短く表記される。この表記に曖昧さはない。なぜなら、ある関手を自然変換と合成するのは意味がなく、それゆえこの文脈ではTTは必ずITI_Tを意味するからだ。

(自己)関手圏[𝐂,𝐂]{[}\mathbf{C}, \mathbf{C}{]}に図式を描くこともできる。

あるいは、T3T^3を合成T2TT^2 \circ Tとして扱い、μT\mu \circ Tを適用してもよい。その結果もTTT \circ Tとして扱い、μ\muをまた使ってTTに戻せる。これら2つの経路は同じ結果を生成する必要がある。

同様に、恒等関手IITTの後に合成したものに対して水平合成ηT\eta \circ Tを適用してT2T^2が得られ、μ\muを使って戻せる。その結果は恒等自然変換をTTに直接適用したかのように同じになるはずだ。そして、同様のことがTηT \circ \etaにも当てはまると類推できる。

クライスリ射の合成が実際に圏の規則を満たすことをこれらの規則が保証するのは納得できるだろう。

モナドとモノイドの類似点は驚くべきものだ。どちらにも乗算μ\mu・単位η\eta・結合律・単位律が存在する。しかし、我々のモノイドの定義は狭すぎて、モナドをモノイドとしては記述できない。そこで、モノイドの概念を一般化しよう。

22.1 モノイダル圏

モノイドの従来の定義に戻ろう。それは二項演算と単位という特殊な要素を持つ集合だ。Haskellでは、これは型クラスとして表現できる:

class Monoid m where
    mappend :: m -> m -> m
    mempty  :: m

二項演算mappendは結合的かつ単位的 (unital) でなければならない(すなわち、単位memptyによる乗算はno-opだ)。

Haskellでのmappendの定義はカリー化されていることに注意してほしい。これはmのすべての要素を次の関数に写すと解釈できる:

mappend :: m -> (m -> m)

この解釈こそがモノイドを単一対象圏として定義するもととなる。その単一対象圏の自己準同型 (endomorphism) である(m -> m)がモノイドの要素を表す。ただし、Haskellにはカリー化が組み込まれているので、乗算の別の定義から始めてもよい:

mu :: (m, m) -> m

ここで、デカルト積(m, m)は乗算されるペアの始域になる。

この定義は一般化への別の道を示唆する:デカルト積を圏論的な積に置き換えるというものだ。積が大域的に定義されている圏から始め、対象mを選択し、乗算を射として定義できる: μm×mm\mu \Colon m\times{}m \to m ただし、問題が1つある:任意の圏では対象の内部を覗き見られないのに、どうすれば単位元を選択できるだろう? そのためのトリックがある。要素選択が単元集合からの関数と等価になる仕組みを覚えているだろうか? Haskellでは、memptyの定義を次の関数に置き換えられた:

eta :: () -> m

単元集合は𝐒𝐞𝐭\mathbf{Set}の終対象なので、終対象ttを持つすべての圏にこの定義を一般化するのは自然だ: ηtm\eta \Colon t \to m これにより、要素について述べることなく単位「要素」を選べる。

以前モノイドを単一対象圏として定義したときとは違って、ここではモノイド則が自動的に満たされるわけではなく、それらを課す必要がある。しかし、それらを定式化するには、基礎となる圏論的な積そのもののモノイドの構造を確立しなければならない。まずはHaskellでモノイドの構造がどう機能するか思い出そう。

結合性から始める。Haskellでは、対応する等式的規則 (equational law) はこうなる:

mu x (mu y z) = mu (mu x y) z

これを他の圏に一般化する前に、関数(射)の等しさとして書き直す必要がある。個々の変数に対する作用から離れた抽象化が必要だ――言い換えれば、ポイントフリー記法を使う必要がある。デカルト積は双関手だと分かっているので、左辺はこう書ける:

(mu . bimap id mu)(x, (y, z))

そして右辺はこうなる:

(mu . bimap mu id)((x, y), z)

これはほぼ希望どおりだ。残念ながら、厳密に言うとデカルト積には結合性がない――(x, (y, z))((x, y), z)は異なる――ので、ポイントフリーでは書けない:

mu . bimap id mu = mu . bimap mu id

一方、ペアのネスト2つは同型だ。それらの間を変換する結合子 (associator) と呼ばれる可逆関数がある:

alpha :: ((a, b), c) -> (a, (b, c))
alpha ((x, y), z) = (x, (y, z))

結合子の助けを借りればmuについての結合律をポイントフリーで書ける:

mu . bimap id mu . alpha = mu . bimap mu id

単位律にも同様のトリックを適用できる。新しい記法では次のような形式になる:

mu (eta (), x) = x
mu (x, eta ()) = x

これらは次のように書き直せる:

(mu . bimap eta id) ((), x) = lambda ((), x)
(mu . bimap id eta) (x, ()) = rho (x, ())

2つの同型射のうちlambdaは左単位子 (left unitor) と呼ばれ、rhoは右単位子 (right unitor) と呼ばれる。これらが示すのは、単位 () は同型を除いてデカルト積の恒等射であるという事実だ:

lambda :: ((), a) -> a
lambda ((), x) = x
rho :: (a, ()) -> a
rho (x, ()) = x

したがって、ポイントフリー版の単位律は次のようになる:

mu . bimap id eta = lambda
mu . bimap eta id = rho

ここまでで、muetaについてのポイントフリーなモノイド則の定式化を、基礎となるデカルト積自体が型の圏におけるモノイド的な乗算のように作用するという事実を用いて行った。ただし、留意すべき点として、デカルト積の結合律と単位律は同型を除いてのみ有効だ。

これらの規則は積と終対象を持つすべての圏に一般化できると分かった。圏論的な積は確かに同型を除いて結合的であり、同様に、同型を除いて終対象は単位だ。結合子と2つの単位子は自然同型だ。規則は可換図式で表せる。

積は双関手なので射のペアをリフトできることに注意してほしい――Haskellではそのためにbimapが使われる。

ここで立ち止まって、圏論的な積と終対象を持つあらゆる圏においてモノイドを定義できる、と述べてよい。モノイド則を満たすような対象mmと2つの射μ\muη\etaを選択できるなら、モノイドが存在する。しかし、もっとうまくやれる。μ\muη\etaについて規則を定式化するために完全な圏論的な積は必要ない。積の定義は射影を使う普遍的構成によって行われるのを思い出してほしい。モノイド則の定式化では射影を一切使わなかった。

積ではないのに積のように振る舞う双関手はテンソル積 (tensor product) と呼ばれ106、中置演算子\otimesで表されることが多い。テンソル積の一般的な定義は少しトリッキーだが、気にしないでよい。ここでは単にその特性を列挙する――最も重要なのは同型を除いた結合性だ。

同様に、対象ttが終対象である必要はない。それが終対象であるという特性――つまり、どの対象からも一意な射が存在すること――を使ったことはない。必要なのは、それがテンソル積とうまく協調して働くことだ。つまり、同型を除いてテンソル積の単位であってほしい。以上をまとめてみよう:

モノイダル圏𝐂\mathbf{C}はテンソル積と呼ばれる双関手を伴う: 𝐂×𝐂𝐂\otimes \Colon \mathbf{C}\times{}\mathbf{C} \to \mathbf{C} そして、単位対象と呼ばれる特定の対象iiと、それぞれ結合子・左単位子・右単位子と呼ばれる3つの自然同型写像も伴う: αabc(ab)ca(bc)λaiaaρaaia \begin{aligned} \alpha_{a b c} &\Colon (a \otimes b) \otimes c \to a \otimes (b \otimes c) \\ \lambda_a &\Colon i \otimes a \to a \\ \rho_a &\Colon a \otimes i \to a \end{aligned} (テンソル四重積を単純化するためのコヒーレンス条件 (coherence condition) も存在する。)

重要なのは、よく知られた多くの双関手がテンソル積で記述できることだ。それは特に、積、余積、そしてすぐ後でみるように自己関手の合成(そしてDay convolutionのようなより深遠な積)に対して機能する。モノイダル圏は豊穣圏の定式化において不可欠な役割を果たすことになる。

22.2 モノイダル圏におけるモノイド

モノイダル圏という、より一般的な設定を使ってモノイドを定義する準備が整った。対象mmを選択することから始めよう。テンソル積を使えばmmの冪を形成できる。mmの平方はmmm \otimes mだ。mmの立方を形成する方法は2つあるが、それらは結合子を通じて同型だ。より高次のmmの冪についても同様だ(そこではコヒーレンス条件が必要になる)。モノイドを形成するには2つの射を選ぶ必要がある: μmmmηim \begin{aligned} \mu &\Colon m \otimes m \to m \\ \eta &\Colon i \to m \end{aligned} ここで、iiはテンソル積の単位対象だ。

これらの射は結合律および単位律を満たさなければならない。それらは以下の可換図式で表せる:

テンソル積は必然的に双関手であることに注意してほしい。これは、μ𝐢𝐝\mu \otimes \mathbf{id}η𝐢𝐝\eta \otimes \mathbf{id}のような積を形成するには射のペアをリフトする必要があるからだ。これらの図式は、圏論的な積に関するこれまでの結果を直接的に一般化しているにすぎない。

22.3 モノイドとしてのモナド

モノイドの構造は思いがけないところに現れる。その1つが関手圏だ。少し目を細めれば、関手の合成が積の生成に見えてくるだろう。問題は、2つの関手は合成できるとは限らないことだ――一方にとっての行き先となる圏が他方にとってのもととなる圏でなければならない。これは射の合成の一般的な規則に他ならない――そして、すでに知っているとおり、関手はまさに圏𝐂𝐚𝐭\mathbf{Cat}内の射だ。ただし、自己射(同じ対象にループバックする射)と同様に自己関手も常に合成可能だ。任意の圏𝐂\mathbf{C}について、𝐂\mathbf{C}から𝐂\mathbf{C}への自己関手は関手圏[𝐂,𝐂]{[}\mathbf{C}, \mathbf{C}{]}を形成する。その対象は自己関手であり、射はそれらの間の自然変換だ。この圏から任意の2つの対象、たとえば自己関手FFGGを取って、3番目の対象FGF \circ G――それらを合成した自己関手――を生成できる。

自己関手の合成はテンソル積の良い候補だろうか? まず、それが双関手だと示さなければならない。それを使って射――ここでは自然変換――のペアをリフトできるだろうか? テンソル積でbimapに相当するもののシグネチャーは次のようになるだろう: 𝑏𝑖𝑚𝑎𝑝(ab)(cd)(acbd)\mathit{bimap} \Colon (a \to b) \to (c \to d) \to (a \otimes c \to b \otimes d) 対象を自己関手で、矢印を自然変換で、テンソル積を合成で置き換えると: (FF)(GG)(FGFG)(F \to F') \to (G \to G') \to (F \circ G \to F' \circ G') となり、水平合成の特殊なケースだと認識してよい。

また、恒等自己関手IIも存在し、自由に使える。これは自己関手の合成――新しいテンソル積――の恒等射として機能する。そのうえ、関手の合成は結合的だ。事実、結合律と単位律は厳密だ――結合子や2つの単位子は必要ない。したがって、自己関手は関手合成をテンソル積として伴う厳密なモノイダル圏を形成する。

この圏でのモノイドとは何だろう? それは対象――自己関手TTと、2つの射――自然変換だ: μTTTηIT \begin{gathered} \mu \Colon T \circ T \to T \\ \eta \Colon I \to T \end{gathered} それだけでなく、モノイド則もある:

これらはいままで見てきたモナド則そのものだ。これでソーンダーズ・マックレーンの有名な引用が理解できるだろう:

端的に言えば、モナドは自己関手からなる圏におけるモノイドに他ならない。

これが関数プログラミングのカンファレンスのTシャツにプリントされているのを見たことがあるかもしれない。

22.4 随伴に基づくモナド

随伴LRL \dashv Rは2つの圏𝐂\mathbf{C}𝐃\mathbf{D}の間を行き来する一対の関手だ。それらを合成する方法は2つあり、2つの自己関手RLR \circ LLRL \circ Rが生じる。随伴によると、これらの自己関手は単位と余単位と呼ばれる2つの自然変換を通じて恒等関手と関係している: ηI𝐃RLεLRI𝐂 \begin{gathered} \eta \Colon I_{\mathbf{D}} \to R \circ L \\ \varepsilon \Colon L \circ R \to I_{\mathbf{C}} \end{gathered} すぐ分かるとおり、随伴の単位はモナドの単位にそっくりだ。自己関手RLR \circ Lは実際にモナドなのが分かる。必要なのは、η\etaに対応する適切なμ\muを定義することだけだ。これは自己関手の四角い図式と自己関手自体との間の自然変換であり、随伴関手では次のように表される: RLRLRLR \circ L \circ R \circ L \to R \circ L そして、実際、余単位を使えば真ん中のLRL \circ Rを潰せる。μ\muの実際の式は水平合成で与えられる: μ=RεL\mu = R \circ \varepsilon \circ L モナド則は、随伴の単位と余単位によって満たされた恒等律 (identity law) と、交換律から導かれる。

随伴から派生したモナドをHaskellであまり見かけないのは、随伴には通常2つの圏が関わるからだ。ただし、冪すなわち関数対象の定義は例外だ。この随伴は次の2つの自己関手から形成される: Lz=z×sRb=sb \begin{gathered} L z = z\times{}s \\ R b = s \Rightarrow b \end{gathered} これらを合成するとお馴染みのstateモナドになるのが分かるだろう: R(Lz)=s(z×s)R (L z) = s \Rightarrow (z\times{}s) このモナドは以前Haskellで見たことがある:

newtype State s a = State (s -> (a, s))

もとの随伴もHaskellに翻訳してみよう。左関手は積関手だ:

newtype Prod s a = Prod (a, s)

そして右関手はreader関手だ:

newtype Reader s a = Reader (s -> a)

これらは随伴を形成する:

instance Adjunction (Prod s) (Reader s) where
  counit (Prod (Reader f, s)) = f s
  unit a = Reader (\s -> Prod (a, s))

reader関手を積関手の後に合成したものが実際にstate関手と等価なのは簡単に納得できる:

newtype State s a = State (s -> (a, s))

予想どおり、随伴のunitはstateモナドのreturn関数と等価だ。counitは、引数に作用する関数を評価することによって作用する。これはrunState関数の非カリー化版と見なせる。

runState :: State s a -> s -> (a, s)
runState (State f) s = f s

(非カリー化する理由は、counitでペアに作用するからだ)。

ここでstateモナドのjoinを自然変換μ\muの成分として定義できる。そのためには3つの自然変換を水平合成したものが必要だ: μ=RεL\mu = R \circ \varepsilon \circ L 言い換えると、余単位ε\varepsilonをreader関手の1つのレベルに忍び込ませる必要がある。単にfmapを直接呼び出すことはできない。なぜなら、コンパイラーがReader関手のものではなくState関手のものを選択するだろうからだ。だが、reader関手のfmapは左関数合成にすぎないことを思い出してほしい。そのため、関数合成を直接使うことにする。

まず、データ構成子Stateを切り離すことでState関手内の関数を公開する必要がある。これはrunStateを使って行われる。

ssa :: State s (State s a)
runState ssa :: s -> (State s a, s)

次に、uncurry runStateによって定義される余単位に左合成する。最後に、Stateデータ構成子に戻す:

join :: State s (State s a) -> State s a
join ssa = State (uncurry runState . runState ssa)

これはまさにStateモナドのためのjoinの実装だ。

あらゆる随伴からモナドが生じるだけでなく、逆もまた真だと分かる:あらゆるモナドは随伴関手2つの合成に分解できる。ただし、そのような分解は一意ではない。

もう1つの自己関手LRL \circ Rについては次章で述べる。

23 コモナド

モナドが使えるようになったので、双対性の恩恵によって、射を逆にして反対圏で作業するだけでコモナドが無料で手に入る。

最も基本的なレベルでは、モナドはクライスリ射の合成に関するのを思い出してほしい:

a -> m b

ここでmはモナドである関手だ。コモナドを文字w(逆さのm)で表すと、余クライスリ射 (co-Kleisli arrow) を次の型の射として定義できる:

w a -> b

余クライスリ射についてfish演算子に相当するものは次のように定義される:

(=>=) :: (w a -> b) -> (w b -> c) -> (w a -> c)

余クライスリ射が圏を形成するにはextractと呼ばれる恒等余クライスリ射も必要だ:

extract :: w a -> a

これはreturnの双対だ。さらに、左恒等射と右恒等射だけでなく結合律も必須だ。すべてをまとめると、Haskellではコモナドを次のように定義できる:

class Functor w => Comonad w where
    (=>=) :: (w a -> b) -> (w b -> c) -> (w a -> c)
    extract :: w a -> a

実用上は、後で説明するように、わずかに異なるプリミティブが使われる。

問題は、コモナドがプログラミングで何の役に立つかだ。

23.1 コモナドでのプログラミング

モナドとコモナドを比べてみよう。モナドはコンテナーに値を入れる手段をreturnによって提供している。そして、内部に格納された値にはアクセスできない。もちろんモナドを実装するデータ構造は内容へのアクセスを提供してもよいが、それはおまけと見なされる。モナドから値を抽出するための一般的なインターフェースはない。例として見たIOモナドは内容を決して公開しないことを誇りとしている。

一方、コモナドは単一の値を取り出す手段を提供している。そして、値を挿入する手段は提供しない。コモナドをコンテナーと見なしたいなら、その中身はいつも事前に満たされていて、覗き見られるようになっている。

クライスリ射は値を受け取り、文脈で装飾した結果を生成する。同様に、余クライスリ射は文脈全体と共に値を受け取り、結果を生成する。これは文脈付き計算 (contextual computation) の実施例だ。

23.2 積コモナド

Readerモナドを覚えているだろうか? それを導入した理由は、何らかの読み取り専用の環境eへのアクセスを必要とする計算を実装する問題に取り組むためだった。そのような計算は純粋関数として次の形で表せる:

(a, e) -> b

それらをクライスリ射に変えるのにはカリー化を使った:

a -> (e -> b)

しかし、これらの関数がすでに余クライスリ射の形をしていることに注目してほしい。引数をもっと便利な関手の形式に書き換えてみよう:

data Product e a = Prod e a deriving Functor

合成演算子は簡単に定義できる。合成しようとしている矢でも同じ環境を使えるようにすればよい:

(=>=) :: (Product e a -> b) -> (Product e b -> c) -> (Product e a -> c)
f =>= g = \(Prod e a) -> let b = f (Prod e a)
                          c = g (Prod e b)
                      in c

extractの実装は環境を単に無視する:

extract (Prod e a) = a

驚くまでもなく、積コモナドではreaderモナドと全く同じ計算が実行できる。ある意味で、環境をコモナドで実装するのはより自然だ――「文脈に沿った計算」の精神に則っている。一方、モナドはdo記法という便利な構文糖を備えている。

readerモナドと積コモナドの関係はさらに深く、reader関手が積関手の右随伴であるという事実に関連している。しかし、一般的には、コモナドはモナドとは異なる計算の概念を扱う。さらなる例について後で説明する。

積コモナドProductは、組やレコードを含む任意の直積型へ簡単に一般化できる。

23.3 合成の分析

双対化のプロセスを続けることで、モナディックなbindとjoinを双対化できた。別の方法として、モナドで用いたプロセスを繰り返すこともできる。そこではfish演算子の解剖学を研究した。そのアプローチの方が啓発的に思える。

出発点は、合成演算子はw aをとりcを生成する余クライスリ射を生成しなければならない、という認識だ。cを生成する唯一の方法は、第2の関数を型w bの引数に適用することだ:

(=>=) :: (w a -> b) -> (w b -> c) -> (w a -> c)
f =>= g = g ...

だが、gに与えうる型w bの値はどうすれば生成できるだろうか? 型w aの引数と関数f :: w a -> bは自由に使える。答えはbindの双対を定義することだ。それはextendと呼ばれる:

extend :: (w a -> b) -> w a -> w b

extendを使えば合成を実装できる:

f =>= g = g . extend f

次はextendを解剖できるだろうか? なぜ単に関数w a -> bを引数w aに適用しないのか、と言いたくなるかもしれない。しかし、結果として得られるbw bに変換する方法がないことにすぐ気付くだろう。コモナドには値をリフトする手段がないことを思い出してほしい。ここではモナドに類似した構成でfmapを使った。ここでfmapが使えるとすれば、型w (w a)の何かが自由に使える場合だけだ。w aw (w a)に変えられさえすればよい。そして、便利なことに、それはまさにjoinの双対だ。それはduplicateと呼ばれる:

duplicate :: w a -> w (w a)

したがって、モナドと同様に、コモナドにも3つの等価な定義がある。すなわち、余クライスリ射の使用、extendduplicateだ。HaskellのControl.Comonadライブラリから直接引用した定義を以下に示す:

class Functor w => Comonad w where
  extract :: w a -> a
  duplicate :: w a -> w (w a)
  duplicate = extend id
  extend :: (w a -> b) -> w a -> w b
  extend f = fmap f . duplicate

extendのデフォルト実装がduplicateによって提供され、逆も同様だ。したがって、どちらか1つをオーバーライドするだけでよい。

これらの関数の背景にある直観は、一般にコモナドは型aの値で満たされたコンテナーと見なせるという考えに基づいている(積コモナドは値が1つしかない特殊なケースだ)。「現在の」値という概念があり、それはextractによって簡単にアクセスできる。余クライスリ射は現在の値に焦点を合わせて計算を実行するものの、周囲のすべての値にアクセスできる。コンウェイのライフゲームを考えてみてほしい。各セルには値が含まれる(通常はTrueFalseだ)。ライフゲームに対応するコモナドは「現在の」セルに焦点を合わせたセルのグリッドだ。

ではduplicateは何をするのだろう? それはコモナディックなコンテナーw aを取ってコンテナーのコンテナーw (w a)を生成する。つまり、それぞれのコンテナーがw a内部の異なるaに焦点を合わせているという考え方だ。ライフゲームでは、グリッドのグリッドが得られる。外側のグリッドの各セルには、個別のセルに焦点を合わせた内側のグリッドが含まれている。

次にextendを見てみよう。それは、余クライスリ射と、複数のaで満たされたコモナディックなコンテナーw aを取る。そして、すべてのaに計算を適用してbに置き換える。結果は複数のbで満たされたコモナディックなコンテナーとなる。これを実現するためにextendは焦点を合わせるaを次々にシフトしていき、それぞれに余クライスリ射を適用する。ライフゲームでは、余クライスリ射は現在のセルの新しい状態を計算する。そのために文脈(すなわち最近傍セル)に注目する。extendのデフォルト実装はこの過程を示している。まずduplicateを呼び出してすべての可能な焦点を生成し、次にそのそれぞれにfを適用する。

23.4 Streamコモナド

コンテナー内で焦点を合わせる要素を次々にシフトしていくこのプロセスを最もうまく説明できる例は無限ストリームだ。そのようなストリームはリストにそっくりだが、空のコンストラクターがない点が異なる:

data Stream a = Cons a (Stream a)

これは次のFunctorにすぎない:

instance Functor Stream where
    fmap f (Cons a as) = Cons (f a) (fmap f as)

ストリームの焦点はその最初の要素なので、extractの実装は次のようになる:

extract (Cons a _) = a

duplicateは、それぞれ異なる要素に焦点を合わせた複数のストリームからなるストリームを1つ生成する:

duplicate (Cons a as) = Cons (Cons a as) (duplicate as)

最初の要素はもとのストリーム、2番目の要素はもとのストリームのtail、3番目の要素はさらにそのtail、というように無限に続く。

完全なインスタンスは次のとおりだ:

instance Comonad Stream where
    extract (Cons a _) = a
    duplicate (Cons a as) = Cons (Cons a as) (duplicate as)

これは非常に関数的な観点でストリームを捉えている。おそらく命令型言語ではストリームを1要素だけシフトするadvanceメソッドから始めるだろう。しかし、ここでのduplicateはシフトされたすべてのストリームを一気に生成する。Haskellの遅延評価がこれを可能にし、望ましいものにさえしている。当然、Streamを実用的なものにするために、advanceに類似したものも実装する:

tail :: Stream a -> Stream a
tail (Cons a as) = as

ただし、これは決してコモナディックなインターフェイスの一部ではない。

デジタル信号処理の経験があればすぐ分かるとおり、ストリームに対する余クライスリ射は単なるデジタルフィルタであり、extendはフィルターされたストリームを生成する。

簡単な例として、移動平均フィルターを実装してみよう。ストリームのn個の要素を合計する関数はこうなる:

sumS :: Num a => Int -> Stream a -> a
sumS n (Cons a as) = if n <= 0 then 0 else a + sumS (n - 1) as

また、ストリームの最初のn要素の平均を計算する関数はこうなる:

average :: Fractional a => Int -> Stream a -> a
average n stm = (sumS n stm) / (fromIntegral n)

部分適用されたaverage nは余クライスリ射なので、ストリーム全体にわたってextendできる:

movingAvg :: Fractional a => Int -> Stream a -> Stream a
movingAvg n = extend (average n)

結果は移動平均のストリームとなる。

ストリームは単方向1次元コモナドの例だ。これは簡単に双方向にしたり多次元に拡張したりできる。

23.5 圏論から見たコモナド

圏論でコモナドを定義することは双対性のストレートな練習になる。モナドと同様に、自己関手Tから始めよう。モナドを定義する2つの自然変換η\etaμ\muは、コモナドでは単に反転される: εTIδTT2 \begin{aligned} \varepsilon &\Colon T \to I \\ \delta &\Colon T \to T^2 \end{aligned} これらの変換の成分は、extractおよびduplicateに対応する。コモナド則は鏡に映ったモナド則だ。驚くまでもない。

そして、モナドは随伴から導出できる。双対性は随伴を反転させる。つまり、左随伴と右随伴が入れ替わる。また、合成RLR \circ Lがモナドを定義するので、LRL \circ Rはコモナドを定義するに違いない。随伴の余単位は次のようになる: εLRI\varepsilon \Colon L \circ R \to I このε\varepsilonは、コモナドの定義や、Haskellのextractのような成分で見られるものと同じだ。随伴の単位: ηIRL\eta \Colon I \to R \circ L も、LRL \circ Rの途中にRLR \circ Lを挿入してLRLRL \circ R \circ L \circ Rを作成するのに使える。TTからT2T^2を作るとδ\deltaが定義され、コモナドの定義が完成する。

モナドがモノイドであることもすでに見た。この宣言の双対にはコモナドが必要だが、ではコモノイドとは何だろう? モノイドとは単一対象圏である、という当初の定義を双対化しても興味深いものは何も得られない。すべての自己準同型を逆向きにすると別のモノイドが得られる。しかし、思い出してほしい。モナドへのアプローチで用いた定義は、モノイドとはモノイダル圏の対象であるという、より一般化されたものだ。その構成は2つの射に基づいていた: μmmmηim \begin{aligned} \mu &\Colon m \otimes m \to m \\ \eta &\Colon i \to m \end{aligned} これらの射を逆にするとモノイダル圏でのコモノイドができる: δmmmεmi \begin{aligned} \delta &\Colon m \to m \otimes m \\ \varepsilon &\Colon m \to i \end{aligned} コモノイドの定義はHaskellで書くと:

class Comonoid m where
  split   :: m -> (m, m)
  destroy :: m -> ()

となり、ほぼ自明だ。明らかにdestroyは引数を無視する:

destroy _ = ()

また、splitは単に関数のペアだ:

split x = (f x, g x)

ここで、モノイドの単位律と双対関係にあるコモノイド則について考えてみよう:

lambda . bimap destroy id . split = id
rho . bimap id destroy . split = id

ここで、lambdaは左単位子でrhoは右単位子だ (モノイダル圏の定義を参照)。これらの定義を代入すると:

lambda (bimap destroy id (split x))
= lambda (bimap destroy id (f x, g x))
= lambda ((), g x)
= g x

となり、g = idが示される。同様に、2番目の規則はf = idへと展開される。結論として:

split x = (x, x)

となり、Haskell(および一般に𝐒𝐞𝐭\mathbf{Set}圏)では、すべての対象が自明にコモナドだと示される。

幸い、モノイダル圏には、コモノイドを定義するともっと興味深いものが他にもある。その1つは自己関手の圏だ。そこで分かることとして、モナドが自己関手からなる圏におけるモノイドであるのと同様に:

コモナドは自己関手からなる圏におけるコモノイドである。

23.6 Storeコモナド

コモナドのもう1つの重要な例はstateモナドの双対だ。それはcostateコモナド、あるいはstoreコモナドと呼ばれる。

以前、冪乗を定義する随伴によってstateモナドが生成されるのを見た: Lz=z×sRa=sa \begin{aligned} L z &= z\times{}s \\ R a &= s \Rightarrow a \end{aligned} 同じ随伴を使って、costateコモナドを定義しよう。コモナドは合成LRL \circ Rによって定義される: L(Ra)=(sa)×sL (R a) = (s \Rightarrow a)\times{}s これをHaskellに変換するには、左Product関手と右Reader関手との間の随伴から始める。ProductReaderの後に合成するのは次の定義と等価だ:

data Store s a = Store (s -> a) s

対象aにおけるこの随伴の余単位は次の射だ: εa((sa)×s)a\varepsilon_a \Colon ((s \Rightarrow a)\times{}s) \to a あるいは、Haskellの記法だと次のようになる:

counit (Prod (Reader f, s)) = f s

extractはこうなる:

extract (Store f s) = f s

随伴の単位:

unit :: a -> Reader s (Product a, s)
unit a = Reader (\s -> Prod (a, s))

は、部分適用されたデータ構成子として書き直せる:

Store f :: s -> Store f s

δ\deltaすなわちduplicateを水平合成として構成しよう: δLRLRLRδ=LηR \begin{aligned} \delta &\Colon L \circ R \to L \circ R \circ L \circ R \\ \delta &= L \circ \eta \circ R \end{aligned} 一番左のLL越しにη\etaをくすねる必要がある。このLLProduct関手だ。これは、ペアの左側の要素にη\etaすなわちStore fを作用させることを意味する(これがProductfmapが行うことだ)。すると、次の結果が得られる:

duplicate (Store f s) = Store (Store f) s

δ\deltaの式でのLLRRは恒等自然変換を表し、成分が恒等射なのを思い出してほしい)。

Storeコモナドの完全な定義を以下に示す:

instance Comonad (Store s) where
  extract (Store f s) = f s
  duplicate (Store f s) = Store (Store f) s

StoreReaderの部分は、一般化されたコンテナーに型sの要素をキーにしてaが格納されていると見なせる。たとえば、sIntなら、Reader Int aaの双方向無限ストリームだ。Storeはこのコンテナーとキー型の値とをペアにする。また、たとえば、Reader Int aはあるIntとペアになっている。この場合、extractはその整数を使って無限ストリームにインデックスを作成する。Storeの2番目の要素は現在位置と見なせる。

この例についてさらに述べると、duplicateIntによって添字付けされた新しい無限ストリームを作成する。このストリームは要素としてストリームを含む。特に、現在位置にはもとのストリームが含まれている。しかし、他の(正または負の)Intをキーとして使うと、新しいインデックスに位置するシフトされたストリームが得られる。

一般に、duplicateされたStoreextractが作用すると、もとのStoreが生成されることを確信できる(実際、コモナドの恒等律ではextract . duplicate = idと規定されている)。

StoreコモナドはLensライブラリの理論的基盤として重要だ。概念としては、Store s aコモナドは、型sをインデックスとしてデータ型aの特定のサブ構造に(レンズのように)「焦点を合わせる」という考え方をカプセル化している。特に、ある関数が型:

a -> Store s a

を取るなら、以下の関数のペアと等価だ:

set :: a -> s -> a
get :: a -> s

aが直積型である場合のsetは、a内の型sのフィールドをセットする一方で、変更版のaを返すように実装できる。同様に、getsフィールドの値をaから読み取るように実装できる。これらの考え方については次章で詳しく説明する。

23.7 課題

  1. Storeコモナドを使ってコンウェイのライフゲームを実装せよ。ヒント:sの型は何がよいだろうか?

24 F-代数

モノイドについて、集合や、単一対象圏や、モノイダル圏の対象としての定式化を見てきた。この単純な概念からどれだけの果汁をさらに搾り取れるだろうか?

やってみよう。次の関数のペアを持つ集合mmとしてのモノイドの定義を取り上げる: μm×mmη1m \begin{aligned} \mu &\Colon m\times{}m \to m \\ \eta &\Colon 1 \to m \end{aligned} ここで、1は𝐒𝐞𝐭\mathbf{Set}の終対象――単元集合だ。最初の関数は乗算(要素のペアを取って積を返す)を定義し、2番目の関数はmmから単位元を選択する。これらのシグネチャーを持つ2つの関数ならどれでもモノイドになるわけではない。結合律と単位律という追加の条件を課す必要がある。しかし、それはしばらく忘れて「潜在的なモノイド」について考えてみよう。関数のペアは、2つの関数集合のデカルト積の要素だ。それらの集合が冪対象として表現できるのは知っている: μmm×mηm1 \begin{aligned} \mu &\in m^{m\times{}m} \\ \eta &\in m^1 \end{aligned} これら2つの集合のデカルト積は次のとおりだ: mm×m×m1m^{m\times{}m}\times{}m^1 高校代数(すべてのデカルト閉圏で機能する)を少し使えば次のように書き直せる: mm×m+1m^{m\times{}m + 1} 記号 ++𝐒𝐞𝐭\mathbf{Set}の余積を表す。ここで、関数のペアを次のような単一の関数――集合の要素――に置き換える: m×m+1mm\times{}m + 1 \to m この関数集合のどの要素も潜在的なモノイドだ。

この定式化の利点は、興味深い一般化を導くことだ。たとえば、この言語を使って群 (group) を記述するにはどうすればよいだろうか? 群は、すべての要素に逆を対応させる関数が追加されたモノイドだ。後者の関数の型はmmm \to mだ。例を挙げると、整数は群を形成し、二項演算として加算、単位元として0、否定として正負反転を持つ。群を定義するには次の3つの関数から始める: m×mmmm1m \begin{aligned} m\times{}m \to m \\ m \to m \\ 1 \to m \end{aligned} 前と同様に、この3つ組を結合して1つの関数集合にできる: m×m+m+1mm\times{}m + m + 1 \to m 1つの二項演算子(加算)、1つの単項演算子(否定)、1つの零項演算子(恒等射――ここでは0)から始めた。そして、それらを1つの関数に結合した。このシグネチャーを持つすべての関数は、潜在的な群を定義する。

同様のことを続けられる。たとえば、環を定義するにはもう1つの二項演算子と1つの零項演算子を追加する、などだ。毎回、左辺が冪(0乗――終対象――を含んでよい)の和で、右辺が集合自体である関数型を得られる。

きっと一般化に夢中になってしまうだろう。まず、集合を対象に、関数を射に置き換えられる。n項演算子は、n項積からの射として定義できる。これは有限積 (finite product) をサポートする圏が必要であることを意味する。零項演算子については、終対象が存在する必要がある。したがって、デカルト圏が必要だ。これらの演算子を結合するには冪が必要なので、必要なのはデカルト閉圏だ。最終的に、この代数的な悪ふざけを完成させるには、余積が必要になる。

あるいは、式の導出方法を忘れて、最終的な積に集中してもよい。射の左側にある積の和は自己関手を定義している。代わりに任意の自己関手FFを選択するとどうなるだろう? その場合は圏に制約を課す必要はない。こうして得られたものはF-代数と呼ばれる。

F-代数は、1つの自己関手FF、1つの対象aaと、次の1つの射からなる3つ組だ: FaaF a \to a ここでの対象はしばしば台 (carrier, underlying object) と呼ばれ、プログラミングの文脈ではキャリアと呼ばれる。また、射は評価射 (evaluation function, structure map) と呼ばれることが多い。関手Fが式を形成し、それを射が評価すると考えてほしい。

HaskellによるF-代数の定義を示す:

type Algebra f a = f a -> a

代数はその評価関数で識別される。

モノイドの例では、問題の関手は次のようになる:

data MonF a = MEmpty | MAppend a a

これはHaskellで1+a×a1 + a \times{}aを表したものだ(代数的データ構造を思い出してほしい)。

環は次の関手を使って定義される:

data RingF a = RZero
        | ROne
        | RAdd a a
        | RMul a a
        | RNeg a

これはHaskellで1+1+a×a+a×a+a1 + 1 + a \times{}a + a \times{}a + aを表したものだ。

環の例としては整数の集合が挙げられる。Integerをキャリア型に選ぶと、評価関数を次のように定義できる:

evalZ :: Algebra RingF Integer
evalZ RZero      = 0
evalZ ROne       = 1
evalZ (RAdd m n) = m + n
evalZ (RMul m n) = m * n
evalZ (RNeg n)   = -n

同じ関手RingFに基づくF-代数は他にもたくさんある。たとえば、多項式も正方行列も環を形成する。

ご覧のとおり、関手の役割は、評価に代数の評価子を使えるような式を生成することだ。ここまでは、非常に単純な式しか見てこなかった。しかし、より複雑な、再帰を使って定義できる式に関心があることも多い。

24.1 再帰

任意の式木を生成する方法の1つは、関手の定義内の変数aを再帰で置き換えることだ。たとえば、環における任意の式は、木に似たデータ構造によって生成される:

data Expr = RZero
          | ROne
          | RAdd Expr Expr
          | RMul Expr Expr
          | RNeg Expr

もとの環の評価子は再帰版に置き換えられる:

evalZ :: Expr -> Integer
evalZ RZero        = 0
evalZ ROne         = 1
evalZ (RAdd e1 e2) = evalZ e1 + evalZ e2
evalZ (RMul e1 e2) = evalZ e1 * evalZ e2
evalZ (RNeg e)     = -(evalZ e)

これはまだあまり実用的ではない。すべての整数を1の和で表現するのが強制されているからだ。もっとも、応急用には役立つ。

それにしても、F-代数の言葉で式木を記述するにはどうすればよいだろうか? 関手の定義において、自由型変数を再帰的に置換の結果で置き換えるプロセスを、何らかの形で形式化する必要がある。これを段階的に行うことを想像してほしい。まず、深さ1の木を次のように定義する:

type RingF1 a = RingF (RingF a)

RingFの定義の穴を、RingF aによって生成された深さ0の木で埋めている。深さ2の木も同様にして得られる:

type RingF2 a = RingF (RingF (RingF a))

これは次のようにも書ける:

type RingF2 a = RingF (RingF1 a)

このプロセスを繰り返すことで、シンボリックな等式を書ける:

type RingFn+1 a = RingF (RingFn a)

概念としては、このプロセスを無限に何度も繰り返すことでExprが得られる。Expraに依存しないことに注意してほしい。旅の出発点によらず、いつも同じ場所に辿り着く。これは任意の圏の任意の自己関手に常に当てはまるわけではないが、𝐒𝐞𝐭\mathbf{Set}圏では適切だ。

当然、これは身振り手振りでの議論なので、後でより厳密に説明する。

自己関手を無限回適用すると、不動点 (fixed point) という対象が生成される。この対象は次のように定義される: 𝐹𝑖𝑥f=f(𝐹𝑖𝑥f)\mathit{Fix}\ f = f\ (\mathit{Fix}\ f) この定義の背景には、𝐹𝑖𝑥f\mathit{Fix}\ fを得るためにffを無限回適用しているので、さらにもう1回適用しても何も変わらない、という直観がある。Haskellでは、不動点の定義は次のようになる:

newtype Fix f = Fix (f (Fix f))

おそらく、次のように、定義されている型の名前がコンストラクターの名前と別ならば、もっと読みやすくなる:

newtype Fix f = In (f (Fix f))

しかし、ここでは広く使われている表記に従うことにする。コンストラクターFix(あるいは好みによってはIn)は関数と見なせる:

Fix :: f (Fix f) -> Fix f

また、関手適用を1層だけ剥がす関数もある:

unFix :: Fix f -> f (Fix f)
unFix (Fix x) = x

2つの関数は互いに逆だ。これらの関数は後で使うことになる。

24.2 F-代数の圏

この本の中で最も古いトリックについて述べよう:何か新しい対象を構築する方法を思いついたら、それらが圏を形成するかどうかを常に確認すべし。驚くまでもなく、任意の自己関手FFにおける代数は圏を形成する。その圏の対象は代数だ――台対象aaと射FaaF a \to aからなるペアで、どちらももとの圏𝐂\mathbf{C}からのものだ。

この描像を完全なものにするには、F-代数の圏における射を定義する必要がある。射は、ある代数 (a,f)(a, f) を別の代数 (b,g)(b, g) に写さなければならない。これを、台を写す射mm――もとの圏でaaからbbへ向かう射――と定義する。どの射でもよいわけではなく、2つの評価子と互換性がなければならない。(構造を保存するそのような射を準同型と呼ぶ。) F-代数の準同型を定義する方法は次のとおりだ。まず、mmを次の写像にリフトできることに注目してほしい: FmFaFbF m \Colon F a \to F b それからggを辿るとbbに行き着く。同様に、ffを使ってFaF aからaaに移ってからmmを辿ってもよい。2つの経路が等しくなるようにしたい: gFm=mfg \circ F m = m \circ f alg

これが本当に圏であることは簡単に納得できる(ヒント:𝐂\mathbf{C}の恒等射については問題なく、また、準同型の合成は準同型だ)。

F-代数の圏に始対象が存在するなら、それは始代数 (initial algebra) と呼ばれる。この始代数の台をiiと呼び、その評価子をjFiij \Colon F i \to iと呼ぼう。始代数の評価子であるjjは同型射だと分かる。この結果はLambekの定理として知られている。その証明は始対象の定義に依存している。その始対象の定義より、他のF-代数への一意な準同型mmの存在が必要とされる。mmが準同型なので、次の図式が可換でなければならない:

alg2

次に、台がFiF iである代数を構築しよう。そのような代数の評価子はF(Fi)F (F i) からFiF iへの射でなければならない。このような評価子はjjをリフトするだけで簡単に構成できる。 FjF(Fi)FiF j \Colon F (F i) \to F i (i,j)(i, j) は始代数なので、そこから (Fi,Fj)(F i, F j) への一意な準同型mmが必要だ。次の図式が可換でなければならない:

alg3a

しかし、次のような自明な可換図式もある(どちらの経路も同じだ!):

alg3

これは、jjが代数の準同型であり、(Fi,Fj)(F i, F j)(i,j)(i, j) に写すことを示していると解釈できる。これら2つの図式を繋ぐと次のようになる:

alg4

この図式は、同様に、jmj \circ mが代数の準同型であることを示していると解釈できる。この場合のみ2つの代数は同じだ。さらに、(i,j)(i, j) は始代数なので、それ自身から自身への準同型は1つしか存在できず、それはどの代数でも準同型だと分かっている恒等射𝐢𝐝i\mathbf{id}_iだ。したがって、jm=𝐢𝐝ij \circ m = \mathbf{id}_iとなる。この事実と左側の図式の可換特性を用いてmj=𝐢𝐝Fim \circ j=\mathbf{id}_{Fi}であることを証明できる。これはmmjjの逆であることを示しているため、jjFiF iiiの間の同型射だ: FiiF i \cong i しかし、これはiiFFの不動点だと言っているにすぎない。以上が最初の身振り手振りでの議論の背景にある形式的証明だ。

Haskellに戻ろう。iiFix fで、jjはコンストラクターFix、その逆はunFixだと理解できる。Lambekの定理における同型からは、始代数を得るには関手ffを取って引数aaFix fに置き換えればよいことが分かる。また、不動点がaaによらない理由も分かる。

24.3 自然数

自然数もF-代数として定義できる。出発点は次のような射のペアだ: zero1NsuccNN \begin{aligned} zero &\Colon 1 \to N \\ succ &\Colon N \to N \end{aligned} 1つ目は0を選択し、2つ目はすべての数をその次の数に写す。前と同じように、この2つは1つにまとめられる: 1+NN1 + N \to N 左辺は関手を定義し、Haskellでは次のように書ける:

data NatF a = ZeroF | SuccF a

この関手の不動点(この関手が生成する始代数)は、Haskellでは次のようにコード化できる:

data Nat = Zero | Succ Nat

自然数は、0か、ある数の次の数かのどちらかだ。これは自然数のPeano表現として知られている。

24.4 カタモルフィズム

始代数の条件をHaskellの表記で書き直してみよう。始代数はFix fと呼ばれる。その評価子はコンストラクターFixだ。始代数から他の任意の代数への一意な射mが同じ関手上に存在する。台がaであり評価子がalgである代数を選択しよう。

alg5

ところで、mが何なのかに注目してほしい。これは不動点の評価子であり、再帰的な式木全体の評価子だ。これを実装するための汎用的な方法を探そう。

Lambekの定理はコンストラクターFixが同型射であることを示している。その逆はunFixと呼ばれる。したがって、この図式で矢印の1つを反転して次のようにできる:

alg6

この図式の可換条件を書こう:

m = alg . fmap m . unFix

この等式はmの再帰的な定義として解釈できる。再帰は関手fを使って作成されたすべての有限木に対して停止する。そのことは、fmap mが関手fの最上層の下で動作することに着目すれば分かる。言い換えると、それはもとの木の子に対して機能する。子は常にもとの木より1レベル浅くなる。

Fix fを使って構築された木にmを適用するとどうなるか述べよう。まず、unFixの作用によってコンストラクターが剥がれ、木の最上位があらわになる。次に、最上位ノードのすべての子にmを適用する。それにより、型aの結果が生成される。最後に、非再帰的評価子algを適用することで、これらの結果を結合する。重要な点は、評価子algが単純な非再帰関数であることだ。

これは任意の代数algに対して行えるので、代数をパラメーターに取ってmと呼ばれる関数を返す高階関数を定義するのは意味がある。この高階関数はカタモルフィズム (catamorphism) と呼ばれる:

cata :: Functor f => (f a -> a) -> Fix f -> a
cata alg = alg . fmap (cata alg) . unFix

例を見てみよう。自然数を定義する関手を考える:

data NatF a = ZeroF | SuccF a

台の型として(Int, Int)を選択し、代数を次のように定義する:

fib :: NatF (Int, Int) -> (Int, Int)
fib ZeroF = (1, 1)
fib (SuccF (m, n)) = (n, m + n)

この代数のカタモルフィズムcata fibでフィボナッチ数が計算されることは簡単に納得できる。

一般に、NatFの代数は漸化式を定義する。つまり、現在の要素の値を前の要素によって表す。そして、カタモルフィズムは一連の要素のうちn番目のものを評価する。

24.5 畳み込み

eのリストは次の関手の始代数だ:

data ListF e a = NilF | ConsF e a

実際、List eと呼ばれる再帰の結果で変数aを置き換えると、次のようになる:

data List e = Nil | Cons e (List e)

リスト関手の代数は、台の特定の型を選択し、2つのコンストラクターについてパターンマッチングを行う関数を定義する。NilFの値は空リストを評価する方法を表し、ConsFの値は現在の要素をそれ以前の累積値と組み合わせる方法を表す。

例として、リストの長さを計算するために使える代数を次に示す(台の型はIntだ):

lenAlg :: ListF e Int -> Int
lenAlg (ConsF e n) = n + 1
lenAlg NilF = 0

実際、結果として得られるカタモルフィズムcata lenAlgによってリストの長さが計算される。評価関数は、(1) リスト要素とアキュムレーターを受け取って新しいアキュムレーターを返す関数と、(2) 開始値(ここでは0)とを組み合わせたものであることに注目してほしい。値の型とアキュムレーターの型は台の型によって与えられる。

これを従来のHaskellの定義と比較してみよう:

length = foldr (\e n -> n + 1) 0

foldrの2つの引数は代数の2つの成分そのものだ。

別の例を見てみよう:

sumAlg :: ListF Double Double -> Double
sumAlg (ConsF e s) = e + s
sumAlg NilF = 0.0

再び、これを次のものと比較する:

sum = foldr (\e s -> e + s) 0.0

ご覧のとおり、foldrはリストに対してカタモルフィズムを特殊化して便利にしたものにすぎない。

24.6 余代数

いつものように、F-余代数という双対構成があり、射が逆向きになっている: aFaa \to F a 任意の関手についての余代数も、その余代数的構造を保存する準同型を伴った圏を形成する。その圏の終対象 (t,u)(t, u) は終余代数 (terminal / final coalgebra) と呼ばれる。他のすべての代数 (a,f)(a, f) では、次の図式を可換にする一意な準同型mが存在する:

alg7

終余代数はその関手の不動点であり、射utFtu \Colon t \to F tが同型射である(余代数に関するLambekの定理)ことを意味する: FttF t \cong t 終余代数は、プログラミングでは通常、(無限でもよい)データ構造または遷移系を生成するためのレシピとして解釈される。

始代数の評価にカタモルフィズムが使えるのと同様に、終余代数の余評価にはアナモルフィズム (anamorphism) が使える:

ana :: Functor f => (a -> f a) -> a -> Fix f
ana coalg = Fix . fmap (ana coalg) . coalg

余代数の正統な例は、不動点が型eの要素の無限ストリームであるような関手に基づいている。これがその関手だ:

data StreamF e a = StreamF e a
  deriving Functor

そして、これがその不動点だ:

data Stream e = Stream e (Stream e)

StreamF eの余代数は、型aのシードを取り、ある要素と次のシードからなるペアを生成する関数だ(ペアをStreamFというファンシーな名前で呼んでいる)。

無限数列を生成する余代数の簡単な例はすぐ生成できる。たとえば、2乗や逆数などのリストだ。

もっと興味深い例としては、素数のリストを生成する余代数がある。無限リストを台として使うのが秘訣だ。最初のシードはリスト[2..]になる。次のシードはこのリストのtailから2の倍数をすべて除いたものになる。これは奇数のリストであり、3から始まる。次の段階では、このリストのtailを取って3の倍数をすべて除く。エラトステネスの篩を作っているのに気付いただろう。この余代数は次の関数で実装される:

era :: [Int] -> StreamF Int [Int]
era (p : ns) = StreamF p (filter (notdiv p) ns)
    where notdiv p n = n `mod` p /= 0

この余代数のアナモルフィズムは素数のリストを生成する:

primes = ana era [2..]

ストリームは無限リストなので、Haskellのリストに変換できるはずだ。そのためには、同じ関手StreamFを使って代数を形成したうえでカタモルフィズムを実行すればよい。たとえば、次のカタモルフィズムはストリームをリストに変換する:

toListC :: Fix (StreamF e) -> [e]
toListC = cata al
   where al :: StreamF e [e] -> [e]
         al (StreamF e a) = e : a

ここでは、同じ不動点が同じ自己関手の始代数でもあり終余代数でもある。これは任意の圏で常に成り立つわけではない。一般に、自己関手には不動点が多数あり得る(不動点がないこともある)。始代数はいわば最小不動点で、終余代数は最大不動点だ。ただし、Haskellでは両方とも同じ式で定義され、一致する。

リストのアナモルフィズムはunfoldと呼ばれる。有限リストを作成するには、ペアについてのMaybeを生成するように関手を改変する:

unfoldr :: (b -> Maybe (a, b)) -> b -> [a]

Nothingの値でリストの生成が終了する。

余代数の面白い例として、レンズに関するものが挙げられる。レンズはゲッターとセッターのペアとして表せる:

set :: a -> s -> a
get :: a -> s

ここで、通常、aは型sのフィールドを持つ直積データ型だ。ゲッターはそのフィールドの値を取得し、セッターはそのフィールドを新しい値に置き換える。これら2つの関数は1つにまとめられる:

a -> (s, s -> a)

この関数はさらに次のように書き直せる:

a -> Store s a

ここで次のような関手を定義した:

data Store s a = Store (s -> a) s

これは積の和から構成される単純な代数的関手ではないことに注意してほしい。これには冪asa^sが含まれる。

レンズはこの関手の余代数であり、台の型はaだ。Store sがコモナドでもあることは以前にも見た。行儀の良い (well-behaved) レンズは、コモナドの構造と適合する余代数に対応することが分かる。これについては次章で説明する。

24.7 課題

  1. 1変数多項式の環について評価関数を実装せよ。多項式はxxの冪の係数のリストとして表せる。たとえば、4x214x^2-1は、(0乗項から始めて)[-1, 0, 4]と表される。
  2. 前の構成を、x2y3y3zx^2y-3y^3zのような、独立変数を多数含む多項式に一般化せよ。
  3. 2×22\times{}2行列の環について代数を実装せよ。
  4. 自然数の2乗のリストを生成するようなアナモルフィズムを持つ余代数を定義せよ。
  5. unfoldrを使って、最初のnn個の素数のリストを生成せよ。

25 モナドの代数

もし自己関手を式の定義方法だと解釈するなら、代数は式を評価し、モナドは式を生成し操作する方法だということになる。代数とモナドを組み合わせることで、多くの機能が得られるだけでなく、いくつかの興味深い質問にも答えられる。そのような疑問の1つはモナドと随伴の関係に関するものだ。これまで見てきたように、すべての随伴はモナド(およびコモナド)を定義する。問題は、すべてのモナド(コモナド)は随伴から導出できるのかだ。できる、というのが答えだ。任意のモナドを生成する随伴の族が存在する。そのような随伴を2つ紹介したい。

定義を確認しよう。モナドは、あるコヒーレンス条件を満たす2つの自然変換を伴う自己関手mmだ。それらの変換のaaにおける成分は次のとおりだ: ηaamaμam(ma)ma \begin{aligned} \eta_a &\Colon a \to m\ a \\ \mu_a &\Colon m\ (m\ a) \to m\ a \end{aligned} 同じ自己関手についての代数は、次の射を伴ったある特定の対象――台aa――の選択だ: 𝑎𝑙𝑔maa\mathit{alg} \Colon m\ a \to a まず注目すべきは、代数がηa\eta_aとは逆行していることだ。直観では、ηa\eta_aは型aaの値から自明な式を作る。第1のコヒーレンス条件は、代数をモナドと互換性のあるものにし、aaを台とするその代数でその式を評価するともとの値が返ることを保証する: 𝑎𝑙𝑔ηa=𝐢𝐝a\mathit{alg} \circ \eta_a = \mathbf{id}_a 第2の条件は、2重にネストされた式m(ma)m\ (m\ a) を評価する方法が2つある、という事実に基づく。まずμa\mu_aを適用して式をフラット化し、次に代数の評価子を使えばよい。あるいは、リフトされた評価子を適用して内側の式を評価してから、その結果に評価子を適用してもよい。これら2つの戦略を等価にしたい: 𝑎𝑙𝑔μa=𝑎𝑙𝑔m𝑎𝑙𝑔\mathit{alg} \circ \mu_a = \mathit{alg} \circ m\ \mathit{alg} ここで、射m algは関手mmを使って𝑎𝑙𝑔\mathit{alg}をリフトした結果だ。次の可換図式はこれら2つの条件を示している(後のことを想定してmmTTに置き換えた):

これらの条件はHaskellでも表現できる:

alg . return = id
alg . join = alg . fmap alg

簡単な例を見てみよう。リスト自己関手の代数は、ある型aと、aのリストからaを生成する関数とで構成される。この関数はfoldrを使って表せる。そのためには、要素の型とアキュムレーターの型を、両方とも同じaになるように選ぶ:

foldr :: (a -> a -> a) -> a -> [a] -> a

この特定の代数は、2つの引数を取る関数fと値zによって指定される。リスト関手はモナドでもあり、returnは単一の値を単要素リストに変換する。ここでの代数はfoldr f zで、returnの後に合成してxを取るとこうなる:

foldr f z [x] = x `f` z

ここで、fの作用を中置記法で表した。代数がモナドと互換性があるのは、すべてのxに対して次のコヒーレンス条件が満たされる場合だ:

x `f` z = x

fを二項演算子と見なすと、この条件はzが右単位元であることを示している。

第2のコヒーレンス条件は、リストのリストに対して作用する。joinの作用は個々のリストを連接する。その後で結果のリストを畳み込めばよい。一方で、まず個々のリストを畳み込み、次に結果のリストを畳み込んでもよい。ここでも、fを二項演算子と見なすと、この条件はその二項演算の結合性を示している。これらの条件は(a, f, z)がモノイドである場合には確実に満たされる。

25.1 T-代数

数学者はモナドをTTと呼ぶのを好むので、それらと互換性のある代数をT-代数と呼ぶ。圏𝐂\mathbf{C}における任意のモナドTTについてのT-代数は圏を形成する。この圏はEilenberg-Moore圏と呼ばれ、𝐂T\mathbf{C}^Tと表記されることが多い。この圏の射は代数の準同型だ。それらはF-代数について定義されているのを見た準同型と同じものだ。

T-代数は台対象と評価子からなるペア (a,f)(a, f) だ。𝐂T\mathbf{C}^Tから𝐂\mathbf{C}への自明な忘却関手UTU^Tが存在し、(a,f)(a, f)aaに写す。また、T-代数の準同型を、𝐂\mathbf{C}内の台対象間の射のうち対応するものに写す。随伴について議論したとき、忘却関手への左随伴は自由関手と呼ばれると述べたのを覚えているだろう。

UTU^Tへの左随伴はFTF^Tと呼ばれる。それは𝐂\mathbf{C}内の対象aa𝐂T\mathbf{C}^T内の自由代数に写す。この自由代数の台はTaT aだ。その評価子はT(Ta)T (T a) からTaT aへ戻る射だ。TTはモナドなので、モナディックなμa\mu_a(Haskellでのjoin)を評価子として使える。

さらに、これがT-代数であることも示す必要がある。そのためには、次の2つのコヒーレンス条件が満たされなければならない: 𝑎𝑙𝑔ηTa=𝐢𝐝Ta𝑎𝑙𝑔μa=𝑎𝑙𝑔T𝑎𝑙𝑔 \begin{aligned} \mathit{alg} &\circ \eta_{Ta} = \mathbf{id}_{Ta} \\ \mathit{alg} &\circ \mu_a = \mathit{alg} \circ T \mathit{alg} \end{aligned} しかし、μ\muを代数に代入すれば、これらはモナド則にすぎない。

覚えているだろうが、すべての随伴はモナドを定義する。FTF^TUTU^Tの間の随伴は、Eilenberg-Moore圏の構成で使われるモナドTTそのものを定義することが知られている。この構成はすべてのモナドに対して行えるので、すべてのモナドは随伴から生成できる、と結論できる。後ほど、同じモナドを生成する別の随伴が存在することを示す。

計画はこうだ:まず、FTF^Tが実際にUTU^Tの左随伴であることを示す。そのために、この随伴の単位と余単位を定義し、対応する三角恒等式が満たされているのを証明する。次に、この随伴によって生成されるモナドが、実際にもとのモナドであることを示す。

随伴の単位は次の自然変換だ: ηIUTFT\eta \Colon I \to U^T \circ F^T この変換のaaにおける成分を計算してみよう。恒等関手によってaaが得られる。自由関手は自由代数 (Ta,μa)(T a, \mu_a) を生成し、忘却関手はそれをTaT aに縮約する。以上により、aaからTaT aへの写像が得られる。単にモナドTTの単位をこの随伴の単位として使うことにする。

余単位を見てみよう: εFTUTI\varepsilon \Colon F^T \circ U^T \to I あるT-代数 (a,f)(a, f) における成分を計算してみよう。忘却関手はffを忘れており、自由関手はペア (Ta,μa)(T a, \mu_a) を生成する。したがって、(a,f)(a, f) における余単位ε\varepsilonを定義するには、Eilenberg-Moore圏内の正しい射、すなわち次のようなT-代数の準同型が必要だ: (Ta,μa)(a,f)(T a, \mu_a) \to (a, f) このような準同型は台TaT aaaに写す必要がある。忘却された評価子ffを復活させよう。今度はそれをT-代数の準同型として使う。実際、ffをT-代数にしたのと同じ可換図式は、それがT-代数の準同型だと示すものとも見なせる:

したがって、(a,f)(a, f)(T-代数の圏の対象)における余単位の自然変換の成分ε\varepsilonffと定義した。

随伴を完成させるには、単位と余単位が三角恒等式を満たすことも示す必要がある。それらの三角形はこう描ける:

最初のものはモナドTTの単位律より成り立つ。2つ目のものはT-代数の規則 (a,f)(a, f) だ。

以上より、これら2つの関手が随伴を形成することが示せた: FTUTF^T \dashv U^T すべての随伴はモナドを生成する。往復旅行: UTFTU^T \circ F^T は、対応するモナドを生成する𝐂\mathbf{C}内の自己関手だ。対象aaに対する作用を見てみよう。FTF^Tによって作られる自由代数は (Ta,μa)(T a, \mu_a) だ。忘却関手UTU^Tは評価子を削除する。したがって、確かに次のものが得られる: UTFT=TU^T \circ F^T = T 予想どおり、随伴の単位はモナドTTの単位だ。

随伴の余単位では次の式からモナディックな乗算が生成されるのを覚えているだろう: μ=RεL\mu = R \circ \varepsilon \circ L これは3つの自然変換の水平合成であり、そのうち2つは恒等自然変換で、LLLLに写すものとRRRRに写すものだ。真ん中のものは余単位で、ある代数 (a,f)(a, f) における成分がffであるような自然変換だ。

成分μa\mu_aを計算してみよう。まず、ε\varepsilonFTF^Tの後に水平合成すると、FTaF^T aにおけるε\varepsilonの成分が得られる。FTF^Taaを代数 (Ta,μa)(T a, \mu_a) に渡し、ε\varepsilonは評価子を選択するので、結果はμa\mu_aになる。左辺のUTU^Tによる水平合成は何も変更しない。なぜなら、射に対するUTU^Tの作用は自明だからだ。したがって、実際に、随伴から得られるμ\muはもとのモナドTTμ\muと同じだ。

25.2 クライスリ圏

クライスリ圏についてはすでに見た。別の圏𝐂\mathbf{C}とモナドTTで構成された圏だ。これを圏𝐂T\mathbf{C}_Tと呼ぼう。クライスリ圏𝐂T\mathbf{C}_Tの対象は𝐂\mathbf{C}の対象だが、射については違う。クライスリ圏でのaaからbbへの射f𝐊f_{\mathbf{K}}は、もとの圏でのaaからTbT bへの射ffに対応する。この射ffaaからbbへのクライスリ射と呼ぶ。

クライスリ圏における射の合成はクライスリ射のモナディック合成によって定義される。例として、g𝐊g_{\mathbf{K}}f𝐊f_{\mathbf{K}}の後に合成するとする。クライスリ圏において: f𝐊abg𝐊bc \begin{gathered} f_{\mathbf{K}} \Colon a \to b \\ g_{\mathbf{K}} \Colon b \to c \end{gathered} のとき、圏𝐂\mathbf{C}において対応するものは、次のとおりだ: faTbgbTc \begin{gathered} f \Colon a \to T b \\ g \Colon b \to T c \end{gathered} 次のような合成: h𝐊=g𝐊f𝐊h_{\mathbf{K}} = g_{\mathbf{K}} \circ f_{\mathbf{K}}𝐂\mathbf{C}でのクライスリ射として定義する: haTch=μ(Tg)f \begin{aligned} h &\Colon a \to T c \\ h &= \mu \circ (T g) \circ f \end{aligned} Haskellでは次のように記述できる:

h = join . fmap g . f

𝐂\mathbf{C}から𝐂T\mathbf{C}_Tへの関手FFがあり、対象に対しては自明な働きをする。射に対しては、ffの戻り値を装飾するクライスリ射を作成することによって、𝐂\mathbf{C}内のff𝐂T\mathbf{C}_T内の射に写す。次のような射が与えられたとき: fabf \Colon a \to b 対応するクライスリ射を使って𝐂T\mathbf{C}_T内の射ができる: ηf\eta \circ f Haskellでは次のように記述できる:

return . f

また、𝐂T\mathbf{C}_Tから𝐂\mathbf{C}へ戻る関手GGも定義できる。それはクライスリ圏から対象aaを取り、𝐂\mathbf{C}内の対象TaT aに写す。クライスリ射: faTbf \Colon a \to T b に相当する射f𝐊f_{\mathbf{K}}について、この関手の作用は𝐂\mathbf{C}内の射: TaTbT a \to T b であり、まずffをリフトしてからμ\muを適用することで得られる: μTbTf\mu_{T b} \circ T f Haskellの記法だと次のようになる:

G fT = join . fmap f

これはモナディックなバインドのjoinに基づく定義に見えるだろう。

2つの関手が随伴を形成しているのは容易に理解できる: FGF \dashv G そして、それらの合成GFG \circ FはもとのモナドTTを再現する。

したがって、これは同じモナドを生成する2番目の随伴だ。実際、𝐀𝐝𝐣(𝐂,T)\mathbf{Adj}(\mathbf{C}, T) という随伴全体の圏が存在し、その結果、𝐂\mathbf{C}において同じモナドTTが生成される。その圏では、いま見たクライスリ随伴が始対象で、Eilenberg-Moore随伴が終対象だ。

25.3 コモナドの余代数

同様の構成は任意のコモナドWWに対して行える。コモナドが成立する余代数の圏を定義できる。それらの余代数は次の図式を可換にする:

ここで、𝑐𝑜𝑎\mathit{coa}aaを台とする余代数の余評価射 (coevaluation morphism) だ: 𝑐𝑜𝑎aWa\mathit{coa} \Colon a \to W a そして、ε\varepsilonδ\deltaはコモナドを定義する2つの自然変換だ(Haskellでは、これらの成分はextractおよびduplicateと呼ばれる)。

これらの余代数の圏から𝐂\mathbf{C}へは自明な忘却関手UWU^Wが存在する。それはただ余評価を忘却している。その右随伴FWF^Wについて考えよう。 UWFWU^W \dashv F^W 忘却関手のこの右随伴は余自由関手 (cofree functor) と呼ばれる。FWF^Wは余自由余代数 (cofree coalgebra) を生成する。それは𝐂\mathbf{C}内の対象aaに余代数 (Wa,δa)(W a, \delta_a) を割り当てる。随伴はもとのコモナドを合成FWUWF^W \circ U^Wとして再生成する。

同様に、余クライスリ射から余クライスリ圏を構築でき、対応する随伴からコモナドを再生成できる。

25.4 レンズ

レンズの話に戻ろう。レンズは余代数を使って書ける: 𝑐𝑜𝑎𝑙𝑔𝑠a𝑆𝑡𝑜𝑟𝑒sa\mathit{coalg_s} \Colon a \to \mathit{Store}\ s\ a ここで関手𝑆𝑡𝑜𝑟𝑒s\mathit{Store}\ sは次のとおりだ:

data Store s a = Store (s -> a) s

この余代数は、以下の関数のペアとしても表せる: setasagetas \begin{aligned} set &\Colon a \to s \to a \\ get &\Colon a \to s \end{aligned} (aaは「すべて」(all) を表し、ssはその「小さな」(small) 一部だと考えてほしい)。このペアに関して、次が成り立つ: 𝑐𝑜𝑎𝑙𝑔𝑠a=𝑆𝑡𝑜𝑟𝑒(𝑠𝑒𝑡a)(𝑔𝑒𝑡a)\mathit{coalg_s}\ a = \mathit{Store}\ (\mathit{set}\ a)\ (\mathit{get}\ a) ここで、aaは型aaの値だ。部分適用されたsetは関数sas \to aであることに注意してほしい。

また、𝑆𝑡𝑜𝑟𝑒s\mathit{Store}\ sがコモナドなのも分かっている。

instance Comonad (Store s) where
  extract (Store f s) = f s
  duplicate (Store f s) = Store (Store f) s

問題は、あるレンズがこのコモナドの余代数になる条件は何かということだ。第1のコヒーレンス条件: εa𝑐𝑜𝑎𝑙𝑔=𝐢𝐝a\varepsilon_a \circ \mathit{coalg} = % \mathbf{id}_{a}% は、次のように解釈できる: mathitseta(mathitgeta)=amathit{set}\ a\ (mathit{get}\ a) = a このレンズ則は、データ構造aaのフィールドを以前の値に設定しても何も変わらないという事実を表す。

第2の条件: 𝑓𝑚𝑎𝑝𝑐𝑜𝑎𝑙𝑔𝑐𝑜𝑎𝑙𝑔=δa𝑐𝑜𝑎𝑙𝑔\mathit{fmap}\ \mathit{coalg} \circ \mathit{coalg} = \delta_a \circ \mathit{coalg} は、もう少し手間がかかる。まず、store関手のfmapの定義を思い出してほしい:

fmap g (Store f s) = Store (g . f) s

fmap coalgcoalgの結果に適用すると、次のようになる:

Store (coalg . set a) (get a)

一方、duplicatecoalgの結果に適用すると、次のようになる:

Store (Store (set a)) (get a)

これら2つの式が等しくなるには、Storeの下の2つの関数はどのようなsに作用しても等しくなければならない。

coalg (set a s) = Store (set a) s

coalgを展開すると、次が得られる:

Store (set (set a s)) (get (set a s)) = Store (set a) s

これは残り2つのレンズ則と等価だ。1つ目:

set (set a s) = set a

は、フィールドの値を2回設定するのは1回設定するのと同じだと示している。2つ目:

get (set a s) = s

は、ssに設定されたフィールドの値を取得するとssが返されることを示す。

言い換えれば、行儀の良いレンズはまさにstore関手のコモナド余代数だ。

25.5 課題

  1. 自由関手FCCTF \Colon C \to C^Tの射に対する作用は何か。ヒント:モナディックなμ\muについて自然性条件を使う。

  2. 次の随伴を定義せよ:

    UWFWU^W \dashv F^W

  3. 上記の随伴がもとのコモナドを再現するのを証明せよ。

26 エンドとコエンド

圏内の射に当てはまる直観はたくさんあるが、誰もが同意できるのは、対象aaから対象bbへの射があれば、2つの対象には何らかの「関連がある」というものだ。ある意味で、射はこの関係の証明だ。すべての半順序集合圏では射が関係であるので、それは一目瞭然だ。一般に、2つの対象間での同じ関係の「証明」は複数あり得る。それらの証明はhom集合と呼ばれる集合を形成する。対象を変化させると、対象のペアから「証明」の集合への写像が得られる。この写像は関手的だ――1番目の引数については反変で、2番目の引数については共変だ。これは、圏内の対象間に大域的な関係を確立していると見なせる。この関係は次のhom関手で表せる: 𝐂(,=)𝐂𝑜𝑝×𝐂𝐒𝐞𝐭\mathbf{C}(-, =) \Colon \mathbf{C}^\mathit{op}\times{}\mathbf{C} \to \mathbf{Set} 一般に、このような関手はすべて、圏内の対象間の関係を確立するものとして解釈できる。関係には2つの異なる圏𝐂\mathbf{C}𝐃\mathbf{D}が含まれる場合もある。そのような関係を表す関手は、次のようなシグネチャーを持ち、プロ関手 (profunctor) と呼ばれる: p𝐃𝑜𝑝×𝐂𝐒𝐞𝐭p \Colon \mathbf{D}^\mathit{op}\times{}\mathbf{C} \to \mathbf{Set} これは数学者によると𝐂\mathbf{C}から𝐃\mathbf{D}へのプロ関手(反転に注意)であり、斜線付き矢印で記される: 𝐂𝐃\mathbf{C} \nrightarrow \mathbf{D} プロ関手は、𝐂\mathbf{C}の対象と𝐃\mathbf{D}の対象の間の証明で関連した関係 (proof-relevant relation) と見なせる。ここで、集合の要素は関係の証明を象徴している。pabp\ a\ bが空の場合は常に、aabbの間に関係はない。関係は対称である必要はないことに注意してほしい。

もう1つの有用な直観は、自己関手はコンテナーであるという考え方の一般化だ。型がpabp\ a\ bのプロ関手の値は、型aaの要素をキーとするbbのコンテナーと見なせる。特に、homプロ関手の要素はaaからbbへの関数だ。

Haskellでは、プロ関手は2引数の型構成子pとして定義され、関数のペアをリフトするdimapと呼ばれるメソッドを備えている。そのペアのうち最初の関数は「間違った」方向を向いている:

class Profunctor p where
    dimap :: (c -> a) -> (b -> d) -> p a b -> p c d

プロ関手の関手性は、abに関連しているという証明があれば、cdに関連しているという証明も、cからaへの射とbからdへの別の射がある限り成り立つことを示している。あるいは、1番目の関数は新しいキーを古いキーに変換し、2番目の関数はコンテナーの内容を変更するとも見なせる。

1つの圏内で作用するプロ関手では、型paap\ a\ aの対角要素からかなり多くの情報が取り出せる。bbccに関連していることは、bab \to aaca \to cという射のペアがある限り証明できる。さらには、単一の射を使って非対角値に到達することもできる。たとえば、射fabf \Colon a \to bがある場合、ペアf,𝐢𝐝b\langle f, % \mathbf{id}_{b}% \rangleをリフトすることでpbbp\ b\ bからpabp\ a\ bへ向かえる:

dimap f id pbb :: p a b

あるいは、ペア𝐢𝐝a,f\langle % \mathbf{id}_{a}% , f \rangleをリフトすることでもpaap\ a\ aからpabp\ a\ bへ向かえる:

dimap id f paa :: p a b

26.1 対角自然変換

プロ関手は関手なので、それらの間の自然変換は通常の方法で定義できる。ただし、多くの場合は2つのプロ関手の対角要素間の写像を定義するだけで十分だ。このような変換は、対角要素を非対角要素に接続できる2つの方法を反映した可換条件を満たす場合、対角自然変換 (dinatural transformation) と呼ばれる。関手圏[𝐂𝑜𝑝×𝐂,𝐒𝐞𝐭]{[}\mathbf{C}^\mathit{op}\times{}\mathbf{C}, \mathbf{Set}{]}のメンバーである2つのプロ関手ppqqについて、その間の対角自然変換は次のような射の族だ: αapaaqaa\alpha_a \Colon p\ a\ a \to q\ a\ a そこではあらゆるfabf \Colon a \to bについて次の図式が可換となる:

これは厳密に自然性条件より弱いことに注意してほしい。α\alpha[𝐂𝑜𝑝×𝐂,𝐒𝐞𝐭]{[}\mathbf{C}^\mathit{op}\times{}\mathbf{C}, \mathbf{Set}{]}内の自然変換だったなら、前記の図式は2つの自然性の四角い図式と1つの関手性条件(合成を保持するプロ関手qq)から構成できただろう:

[𝐂𝑜𝑝×𝐂,𝐒𝐞𝐭]{[}\mathbf{C}^\mathit{op}\times{}\mathbf{C}, \mathbf{Set}{]}内の自然変換α\alphaの成分は、対象のペアによってαab\alpha_{a b}のように添字付けされていることに注意してほしい。一方で、対角自然変換では1つの対象によって添字付けされる。対応するプロ関手の対角要素のみを写すからだ。

26.2 エンド

いまや「代数」から圏論の「微積分」と見なせるものへ進む準備ができた。エンド(およびコエンド)の微積分は、古典的な微積分から、その概念だけでなく記法さえいくつか借用している。特に、コエンドは無限和あるいは積分として理解でき、エンドは無限積に類似している。ディラックのデルタ関数に似たものさえある。

エンドは極限を一般化したもので、関手がプロ関手に置き換えられている。錐の代わりに、くさび (wedge) がある。くさびの底面はプロ関手ppの対角要素によって形成される。くさびの頂点は対象(ここでは、𝐒𝐞𝐭\mathbf{Set}値のプロ関手を想定しているため、集合)であり、側面は頂点を底面内の集合に写す関数の族だ。この族は、1つの多相関数――戻り値の型が多相である関数――と見なせる: αa.𝑎𝑝𝑒𝑥paa\alpha \Colon \forall a\ .\ \mathit{apex} \to p\ a\ a 錐とは違って、くさびには底面の頂点同士を接続する関数はない。しかし、すでに見たとおり、𝐂\mathbf{C}内の任意の射fabf \Colon a \to bについて、paap\ a\ apbbp\ b\ bの両方を共通の集合pabp\ a\ bに接続できる。したがって、次の図式が可換だと主張できる:

これはくさび条件 (wedge condition) と呼ばれる。これは次のように書ける: p𝐢𝐝afαa=pf𝐢𝐝bαbp\ % \mathbf{id}_{a}% \ f \circ \alpha_a = p\ f\ % \mathbf{id}_{b}% \circ \alpha_b あるいは、Haskellの記法を使うと次のようになる:

dimap id f . alpha = dimap f id . alpha

ここで普遍的構成を使って進み、ppのエンドを普遍くさび (universal wedge) として定義できる。これはeeと関数π\piの族の組で、頂点aaと族α\alphaの組からなる他のすべてのくさびについて一意な関数haeh \Colon a \to eが存在し、すべての三角図式を可換にする: πah=αa\pi_a \circ h = \alpha_a

エンドの記号は、下付きの「積分変数」を添えた積分記号だ: cpcc\int_c p\ c\ c π\piの成分はエンドの射影写像 (projection map) と呼ばれる: πacpccpaa\pi_a \Colon \int_c p\ c\ c \to p\ a\ a 𝐂\mathbf{C}が離散圏(恒等射以外の射がない)の場合、エンドは圏𝐂\mathbf{C}全体にわたるppのすべての対角要素の大域的な積になる。後ほど説明するが、より一般には、この積とエンドの間には等化子を介した関係がある。

Haskellでは、エンドの式は全称量化子 (universal quantifier) に直接変換される:

forall a. p a a

厳密に言うと、これはppのすべての対角要素の積にすぎないが、くさび条件はパラメトリシティ107によって自動的に満たされる。任意の関数fabf \Colon a \to bについて、くさび条件は次のようになる:

dimap f id . pi = dimap id f . pi

あるいは、型注釈を付けると次のようになる:

dimap f idb . pib = dimap ida f . pia

この式の両辺の型は:

Profunctor p => (forall c. p c c) -> p a b

であり、piは多相射影だ:

pi :: Profunctor p => forall c. (forall a. p a a) -> p c c
pi e = e

ここで、型推論によって自動的にeの正しい成分が選択される。

錐のすべての可換条件を1つの自然変換として表せたように、すべてのくさび条件も1つの対角自然変換にまとめられる。そのためには、定関手Δc\Delta_cをある定プロ関手 (constant profunctor) へと一般化する必要がある。その定プロ関手は、対象のすべてのペアを単一の対象ccに写し、射のすべてのペアをその対象の恒等射に写す。くさびは、その関手からプロ関手ppへの対角自然変換だ。実際、Δc\Delta_cがすべての射を単一の恒等関数にリフトすることに気付けば、対角自然性の六角形はくさびのダイヤモンドへと収縮できる。

行き先の圏が𝐒𝐞𝐭\mathbf{Set}でなくてもその圏に対してエンドを定義できるが、これ以降では集合値プロ関手 (𝐒𝐞𝐭\mathbf{Set}-valued profunctor) とそのエンドのみを扱う。

26.3 等化子としてのエンド

エンドの定義における可換条件は等化子を用いて書ける。まず、2つの関数を定義しよう(ここではHaskellの表記の方が数学的表記よりユーザーフレンドリーだと思うので、そちらを使う)。それらの関数はくさび条件の2つの収束分岐 (converging branch) に対応する:

lambda :: Profunctor p => p a a -> (a -> b) -> p a b
lambda paa f = dimap id f paa

rho :: Profunctor p => p b b -> (a -> b) -> p a b
rho pbb f = dimap f id pbb

どちらの関数もプロ関手pの対角要素を次の型の多相関数に写す:

type ProdP p = forall a b. (a -> b) -> p a b

これらの関数の型は異なる。しかし、pのすべての対角要素を集めて大きな直積型を1つ形成すれば、それらの型を単一化できる。

newtype DiaProd p = DiaProd (forall a. p a a)

関数lambdarhoはこの直積型から2つの写像を導出する:

lambdaP :: Profunctor p => DiaProd p -> ProdP p
lambdaP (DiaProd paa) = lambda paa

rhoP :: Profunctor p => DiaProd p -> ProdP p
rhoP (DiaProd paa) = rho paa

pのエンドはこれら2つの関数の等化子だ。等化子は2つの関数が等しい最大の部分集合を選択することを思い出してほしい。ここでは、くさびの図式が可換となるような、すべての対角要素の積の部分集合が選択される。

26.4 エンドとしての自然変換

エンドの最も重要な例は自然変換の集合だ。2つの関手FFGGの間の自然変換は、𝐂(Fa,Ga)\mathbf{C}(F a, G a) という形式のhom集合から選択された射の族だ。もし自然性条件がなければ、自然変換の集合はこれらすべてのhom集合の積にすぎないだろう。実際、Haskellでは次のようになる:

forall a. f a -> g a

Haskellでこれが動作する理由は、自然性がパラメトリック性に従うためだ。ただし、Haskell以外では、このようなhom集合に渡るすべての対角要素が自然変換を生成するわけではない。しかし、写像: a,b𝐂(Fa,Gb)\langle a, b \rangle \to \mathbf{C}(F a, G b) がプロ関手であることに注目すれば、そのエンドを研究するのは意味がある。くさび条件は次のとおりだ:

集合c𝐂(Fc,Gc)\int_c \mathbf{C}(F c, G c) から要素を1つだけ選択しよう。この要素は2つの射影によって特定の変換の2つの成分に写される。それらを次のように呼ぼう: τaFaGaτbFbGb \begin{aligned} \tau_a &\Colon F a \to G a \\ \tau_b &\Colon F b \to G b \end{aligned} 図式の左側の分岐では、一対の射𝐢𝐝a,Gf\langle % \mathbf{id}_{a}% , G f \rangleをリフトするのにhom関手を使う。このようなリフトを前合成と後合成として一度に実装したのを思い出してほしい。τa\tau_aに作用するとき、リフトされたペアから次が得られる: Gfτa𝐢𝐝aG f \circ \tau_a \circ % \mathbf{id}_{a}% 図式のもう一方の分岐からは次が得られる: 𝐢𝐝bτbFf% \mathbf{id}_{b}% \circ \tau_b \circ F f くさび条件が要求するこれらの等しさはτ\tauの自然性条件に他ならない。

26.5 コエンド

予想どおり、エンドの双対はコエンドと呼ばれる。それはくさびの双対で構成され、余くさび (cowedge) と呼ばれる(cowedgeの発音はco-wedgeで、cow-edgeではない)。

エッジの効いた牛 (An edgy cow)?

コエンドの記号は、上付きの「積分変数」を添えた積分記号だ: cpcc\int^c p\ c\ c エンドが積に関連しているのと同様に、コエンドは余積すなわち和に関連している(この観点でコエンドは和の極限である積分に似ている)。射影を持つのではなく、プロ関手の対角要素からコエンドに向かう単射を持つ。余くさび条件がなければ、プロ関手ppのコエンドはpaap\ a\ apbbp\ b\ bpccp\ c\ cなどのどれかだと言えただろう。あるいは、コエンドが単なる集合paap\ a\ aになるようなaaが存在すると言えただろう。エンドの定義で使った全称量化子は、コエンドでは存在量化子 (existential quantifier) になる。

このため、擬似Haskellでは、コエンドを次のように定義する:

exists a. p a a

Haskellで存在量化子を実装する標準的な方法では、全称量化されたデータ構成子を用いる。したがって、次のように定義できる:

data Coend p = forall a. Coend (p a a)

この背後にあるロジックは、型paap\ a\ aの族のうち任意の型の値を使うなら、どのようなaaを選択してもコエンドを構築できるというものだ。

エンドが等化子を使って定義できるのと同様に、コエンドは余等化子 (coequalizer) を使って記述できる。すべての余くさび条件は、要約すると、可能なすべての関数bab \to aについてpabp\ a\ bの単一の巨大な余積を取ることだ。これはHaskellでは存在型 (existential type) として表現される:

data SumP p = forall a b. SumP (b -> a) (p a b)

この直和型を評価する方法は2つある。dimapを使って関数をリフトする方法と、プロ関手ppに適用する方法だ:

lambda, rho :: Profunctor p => SumP p -> DiagSum p
lambda (SumP f pab) = DiagSum (dimap f id pab)
rho    (SumP f pab) = DiagSum (dimap id f pab)

ここで、DiagSumppの対角要素の和だ:

data DiagSum p = forall a. DiagSum (p a a)

これら2つの関数の余等化子はコエンドだ。余等化子をDiagSum pから得るには、lambdaまたはrhoを同じ引数に対して適用することで得られる値を同一視する。ここで、引数は関数bab \to apabp\ a\ bの要素のペアだ。lambdarhoを適用すると、潜在的に異なる2つの値が生成され、それらの型はDiagSum pだ。コエンドでは、これらの2つの値が同一視され、余くさび条件が自動的に満たされる。

集合内の関連する要素を同一視するこのプロセスは形式的に、商を取る、と呼ばれる。商を定義するには同値関係 (equivalence relation) \simが必要だ。同値関係は反射律・対称律・推移律を満たす: aaifabthenbaifabandbcthenac \begin{aligned} &a \sim a \\ &\text{if}\ a \sim b\ \text{then}\ b \sim a \\ &\text{if}\ a \sim b\ \text{and}\ b \sim c\ \text{then}\ a \sim c \end{aligned} このような関係は集合を同値類 (equivalence class) に分割する。各同値類は相互に関連する要素で構成される。商集合は各同値類から1人の代表を選んで作られる。古典的な例は、同値関係を持つ整数のペアとしての有理数の定義だ: (a,b)(c,d)iffa*d=b*c(a, b) \sim (c, d)\ \text{iff}\ a * d = b * c これが同値関係なのは簡単に確認できる。ペア (a,b)(a, b) は分数ab\frac{a}{b}として解釈され、分子と分母が公約数を持つものは同一視される。有理数はそのような分数の同値類だ。

極限と余極限についての以前の議論から、hom関手は連続的であり、すなわち極限を保存することを思い出してほしい。双対性より、反変hom関手は余極限を極限に変える。これらの特性はエンドとコエンドに一般化でき、それぞれ極限と余極限を一般化したものだ。特に、コエンドをエンドに変換するのに非常に便利な恒等式が得られる: 𝐒𝐞𝐭(xpxx,c)x𝐒𝐞𝐭(pxx,c)\mathbf{Set}(\int^x p\ x\ x, c) \cong \int_x \mathbf{Set}(p\ x\ x, c) 擬似Haskellで見てみよう:

(exists x. p x x) -> c ≅ forall x. p x x -> c

これは、存在型を取る関数は多相関数と等価だと示している。これは完全に理にかなっている。そのような関数は、存在型として表される可能性のある型ならどれでも処理できるようになっている必要があるからだ。これは、直和型を受け入れる関数についての、すべての型に対応したハンドラーの組を持ったcase式として実装される、という原則と同じものだ。ここでは、直和型はコエンドに置き換えられ、ハンドラーの族はエンドすなわち多相関数になる。

26.6 忍者米田の補題

米田の補題に現れる自然変換の集合はエンドを使って表せて、結果としてこう定式化できる: z𝐒𝐞𝐭(𝐂(a,z),Fz)Fa\int_z \mathbf{Set}(\mathbf{C}(a, z), F z) \cong F a 双対として次の式も存在する: z𝐂(z,a)×FzFa\int^z \mathbf{C}(z, a)\times{}F z \cong F a この恒等式はディラックのデルタ関数の式(関数δ(az)\delta(a - z)、というよりa=za = zに無限大のピークを持つ分布)を強く連想させる。ここでは、hom関手がデルタ関数の役割を果たしている。

これら2つの恒等式を合わせて忍者米田の補題 (Ninja Yoneda lemma) と呼ぶことがある108

2番目の式を証明するには米田埋め込みからの帰結を使う。その帰結によると、2つの対象が同型となるのは、それらのhom関手が同型である場合に限られる。言い換えると、aba \cong bとなるのは、次の型の自然変換: [𝐂,𝐒𝐞𝐭](𝐂(a,),𝐂(b,=))[\mathbf{C}, \mathbf{Set}](\mathbf{C}(a, -), \mathbf{C}(b, =)) があって同型射である場合だけだ。

まず、証明したい同型の左辺を、任意の対象ccに向かうhom関手の内側に代入する: 𝐒𝐞𝐭(z𝐂(z,a)×Fz,c)\mathbf{Set}(\int^z \mathbf{C}(z, a)\times{}F z, c) Continuity argumentを使うと、コエンドをエンドに置き換えられる: z𝐒𝐞𝐭(𝐂(z,a)×Fz,c)\int_z \mathbf{Set}(\mathbf{C}(z, a)\times{}F z, c) これで積と冪の間の随伴を利用できるようになった: z𝐒𝐞𝐭(𝐂(z,a),c(Fz))\int_z \mathbf{Set}(\mathbf{C}(z, a), c^{(F z)}) 米田の補題を使って「積分を実行」し、次が得られる: c(Fa)c^{(F a)} (関手c(Fz)c^{(F z)}zzについて反変なので、米田の補題の反変版を使っていることに注意してほしい。) この冪対象は次のhom集合と同型だ: 𝐒𝐞𝐭(Fa,c)\mathbf{Set}(F a, c) 最後に、米田埋め込みを利用して同型に辿り着く: z𝐂(z,a)×FzFa\int^z \mathbf{C}(z, a)\times{}F z \cong F a

26.7 プロ関手の合成

プロ関手が記述するのは関係――より正確には証明で関連した関係――だ、という考え方をさらに検討しよう。つまり、集合pabp\ a\ bは、aabbに関連する証明の集合を表す。2つの関係ppqqがあるなら、それらを合成できる。qbcq\ b\ cpcap\ c\ aが両方とも空でないような中間対象ccが存在する場合、qqppの後に合成したものを介してaabbに関連していると言える。この新しい関係の証明はすべて、個々の関係の証明のペアだ。したがって、存在量化子はコエンドに対応し、2つの集合のデカルト積は「証明のペア」に対応する、という理解に基づいて、次の式を使ってプロ関手の合成を定義できる: (qp)ab=cpca×qbc(q \circ p)\ a\ b = \int^c p\ c\ a\times{}q\ b\ c HaskellのData.Profunctor.Compositionでのプロ関手は、名前をいくらか変更したうえで、こう定義されている:

data Procompose q p a b where
  Procompose :: q a c -> p c b -> Procompose q p a b

これは一般化代数的データ型 (generalized algebraic data type) すなわちGADTの構文を使っており、自由型変数(ここではc)が自動的に存在量化される。したがって、(非カリー化された)データ・コンストラクターProcomposeは次と等価だ:

exists c. (q a c, p c b)

こうして定義された合成の単位元はhom関手だ――これは忍者米田の補題からすぐに導出される。したがって、プロ関手が射の役割を果たす圏があるかを問うのは理にかなっている。ある、というのがその問いの答えだ。ただし、プロ関手の合成に関する結合律と恒等律は自然同型を除いてのみ満たされることに注意する必要がある。それらの結合律と恒等律が同型を除いて満たされる圏は、(𝟐\mathbf{2}-圏よりも一般化された名前で) 双圏 (bicategory) と呼ばれる。すなわち、双圏𝐏𝐫𝐨𝐟\mathbf{Prof}では、対象は圏であり、射はプロ関手であり、射同士の間の射 (別名2-セル) は自然変換だ。実際には、さらに先へも進める。プロ関手以外にも圏間の射としての通常の関手があるからだ。2種類の射を持つ圏は二重圏 (double category) と呼ばれる。

プロ関手はHaskellのlensライブラリとarrowライブラリで重要な役割を果たしている。

27 カン拡張

ここまでは、主に1つの圏または1対の圏を扱ってきた。しかし、それでは制約が強すぎる場合もある。

たとえば、圏𝐂\mathbf{C}で極限を定義するとき、添字圏 (index category) 𝐈\mathbf{I}を導入し、錐の基礎となるパターンのテンプレートとした。錐の頂点のテンプレートとしては、別の自明な圏を導入した方が理にかなっていただろう。それなのに、代わりに𝐈\mathbf{I}から𝐂\mathbf{C}への定関手Δc\Delta_cを使った。

この不自然さを正す時が来た。極限を3つの圏を使って定義しよう。初めに、添字圏𝐈\mathbf{I}から𝐂\mathbf{C}への関手DDについて考える。これは錐の底面を指す関手――図式関手 (diagram functor) だ。

新しく追加された圏𝟏\mathbf{1}は、1つの対象(および1つの恒等射)を含む。𝐈\mathbf{I}からこの圏への関手はKKだけしかない。対象はすべて𝟏\mathbf{1}内の唯一の対象に写され、射はすべて恒等射に写される。𝟏\mathbf{1}から𝐂\mathbf{C}への関手FFはどれも、ここでの錐の潜在的な頂点を指す。

錐はFKF \circ KからDDへの自然変換ε\varepsilonだ。FKF \circ KはもとのΔc\Delta_cと全く同じことを行う。次の図式はこの変換を示している。

ここで、このような関手FFのうち「最高」のものを選択するような普遍性を定義できる。このFF𝟏\mathbf{1}𝐂\mathbf{C}内のDDの極限である対象に写し、FKF \circ KからDDへの自然変換ε\varepsilonがそれに対応する射影を提供する。この普遍関手はKKに沿ったDDの右カン拡張 (right Kan extension) と呼ばれ、𝐑𝐚𝐧KD\mathbf{Ran}_{K}Dで表される。

普遍性を定式化しよう。錐がもう1つあるとする――別の関手FF'と、FKF' \circ KからDDへの自然変換ε\varepsilon'から構成された錐だ。

カン拡張F=𝐑𝐚𝐧KDF = \mathbf{Ran}_{K}Dが存在するなら、FF'からそれへの一意な自然変換σ\sigmaが必ず存在し、ε\varepsilon'ε\varepsilonを通じて次のように分解される: ε=ε.(σK)\varepsilon' = \varepsilon\ .\ (\sigma \circ K) ここで、σK\sigma \circ Kは2つの自然変換の水平合成だ(片方はKKにおける恒等自然変換だ)。この変換はε\varepsilonによって垂直合成される。

成分でみると、𝐈\mathbf{I}内の対象iiに作用するとき、次が得られる: εi=εiσKi\varepsilon'_i = \varepsilon_i \circ \sigma_{K i} この例において、𝟏\mathbf{1}の単一の対象に対応するσ\sigmaの成分は1つしかない。したがって、これは実際に、FF'が定義する錐の頂点から𝐑𝐚𝐧KD\mathbf{Ran}_{K}Dが定義する普遍錐の頂点へ向かう一意な射だ。その可換条件はまさに極限の定義で求められているものだ。

もっとも、重要なのは、自明圏 (trivial category) 𝟏\mathbf{1}を任意の圏𝐀\mathbf{A}に自由に置き換えてよく、右カン拡張の定義は有効なままだということだ。

27.1 右カン拡張

関手K𝐈𝐀K \Colon \mathbf{I} \to \mathbf{A}に沿った関手D𝐈𝐂D \Colon \mathbf{I} \to \mathbf{C}の右カン拡張は関手F𝐀𝐂F \Colon \mathbf{A} \to \mathbf{C}であり、𝐑𝐚𝐧KD\mathbf{Ran}_{K}Dと表記され、自然変換 εFKD\varepsilon \Colon F \circ K \to D を伴い、他のすべての関手F𝐀𝐂F' \Colon \mathbf{A} \to \mathbf{C}と自然変換 εFKD\varepsilon' \Colon F' \circ K \to D について、一意な自然変換 σFF\sigma \Colon F' \to F が存在し、ε\varepsilon'を分解する: ε=ε.(σK)\varepsilon' = \varepsilon\ .\ (\sigma \circ K) これはかなり長ったらしいが、図式ならすっきりと描ける:

この図式はあることに気付くと興味深く見えてくる。それはカン拡張が、ある意味で「関手の積」の逆のように作用していることだ。𝐑𝐚𝐧KD\mathbf{Ran}_{K}Dに対してD/KD/Kという表記を使う著者さえいる。実際、その表記なら、右カン拡張の余単位とも呼ばれるε\varepsilonの定義は単純な簡約のように見える: εD/KKD\varepsilon \Colon D/K \circ K \to D カン拡張には別の解釈もある。関手KKが圏𝐈\mathbf{I}𝐀\mathbf{A}の中に埋め込むとする。最も単純なケースでは、𝐈\mathbf{I}は単に𝐀\mathbf{A}の部分圏になる。𝐈\mathbf{I}𝐂\mathbf{C}に写す関手DDがある。DD𝐀\mathbf{A}全体で定義された関手FFに拡張できるだろうか? 理想的には、そのような拡張によって合成FKF \circ KDDと同型になる。言い換えると、FFDDの始域を𝐀\mathbf{A}に拡張する。しかし、本格的な同型は通常求めすぎで、半分で十分だ。つまり、FKF \circ KからDDへの片道の自然変換ε\varepsilonだけでよい。(逆方向は左カン拡張が指す。)

当然、この埋め込みの描像は、極限のような例では破綻する。関手KKが対象の単射ではなく、すなわちhom集合の忠実関手ではないからだ。その場合、カン拡張は失われた情報を推定するために最善を尽くす。

27.2 随伴としてのカン拡張

さて、右カン拡張が任意のDD(および決まったKK)に対して存在するとしよう。この場合、(DDをダッシュで置き換えた)𝐑𝐚𝐧K\mathbf{Ran}_{K}-は関手圏[𝐈,𝐂]{[}\mathbf{I}, \mathbf{C}{]}から関手圏[𝐀,𝐂]{[}\mathbf{A}, \mathbf{C}{]}への関手だ。この関手は前合成関手K- \circ Kに対する右随伴だと分かる。この前合成関手は[𝐀,𝐂]{[}\mathbf{A}, \mathbf{C}{]}内の関手を[𝐈,𝐂]{[}\mathbf{I}, \mathbf{C}{]}内の関手に写す。この随伴はこうなる: [𝐈,𝐂](FK,D)[𝐀,𝐂](F,𝐑𝐚𝐧KD)[\mathbf{I}, \mathbf{C}](F' \circ K, D) \cong [\mathbf{A}, \mathbf{C}](F', \mathbf{Ran}_{K}D) これは、ε\varepsilon'と呼んでいたすべての自然変換が、σ\sigmaと呼んでいた一意な自然変換に対応しているという事実を言い換えたものだ。

さらに、圏𝐈\mathbf{I}𝐂\mathbf{C}と同じになるように選んだ場合は、恒等関手I𝐂I_{\mathbf{C}}DDに置き換えられる。すると、次の恒等式が得られる: [𝐂,𝐂](FK,I𝐂)[𝐀,𝐂](F,𝐑𝐚𝐧KI𝐂)[\mathbf{C}, \mathbf{C}](F' \circ K, I_{\mathbf{C}}) \cong [\mathbf{A}, \mathbf{C}](F', \mathbf{Ran}_{K}I_{\mathbf{C}}) これで、𝐑𝐚𝐧KI𝐂\mathbf{Ran}_{K}I_{\mathbf{C}}と同じになるようなFF'を選択できる。その場合、右辺は恒等自然変換を含み、それに対応して、左辺から次の自然変換が得られる: ε𝐑𝐚𝐧KI𝐂KI𝐂\varepsilon \Colon \mathbf{Ran}_{K}I_{\mathbf{C}} \circ K \to I_{\mathbf{C}} これは随伴の余単位によく似ている: 𝐑𝐚𝐧KI𝐂K\mathbf{Ran}_{K}I_{\mathbf{C}} \dashv K 実際、関手KKに沿った恒等関手の右カン拡張を使ってKKの左随伴を計算できる。そのためには、もう1つの条件が必要だ。すなわち、右カン拡張が関手KKによって保存されなければならない。このカン拡張の保存が意味するのは、この関手のカン拡張をKKに前合成したものを計算すると、もとのカン拡張をKKに前合成したのと同じ結果が得られるということだ。ここでは、この条件は次のように単純化される: K𝐑𝐚𝐧KI𝐂𝐑𝐚𝐧KKK \circ \mathbf{Ran}_{K}I_{\mathbf{C}} \cong \mathbf{Ran}_{K}K KKによる除算で表記すると、前述の随伴は次のように記述できる: I/KKI/K \dashv K これは随伴がある種の逆を表しているという直観を裏付ける。また、保存の条件は次のようになる: KI/KK/KK \circ I/K \cong K/K ある関手自身に沿った右カン拡張K/KK/Kは余稠密モナド (codensity monad) と呼ばれる。

この随伴の式は重要な結果だ。これからすぐ見るように、エンド(コエンド)を使ってカン拡張を計算でき、すなわち右(左)随伴を見出す実用的な手段が得られるからだ。

27.3 左カン拡張

左カン拡張を与えるような双対構成が存在する。直観を得るために、余極限の定義から始めて、単元圏𝟏\mathbf{1}を使うようにそれを再構築しよう。余錐を作るには、関手D𝐈𝐂D \Colon \mathbf{I} \to \mathbf{C}を使って底面を形成し、関手F𝟏𝐂F \Colon \mathbf{1} \to \mathbf{C}を使って頂点を選択する。

余錐の側面である単射はどれも、DDからFKF \circ Kへの自然変換η\etaの成分だ。

余極限は普遍余錐だ。したがって、他のすべての関手FF'と自然変換 ηDFK\eta' \Colon D \to F' \circ K

について、FFからFF'への一意な自然変換σ\sigma

が存在する。ただし: η=(σK).η\eta' = (\sigma \circ K)\ .\ \eta これは次の図式で表される:

単元圏𝟏\mathbf{1}𝐀\mathbf{A}に置き換えると、この定義は左カン拡張の定義へと自然に一般化される。左カン拡張は𝐋𝐚𝐧KD\mathbf{Lan}_{K}Dと表記される。

自然変換: ηD𝐋𝐚𝐧KDK\eta \Colon D \to \mathbf{Lan}_{K}D \circ K は左カン拡張の単位と呼ばれる。

先ほどと同様に、自然変換の間の1対1の対応関係: η=(σK).η\eta' = (\sigma \circ K)\ .\ \eta を随伴によって作り直せる: [𝐀,𝐂](𝐋𝐚𝐧KD,F)[𝐈,𝐂](D,FK)[\mathbf{A}, \mathbf{C}](\mathbf{Lan}_{K}D, F') \cong [\mathbf{I}, \mathbf{C}](D, F' \circ K) 言い換えると、KKに前合成することで、左カン拡張は左随伴となり、右カン拡張は右随伴となる。

恒等関手の右カン拡張がKKの左随伴を計算するのに使われるように、恒等関手の左カン拡張はKKの右随伴だと分かる(η\etaは随伴の単位だとする): K𝐋𝐚𝐧KI𝐂K \dashv \mathbf{Lan}_{K}I_{\mathbf{C}} 2つの結果を組み合わせると、次が得られる: 𝐑𝐚𝐧KI𝐂K𝐋𝐚𝐧KI𝐂\mathbf{Ran}_{K}I_{\mathbf{C}} \dashv K \dashv \mathbf{Lan}_{K}I_{\mathbf{C}}

27.4 エンドとしてのカン拡張

カン拡張の真の力は、エンド(およびコエンド)を使って計算できるという事実に由来する。簡単のため行き先の圏𝐂\mathbf{C}𝐒𝐞𝐭\mathbf{Set}である場合に注目するが、どの式も任意の圏に拡張できる。

カン拡張を使えば関手の作用をもとの始域の外側に拡張できる、という考え方を再検討してみよう。KK𝐈\mathbf{I}𝐀\mathbf{A}の中に埋め込むとする。関手DD𝐈\mathbf{I}𝐒𝐞𝐭\mathbf{Set}に写す。KKの像内の任意の対象aaについてa=Kia = K iであり、拡張された関手はaaDiD iに写すと言える。問題は、KKの像の外側にある𝐀\mathbf{A}内の対象をどうするかだ。そのような対象はどれもたくさんの射を通じてKKの像内のすべての対象と潜在的に繋がっている、と考えればよい。関手はそれらの射を保存しなければならない。対象aaからKKの像への射の全体は次のhom関手によって特徴づけられる: 𝐀(a,K)\mathbf{A}(a, K -)

このhom関手は2つの関手の合成であることに注意してほしい: 𝐀(a,K)=𝐀(a,)K\mathbf{A}(a, K -) = \mathbf{A}(a, -) \circ K 右カン拡張は関手合成の右随伴だ: [𝐈,𝐒𝐞𝐭](FK,D)[𝐀,𝐒𝐞𝐭](F,𝐑𝐚𝐧KD)[\mathbf{I}, \mathbf{Set}](F' \circ K, D) \cong [\mathbf{A}, \mathbf{Set}](F', \mathbf{Ran}_{K}D) FF'をhom関手に置き換えるとどうなるか見てみよう: [𝐈,𝐒𝐞𝐭](𝐀(a,)K,D)[𝐀,𝐒𝐞𝐭](𝐀(a,),𝐑𝐚𝐧KD)[\mathbf{I}, \mathbf{Set}](\mathbf{A}(a, -) \circ K, D) \cong [\mathbf{A}, \mathbf{Set}](\mathbf{A}(a, -), \mathbf{Ran}_{K}D) そして、先ほどの合成で表すとこうなる: [𝐈,𝐒𝐞𝐭](𝐀(a,K),D)[𝐀,𝐒𝐞𝐭](𝐀(a,),𝐑𝐚𝐧KD)[\mathbf{I}, \mathbf{Set}](\mathbf{A}(a, K -), D) \cong [\mathbf{A}, \mathbf{Set}](\mathbf{A}(a, -), \mathbf{Ran}_{K}D) 右辺は米田の補題を使って簡潔にできる: [𝐈,𝐒𝐞𝐭](𝐀(a,K),D)𝐑𝐚𝐧KDa[\mathbf{I}, \mathbf{Set}](\mathbf{A}(a, K -), D) \cong \mathbf{Ran}_{K}D a ここで、自然変換の集合をエンドに書き直すと、右カン拡張に対する非常に便利な式が得られる: 𝐑𝐚𝐧KDai𝐒𝐞𝐭(𝐀(a,Ki),Di)\mathbf{Ran}_{K}D a \cong \int_i \mathbf{Set}(\mathbf{A}(a, K i), D i) 左カン拡張に対しても、コエンドによって同様の式が得られる: 𝐋𝐚𝐧KDa=i𝐀(Ki,a)×Di\mathbf{Lan}_{K}D a = \int^i \mathbf{A}(K i, a)\times{}D i 確認のため、これが実際に関手合成に対する左随伴であることを証明しよう: [𝐀,𝐒𝐞𝐭](𝐋𝐚𝐧KD,F)[𝐈,𝐒𝐞𝐭](D,FK)[\mathbf{A}, \mathbf{Set}](\mathbf{Lan}_{K}D, F') \cong [\mathbf{I}, \mathbf{Set}](D, F' \circ K) 前述の式で左辺を置換しよう: [𝐀,𝐒𝐞𝐭](i𝐀(Ki,)×Di,F)[\mathbf{A}, \mathbf{Set}](\int^i \mathbf{A}(K i, -)\times{}D i, F') これは自然変換の集合なので、エンドに書き直せる: a𝐒𝐞𝐭(i𝐀(Ki,a)×Di,Fa)\int_a \mathbf{Set}(\int^i \mathbf{A}(K i, a)\times{}D i, F' a) Hom関手の連続性を使って、コエンドをエンドに置き換えられる: ai𝐒𝐞𝐭(𝐀(Ki,a)×Di,Fa)\int_a \int_i \mathbf{Set}(\mathbf{A}(K i, a)\times{}D i, F' a) 積と冪についての随伴が使える: ai𝐒𝐞𝐭(𝐀(Ki,a),(Fa)Di)\int_a \int_i \mathbf{Set}(\mathbf{A}(K i, a),\ (F' a)^{D i}) この冪は、対応するhom集合と同型だ: ai𝐒𝐞𝐭(𝐀(Ki,a),𝐒𝐞𝐭(Di,Fa))\int_a \int_i \mathbf{Set}(\mathbf{A}(K i, a),\ \mathbf{Set}(D i, F' a)) Fubiniの定理と呼ばれる定理より、2つのエンドを交換してよい: ia𝐒𝐞𝐭(𝐀(Ki,a),𝐒𝐞𝐭(Di,Fa))\int_i \int_a \mathbf{Set}(\mathbf{A}(K i, a),\ \mathbf{Set}(D i, F' a)) 内側のエンドは2つの関手間の自然変換の集合を表しているので、米田の補題を使える: i𝐒𝐞𝐭(Di,F(Ki))\int_i \mathbf{Set}(D i, F' (K i)) この自然変換の集合はまさに、証明しようとしていた随伴の右辺を形成する: [𝐈,𝐒𝐞𝐭](D,FK)[\mathbf{I}, \mathbf{Set}](D, F' \circ K) このようなエンド・コエンド・米田の補題を使った計算は、エンドの「微積分」としてはごく典型的なものだ。

27.5 Haskellでのカン拡張

カン拡張のエンド・コエンドの式はHaskellに簡単に変換できる。右カン拡張から始めよう: 𝐑𝐚𝐧KDai𝐒𝐞𝐭(𝐀(a,Ki),Di)\mathbf{Ran}_{K}D a \cong \int_i \mathbf{Set}(\mathbf{A}(a, K i), D i) エンドを全称量化子に、hom集合を関数型に置き換える:

newtype Ran k d a = Ran (forall i. (a -> k i) -> d i)

この定義を見ると、Ranには、関数を適用できる型aの値と、2つの関手kdの間の自然変換が含まれている必要があることは明らかだ。たとえば、kがtree関手、dがリスト関手だとして、Ran Tree [] Stringが与えられたとする。これは、関数:

f :: String -> Tree Int

を渡したならIntのリストが返ってくる、などのように動作する。右カン拡張はこの関数を使って木を生成し、再パッケージしてリストにする。たとえば、文字列から構文解析木を生成するパーサーを渡すと、その木の深さ優先探索順に対応するリストが得られる。

右カン拡張は、関手dを恒等関手に置き換えれば、任意の関手の左随伴を計算するために使える。これにより、関手kの左随伴は、次の型の多相関数の集合で表される:

forall i. (a -> k i) -> i

kがモノイドの圏からの忘却関手だとしよう。すると、全称量化子はすべてのモノイドに及ぶ。もちろんHaskellではモノイド則を表現できないが、以下は結果の自由関手の適切な近似になっている(忘却関手kは対象における恒等関手だ):

type Lst a = forall i. Monoid i => (a -> i) -> i

期待どおり、自由モノイド、すなわちHaskellのリストが得られる:

toLst :: [a] -> Lst a
toLst as = \f -> foldMap f as

fromLst :: Lst a -> [a]
fromLst f = f (\a -> [a])

この左カン拡張はコエンドだ: 𝐋𝐚𝐧KDa=i𝐀(Ki,a)×Di\mathbf{Lan}_{K}D a = \int^i \mathbf{A}(K i, a)\times{}D i したがって、存在量化子へと翻訳される。記号的に表すとこうなる:

Lan k d a = exists i. (k i -> a, d i)

これはHaskellではGADTを使うか、全称量化されたデータ構成子を使ってコード化できる:

data Lan k d a = forall i. Lan (k i -> a) (d i)

このデータ構造を解釈すると、不特定の数のiを含むコンテナーを取ってaを1つ生成する関数を含むものだと言える。また、それらのiのコンテナーも持つ。iが何かは分からないので、このデータ構造で唯一できるのは、iのコンテナーを取得し、自然変換を使って関手kで定義されたコンテナーに再パックし、関数を呼んでaを得ることだけだ。たとえば、dが木でkがリストの場合は、木を直列化して得られるリストで関数を呼べばaが得られる。

左カン拡張は関手の右随伴を計算するために使える。積関手の右随伴が冪なのは知っているので、カン拡張を使って実装してみよう:

type Exp a b = Lan ((,) a) I b

これが実際に関数型と同型であることは、次の関数のペアによって分かる:

toExp :: (a -> b) -> Exp a b
toExp f = Lan (f . fst) (I ())

fromExp :: Exp a b -> (a -> b)
fromExp (Lan f (I x)) = \a -> f (a, x)

すでに一般的な場合について説明したとおり、次のような段階を経ることに注意してほしい:

  1. まず、xのコンテナー(ここでは単なる自明な恒等コンテナー)と関数fを得た。
  2. 次に、そのコンテナーを、恒等関手と関手のペアとの間の自然変換を使って再パックした。
  3. 最後に、関数fを呼んだ。

27.6 自由関手

カン拡張の興味深い応用として自由関手 (free functor) の構成がある。これは以下のような実際的な問題の解となる:対象の写像である型構成子があるとする。その型構成子に基づいて関手を定義可能だろうか? 言い換えると、その型構成子を完全な自己関手へと拡張するような射の写像を定義できるだろうか?

鍵となる観察は、型構成子は離散圏を始域とする関手として表現できる、というものだ。離散圏の射は恒等射だけだ。任意の圏𝐂\mathbf{C}について、単に恒等射以外の射を捨て去れば、常に離散圏|𝐂|\mathbf{|C|}を構成できる。したがって、|𝐂|\mathbf{|C|}から𝐂\mathbf{C}への関手FFは、対象の単純な写像、すなわちHaskellで型構成子と呼ばれるものになる。また、標準関手 (canonical functor) JJも存在し、|𝐂|\mathbf{|C|}𝐂\mathbf{C}内へ注入する。それは対象についての(および恒等射についての)恒等関手だ。FFJJに沿った左カン拡張は、存在するなら、𝐂\mathbf{C}から𝐂\mathbf{C}への関手だ: 𝐋𝐚𝐧JFa=i𝐂(Ji,a)×Fi\mathbf{Lan}_{J}F a = \int^i \mathbf{C}(J i, a)\times{}F i これはFFに基づく自由関手と呼ばれる。

Haskellでは、次のように書ける:

data FreeF f a = forall i. FMap (i -> a) (f i)

実際、すべての型構成子fについてFreeF fは関手となる:

instance Functor (FreeF f) where
  fmap g (FMap h fi) = FMap (g . h) fi

ご覧のとおり、自由関手は、関数と引数の両方を記録することで関数のリフトを偽装する。それは、関数合成を記録することによって、リフトされた関数を累積する。関手則は自動的に満たされる。この構成は論文Freer Monads, More Extensible Effects109で使われたものだ。

別の方法として、右カン拡張も同じ目的に使える:

newtype FreeF f a = FreeF (forall i. (a -> i) -> f i)

これが実際に関手であることは簡単に確認できる:

instance Functor (FreeF f) where
    fmap g (FreeF r) = FreeF (\bi -> r (bi . g))

28 豊穣圏

圏が小さいとは、対象が集合を形成することだ。しかし、集合より大きいものの存在も知られている。有名な話として、すべての集合の集合は、標準的な集合論では形成できない(標準的な集合論とはツェルメロ=フレンケル集合論を指し、選択公理 (Axiom of Choice) を議論に含めることもある)110。つまり、すべての集合の圏は大きいことになる。グロタンディーク宇宙 (Grothendieck universe) などの数学的トリックがいくつかあって、集合を超える集まりを定義するのに使える。それらのトリックによって大きい圏について語れるようになる。

任意の2つの対象間の射が集合を形成するような圏は、局所的に小さい、という。それらが集合を形成しないなら、定義のいくつかは再考する必要がある。特に、射を集合から選ぶことさえできない場合、射の合成とは何を意味するのだろう? 解決策は、𝐒𝐞𝐭\mathbf{Set}内の対象からなるhom集合を他の圏𝐕\mathbf{V}からの対象で置き換えることによってブートストラップすることだ。ただし、一般に、対象には要素がないため個々の射については語れない点が異なる。豊穣圏 (enriched category) のすべての特性を、hom対象に対して実行できる操作全体として定義する必要がある。そのためには、hom対象を提供する圏は、追加の構造を持たなければならない――モノイダル圏でなければならない。このモノイダル圏を𝐕\mathbf{V}と呼ぶとき、圏𝐂\mathbf{C}𝐕\mathbf{V}上の豊穣圏だと言える。

大きさの理由に加えて、hom集合をただの集合以上の構造があるものへと一般化することも興味深そうだ。たとえば、従来の圏には対象間の距離という概念はない。2つの対象は射によって接続されているかいないかのどちらかだ。ある対象に接続されているすべての対象は近傍 (neighbors) と呼ばれる。現実の人生とは違って、圏においては、友人の友人の友人は、親友と同じくらい近しい。だが、適切な豊穣圏では、対象間の距離を定義できる。

豊穣圏について経験を積むべき極めて実用的な理由はもう1つある。それは、圏に関する知識の非常に有用なオンライン情報源であるnLab111が主に豊穣圏について書かれているからだ。

28.1 なぜモノイダル圏か?

豊穣圏を構築する際には、モノイダル圏を𝐒𝐞𝐭\mathbf{Set}に置き換え、hom対象をhom集合に置き換えれば、通常の定義を復元できることに留意する必要がある。これを実現する一番良い方法は、通常の定義から始めて、ポイントフリーな方法で――つまり、集合の要素に名前を付けずに――再定式化を繰り返すことだ。

まず、合成の定義から始めよう。通常は、𝐂(b,c)\mathbf{C}(b, c) からの射1つと𝐂(a,b)\mathbf{C}(a, b) からの射1つからなる射のペアを取り、𝐂(a,c)\mathbf{C}(a, c) からの射に写す。言い換えれば、次の写像だ: 𝐂(b,c)×𝐂(a,b)𝐂(a,c)\mathbf{C}(b, c)\times{}\mathbf{C}(a, b) \to \mathbf{C}(a, c) これは集合間の関数だ――片方は2つのhom集合のデカルト積だ。この式は、デカルト積をより一般的な何かに置き換えれば容易に一般化できる。圏論的な積でもよいが、さらに進んで、完全に一般的なテンソル積を使ってもよい。

次は恒等射の番だ。個々の要素は、hom集合から選択する代わりに、単元集合𝟏\mathbf{1}の関数を使って定義できる: ja𝟏𝐂(a,a)j_a \Colon \mathbf{1} \to \mathbf{C}(a, a) ここでも単元集合は終対象に置き換えられるが、さらに進んでテンソル積の単位元iiにも置き換えられる。

ご覧のように、あるモノイダル圏𝐕\mathbf{V}から取った対象は、hom集合を置き換える候補として適している。

28.2 モノイダル圏

モノイダル圏については前にも述べたが、定義をもう一度述べておく価値はある。モノイダル圏は、テンソル積を双関手として定義する: 𝐕×𝐕𝐕\otimes \Colon \mathbf{V}\times{}\mathbf{V} \to \mathbf{V} このテンソル積は結合的なのが望ましいが、自然同型を除いた結合律さえ満たせば十分だ。この同型射は結合子と呼ばれる。その成分はこうなる: αabc(ab)ca(bc)\alpha_{a b c} \Colon (a \otimes b) \otimes c \to a \otimes (b \otimes c) これは3つの引数すべてにおいて自然である必要がある。

モノイダル圏はまた、テンソル積の単位元として機能する特別な単位対象iiを、これも自然同型を除いて一意に定義する必要がある。これら2つの同型は、それぞれ左単位子・右単位子と呼ばれ、次の構成要素を持つ: λaiaaρaaia \begin{aligned} \lambda_a &\Colon i \otimes a \to a \\ \rho_a &\Colon a \otimes i \to a \end{aligned} 結合子と単位子はコヒーレンス条件を満たす必要がある:

モノイダル圏が対称 (symmetric) と呼ばれる条件は、自然同型が成分: γababba\gamma_{a b} \Colon a \otimes b \to b \otimes a を持ち、その平方が恒等射であり: γbaγab=𝐢𝐝ab\gamma_{b a} \circ \gamma_{a b} = % \mathbf{id}_{a \otimes b}% かつモノイドの構造と一致していることだ。

モノイダル圏に関して興味深いのは、内部hom(関数対象)をテンソル積に対する右随伴として定義できることだ。この関数対象、すなわち冪の標準的な定義は、右随伴を通じて圏論的な積へと向かっていたことを思い出してほしい。そのような対象が存在する圏をデカルト閉圏と呼んだ。モノイダル圏の内部homを定義する随伴は次のとおりだ: 𝐕(ab,c)𝐕(a,[b,c])\mathbf{V}(a \otimes b, c) \sim \mathbf{V}(a, [b, c]) G. M. Kelly112にならって、内部homを[b,c]{[}b, c{]}と表記しよう。この随伴の余単位は自然変換であり、その成分は評価射 (evaluation morphism) と呼ばれる: εab([a,b]a)b\varepsilon_{a b} \Colon ([a, b] \otimes a) \to b テンソル積が非対称なら、次の随伴を使って、[[a,c]]{[}{[}a, c{]}{]}で示される別の内部homを定義できることに注目してほしい: 𝐕(ab,c)𝐕(b,[[a,c]])\mathbf{V}(a \otimes b, c) \sim \mathbf{V}(b, [[a, c]]) 両方が定義されているモノイダル圏は双閉 (biclosed) と呼ばれる。双閉ではない圏の例としては𝐒𝐞𝐭\mathbf{Set}内の自己関手の圏があり、関手合成がテンソル積として機能する。この圏はモナドを定義するのに使った。

28.3 豊饒圏

モノイダル圏𝐕\mathbf{V}上の豊穣圏𝐂\mathbf{C}は、hom集合をhom対象で置き換える。𝐂\mathbf{C}内の対象aabbのすべてのペアに対し、𝐕\mathbf{V}内の対象𝐂(a,b)\mathbf{C}(a, b) を関連付けよう。hom集合に使ったのと同じ表記をhom対象にも使うことにする。ただし、hom対象が射を含まないことには注意が必要だ。一方で、𝐕\mathbf{V}はhom集合と射を持つ正則圏(豊穣化されていない圏)だ。したがって、集合を完全に一掃したわけではない――絨毯で覆って見えなくしただけだ。

𝐂\mathbf{C}内の個々の射については語れないため、射の合成を𝐕\mathbf{V}内の射の族に置き換える: 𝐂(b,c)𝐂(a,b)𝐂(a,c)\circ \Colon \mathbf{C}(b, c) \otimes \mathbf{C}(a, b) \to \mathbf{C}(a, c)

同様に、恒等射を𝐕\mathbf{V}内の射の族に置き換える: jai𝐂(a,a)j_a \Colon i \to \mathbf{C}(a, a) ここで、ii𝐕\mathbf{V}内のテンソル単位元だ。

合成の結合律は𝐕\mathbf{V}内の結合子で定義される:

単位律も同様に単位子として表現される:

28.4 前順序

前順序は細い圏として定義され、その中のhom集合はどれも空集合か単元集合だ。集合𝐂(a,b)\mathbf{C}(a, b) が空でないことは、aabb以下であることの証明と解釈される。そのような圏は、2つの対象0011𝐹𝑎𝑙𝑠𝑒\mathit{False}𝑇𝑟𝑢𝑒\mathit{True}とも呼ばれる)だけを含む非常に単純なモノイダル圏上の豊穣圏だと解釈できる。必須の恒等射に加えて、この圏には00から11への単一の射がある。これを010 \to 1と呼ぼう。単純なモノイドの構造をその中に確立できる。そのためには、0011の単純な算術をモデル化したテンソル積を使う(たとえば、0でない積は111 \otimes 1だけだ)。この圏の恒等対象は11だ。これは厳密なモノイダル圏であり、つまり結合子と単位子は恒等射だ。

前順序のhom集合は空集合か単元集合なので、ここでのちっぽけな圏からのhom対象に簡単に置き換えられる。豊穣化された前順序𝐂\mathbf{C}は、対象aabbの任意のペアについてのhom対象𝐂(a,b)\mathbf{C}(a, b) を持つ。この対象は、aabb以下なら11で、そうでなければ00だ。

合成を見てみよう。任意の2つの対象のテンソル積は通常は00で、両方が11のときだけ11となる。テンソル積が00の場合、合成射には2つの候補がある。𝐢𝐝0% \mathbf{id}_{0}% または010 \to 1のいずれかだ。しかし、11の場合、候補は𝐢𝐝1% \mathbf{id}_{1}% だけだ。これを再翻訳して関係に戻すと、aba \leqslant bbcb \leqslant cの場合はaca \leqslant cとなり、必要としていた推移律そのものになる。

恒等射についてはどうだろう? それは11から𝐂(a,a)\mathbf{C}(a, a) への射だ。11からの射は1つだけで、恒等射𝐢𝐝1% \mathbf{id}_{1}% だ。したがって、𝐂(a,a)\mathbf{C}(a, a)11である必要がある。これは前順序の反射律aaa \leqslant aを意味する。したがって、前順序を豊饒圏として実装すると、推移律と反射律の両方が自動的に強制される。

28.5 距離空間

ウィリアム・ローヴェア113 (William Lawvere) による興味深い例がある。ローヴェアは豊穣圏を使って距離空間 (metric space) を定義できることに気付いた。距離空間は任意の2対象間の距離を定義する。この距離は非負実数だ。取り得る値に無限大を含めると便利だ。距離が無限大の場合、始点となる対象から終点となる対象へ到達する方法はない。

距離には、満たさなければならない自明な特性がいくつかある。その1つは、ある対象からそれ自身への距離は0でなければならない、というものだ。もう1つは三角不等式で、直線距離は中継地点同士の距離の合計を超えない、というものだ。距離が対称でなくてよいのは、最初は奇妙に思えるかもしれないが、ローヴェアによる説明どおりに、ある方向へは坂を上り、逆方向へは坂を下るのを想像すれば分かる。いずれにしても、対称性は追加の制約として後から課してもよい。

さて、どうすれば距離空間を圏論の言葉で語れるだろうか? それには、hom対象が距離であるような圏を構築する必要がある。注意してほしいのは、距離は射ではなくhom対象だということだ。一体どうすればhom対象が数値になるだろう? それらの数値が対象であるようなモノイダル圏𝐕\mathbf{V}さえ構築できればよい。非負実数(と無限大)は全順序を形成するので、細い圏として扱える。このような2つの数xxyyの間の射は、xyx \geqslant yの場合だけ存在する(注:これは前順序の定義での伝統的な向きとは逆だ)。モノイドの構造は加算で与えられ、0が単位対象として機能する。言い換えると、2つの数値のテンソル積はそれらの合計だ。

距離空間は、このようなモノイダル圏上の豊穣圏だ。対象aaからbbへのhom対象𝐂(a,b)\mathbf{C}(a, b) は、非負の(無限大を取り得る)数であり、aaからbbへの距離と呼ばれる。このような圏で恒等射と合成に何ができるか見てみよう。

我々の定義では、0という数であるテンソル単位元からhom対象𝐂(a,a)\mathbf{C}(a, a) への射は次の関係になる: 0𝐂(a,a)0 \geqslant \mathbf{C}(a, a) 𝐂(a,a)\mathbf{C}(a, a) は負の数ではないため、この条件はaaからaaへの距離が常に0であることを示す。良し!

では合成について語ろう。隣接する2つのhom対象のテンソル積𝐂(b,c)𝐂(a,b)\mathbf{C}(b, c) \otimes \mathbf{C}(a, b) から始める。テンソル積は、2つの距離の合計として定義された。合成は、この積から𝐂(a,c)\mathbf{C}(a, c) への、𝐕\mathbf{V}内の射だ。𝐕\mathbf{V}内の射は「以上」の関係として定義される。言い換えると、aaからbbへの距離とbbからccへの距離の合計は、aaからccへの距離以上になる。もっとも、これは標準的な三角不等式にすぎない。良し!

距離空間を豊饒圏へと再投影することにより、三角不等式と自己距離0が「無料で」得られる。

28.6 豊穣関手

関手の定義には射の写像が含まれる。豊穣化された設定では、個々の射という概念がないため、hom対象を一括して扱う必要がある。hom対象はモノイダル圏𝐕\mathbf{V}内の対象であり、それらの間には自由に使える射がある。したがって、同一のモノイダル圏𝐕\mathbf{V}上の複数の豊穣圏について、それらの間に豊穣関手を定義することは意味がある。そうすれば、𝐕\mathbf{V}内の射を使って2つの豊穣圏の間でhom対象を写せる。

2つの圏𝐂\mathbf{C}𝐃\mathbf{D}の間の豊穣関手 (enriched functor) FFは、対象を対象に写すだけでなく、𝐂\mathbf{C}内の対象のすべてのペアに対して𝐕\mathbf{V}内の射を割り当てる: Fab𝐂(a,b)𝐃(Fa,Fb)F_{a b} \Colon \mathbf{C}(a, b) \to \mathbf{D}(F a, F b) 関手は構造を保持する写像だ。それは通常の関手では合成と恒等射を保存することを意味した。豊穣化された設定においては、合成の保存とは次の図式が可換であることを意味する:

恒等射の保存は、恒等射を「選択」するような𝐕\mathbf{V}内の射の保存に置き換えられる:

28.7 自己豊穣化

閉じた対称モノイダル圏は、hom集合を内部hom(定義は上記を参照)に置き換えることによって自己豊穣化できる。そのためには、内部homについての合成律を定義する必要がある。言い換えると、次のシグネチャーを持つ射を実装する必要がある: [b,c][a,b][a,c][b, c] \otimes [a, b] \to [a, c] これはプログラミングでの他のタスクと大差ない。ただし、圏論では通常、ポイントフリー実装を使う。まず、これを要素とするような集合を指定する。ここでは、次のhom集合の元だ: 𝐕([b,c][a,b],[a,c])\mathbf{V}([b, c] \otimes [a, b], [a, c]) このhom集合は次と同型だ: 𝐕(([b,c][a,b])a,c)\mathbf{V}(([b, c] \otimes [a, b]) \otimes a, c) ここで、内部hom[a,c]{[}a, c{]}の定義で出てきた随伴を使った。この新しい集合に射を構築できるなら、随伴はもとの集合の射を指し、そして、その射は合成として使える。その射は、自由に使えるいくつかの射を合成することによって構築される。まず、結合子α[b,c][a,b]a\alpha_{[b, c]\ [a, b]\ a}を使って左側の式を再結合できる: ([b,c][a,b])a[b,c]([a,b]a)([b, c] \otimes [a, b]) \otimes a \to [b, c] \otimes ([a, b] \otimes a) 続けて、随伴の余単位εab\varepsilon_{a b}を使える: [b,c]([a,b]a)[b,c]b[b, c] \otimes ([a, b] \otimes a) \to [b, c] \otimes b もう一度余単位εbc\varepsilon_{b c}を使うとccが得られる。以上より射が構築できた: εbc.(𝐢𝐝[b,c]εab).α[b,c][a,b]a\varepsilon_{b c}\ .\ (% \mathbf{id}_{{[b, c]}}% \otimes \varepsilon_{a b})\ .\ \alpha_{[b, c] [a, b] a} これは次のhom集合の要素だ: 𝐕(([b,c][a,b])a,c)\mathbf{V}(([b, c] \otimes [a, b]) \otimes a, c) 随伴によって、目指していた合成律が得られるだろう。

同様に、恒等射: jai[a,a]j_a \Colon i \to [a, a] は次のhom集合の元だ: 𝐕(i,[a,a])\mathbf{V}(i, [a, a]) これは、随伴を通じて、次と同型だ: 𝐕(ia,a)\mathbf{V}(i \otimes a, a) このhom集合は左単位子λa\lambda_aを含むことが分かっている。jaj_aは随伴の下の像として定義できる。

自己豊穣化の実際的な例としては、プログラミング言語における型のプロトタイプとして機能する圏𝐒𝐞𝐭\mathbf{Set}が挙げられる。以前、それがデカルト積ついてのモノイダル閉圏であることを見た。𝐒𝐞𝐭\mathbf{Set}では、任意の2集合間のhom集合はそれ自体が集合であるため、𝐒𝐞𝐭\mathbf{Set}内の対象になる。それが冪集合と同型であることは分かっているので、外部homと内部homは等価となる。さらに、自己豊穣化を通じて、冪集合をhom対象として使えて合成を冪対象のデカルト積として表現できるのも分かった。

28.8 2-圏との関係

𝟐\mathbf{2}-圏については、(小さい)圏の圏である𝐂𝐚𝐭\mathbf{Cat}の文脈において説明した。圏の間の射は関手だが、追加の構造がある:関手間の自然変換だ。𝟐\mathbf{2}-圏では、対象は00-セル、射は11-セル、射間の射は22-セルと呼ばれることが多い。𝐂𝐚𝐭\mathbf{Cat}では、00-セルは圏、11-セルは関手、22-セルは自然変換だ。

しかし、2つの圏の間の関手も圏を形成することに注意してほしい。したがって、𝐂𝐚𝐭\mathbf{Cat}に実際にあるのは、hom集合というよりhom圏だ。𝐒𝐞𝐭\mathbf{Set}𝐒𝐞𝐭\mathbf{Set}上の豊穣圏として扱えるのと同様に、𝐂𝐚𝐭\mathbf{Cat}𝐂𝐚𝐭\mathbf{Cat}上の豊穣圏として扱えることが知られている。さらに一般的に、すべての圏が𝐒𝐞𝐭\mathbf{Set}上の豊穣圏として扱えるのと同様に、すべての𝟐\mathbf{2}-圏は𝐂𝐚𝐭\mathbf{Cat}上の豊穣圏と見なせる。

29 トポイ

プログラミングから離れてハードコアな数学に飛び込んでしまったかもしれないのは分かっている。しかし、プログラミングにおける次の大きな革命が何をもたらすのか、理解のためにどんな数学が必要となるのかは、誰にも分からない。すでにいくつか非常に興味深いアイデアがある。連続時間114での関数型リアクティブプログラミング、Haskellの型システムの依存型による拡張、ホモトピー型理論のプログラミングにおける探究などだ。

ここまでは、値の集合を使って気楽に型を区別してきた。これは厳密には正しくない。なぜなら、このようなアプローチはある事実を考慮していないからだ。それは、プログラミングでは値を計算し、そして計算は時間のかかるプロセスであり、極端な場合には停止しない可能性がある、という事実だ。すべてのチューリング完全な言語は発散計算 (divergent computation) を含む。

また、集合論には、計算機科学の基礎として、そして数学自体の基礎としても、最適とは言い難い根本的な理由がある。良い比喩としては、集合論は特定のアーキテクチャに結び付けられたアセンブリー言語だと言える。別のアーキテクチャで計算を実行したいなら、より一般的なツールを使う必要がある。

可能性の1つは、集合の代わりに空間 (space) を使うことだ。空間は構造がより豊かで、集合を使わずに定義できる。通常、空間に関連付けられるものの1つは位相 (topology) で、連続性などを定義するために必要だ。位相への従来のアプローチは、ご想像のとおり、集合論によるものだ。特に、部分集合は位相の中心的な概念だ。驚くまでもなく、圏論の研究者たちはこの概念を𝐒𝐞𝐭\mathbf{Set}以外の圏に一般化した。集合論に代わるものとしてふさわしい性質を持つ圏の型はトポス(topos, 複数形:トポイtopoi)と呼ばれ、部分集合を一般化した概念などを提供する。

29.1 部分対象分類子

要素ではなく関数を使って部分集合の概念を表現することから始めよう。aaからbbへの任意の関数ffは、bbの部分集合――ffの下にあるaaの像――を定義する。しかし、同一の部分集合を定義する関数はたくさんある。もっと具体的にする必要がある。まず初めに、単射関数 (injective function) ――複数の要素を1つに潰さない関数に焦点を当てよう。単射関数はある集合を別の集合に「注入」する。有限集合における単射関数は、ある集合の要素を別の集合の要素に接続する平行な射として視覚化できる。当然、最初の集合を2つ目の集合より大きくすることはできない。そうでないと射は必然的に収束する。まだ曖昧さは残っている:別の集合aa'と、その集合からbbへの別の単射関数ff'があって、同じ部分集合が選ばれる可能性がある。しかし、そのような集合がaaと同型でなければならないことは簡単に納得できる。この事実を利用して、部分集合を、終域の同型によって関連付けられた単射関数の族として定義できる。より正確には、2つの単射関数: fabfab \begin{aligned} f &\Colon a \to b \\ f' &\Colon a' \to b \end{aligned} が等価になるのは、同型射: haah \Colon a \to a' が存在する場合だ。ただし: f=f.hf = f'\ .\ h このような等価な単射の族は、bbの部分集合を定義する。

この定義は、単射関数をモノ射 (monomorphism) に置き換えることで任意の圏にリフトできる。aaからbbへのモノ射mmは普遍性によって定義されることを思い出してほしい。任意の対象ccおよび任意の射のペア: gcagca \begin{aligned} g &\Colon c \to a \\ g' &\Colon c \to a \end{aligned} のうち次を満たすもの: m.g=m.gm\ .\ g = m\ .\ g' についてはg=gg = g'である必要がある。

集合においてこの定義をより理解しやすくするには、関数mmがモノ射でないことが何を意味するかを考えるとよい。その場合はaaの2つの異なる要素がbbの1つの要素に写されるだろう。そして、それら2つの要素についてのみ違いがある2つの関数gggg'を見つけられる。mmに後合成すると、その違いは見えなくなる。

部分集合を定義する別の方法もある:特性関数 (characteristic function) と呼ばれる単一の関数を使う方法だ。これは、集合bbから二元集合Ω\Omegaへの関数χ\chiだ。二元集合の片方の要素には “true” が、もう片方には “false” が指定されている。この関数は、bbの要素のうち部分集合の元である要素に “true” を割り当て、そうでない要素に “false” を割り当てる。

Ω\Omegaの要素を “true” に指定することの意味を明確にする必要が残る。ここで標準的なトリックが使える:単元集合からΩ\Omegaへの関数を使えばよい。この関数を𝑡𝑟𝑢𝑒\mathit{true}と呼ぼう: 𝑡𝑟𝑢𝑒1Ω\mathit{true} \Colon 1 \to \Omega

これらの定義の組み合わせ方によっては、部分対象 (subobject) が何であるかだけでなく、特別な対象Ω\Omegaについても要素に触れずに定義できる。ここでのアイデアは、射𝑡𝑟𝑢𝑒\mathit{true}が「総称」部分対象を表すようにしたい、というものだ。𝐒𝐞𝐭\mathbf{Set}では、これは2要素の集合Ω\Omegaから1要素の部分集合を選ぶ。これは最大限に総称的だ。ここで選ばれたものがれっきとした部分集合なのは明らかだ。なぜなら、この部分集合にはない要素がΩ\Omegaにはもう1つあるからだ。

より一般的な条件では、𝑡𝑟𝑢𝑒\mathit{true}を終対象から分類対象 (classifying object) Ω\Omegaへのモノ射として定義する。ただし、分類対象の定義が必要になる。つまり、この対象を特性関数に結びつける普遍性が必要だ。𝐒𝐞𝐭\mathbf{Set}では、特性関数χ\chiに沿った𝑡𝑟𝑢𝑒\mathit{true}の引き戻しが、部分集合aaとそれをbbに埋め込む単射関数の両方を定義することが知られている。その引き戻し図式を以下に示す:

この図式を分析してみよう。引き戻しの式は次のとおりだ: 𝑡𝑟𝑢𝑒.𝑢𝑛𝑖𝑡=χ.f\mathit{true}\ .\ \mathit{unit} = \chi\ .\ f 関数𝑡𝑟𝑢𝑒.𝑢𝑛𝑖𝑡\mathit{true}\ .\ \mathit{unit}aaのすべての要素を “true” に写す。したがって、ffaaのすべての要素を、bbの要素のうちχ\chiが “true” であるものに写す必要がある。定義より、これらは特性関数χ\chiによって指定される部分集合の要素だ。つまり、ffの像はまさに求めていた部分集合だ。引き戻しの普遍性によってffは単射だと保証される。

この引き戻し図式は𝐒𝐞𝐭\mathbf{Set}以外の圏の分類対象を定義するのに使える。そのような圏には終対象が必要で、それによってモノ射𝑡𝑟𝑢𝑒\mathit{true}を定義できる。また、引き戻しも必要だ――実際の要件は、すべての有限の極限を持つことだ(引き戻しは有限の極限の一例だ)。これらの仮定の下で、分類対象Ω\Omegaを、すべてのモノ射ffについて引き戻し図式を完成させる一意な射χ\chiが存在する、という特性によって定義する。

この特性を分析しよう。引き戻しを構築するとき、3つの対象Ω\Omegabb11と、2つの射𝑡𝑟𝑢𝑒\mathit{true}χ\chiが与えられる。引き戻しの存在は、図式を可換にするような2つの射ff𝑢𝑛𝑖𝑡\mathit{unit}(後者は終対象の定義によって一意に決定される)を伴った最適な対象aaを見つけられることを意味する。

ここでは連立微分方程式を解いていることになる。つまり、aabbの両方を変化させることでΩ\Omega𝑡𝑟𝑢𝑒\mathit{true}について解いている。与えられたaaおよびbbについて、モノ射fabf \Colon a \to bがある場合とない場合がある。だが、もしあるなら、何らかのχ\chiの引き戻しにしたい。さらに、このχ\chiffによって一意に決定されるようにしたい。

モノ射ffと特性関数χ\chiの間に1対1の対応関係があるとは言えない。なぜなら、引き戻しは同型を除いて一意なだけだからだ。しかし、部分集合を等価な単射の族として以前定義したのを思い出してほしい。これを一般化するには、bbの部分対象を、bbへの等価なモノ射の族として定義すればよい。このモノ射の族は、先ほどの図式における等価な引き戻しの族と1対1で対応している。

したがって、bbの部分対象の集合𝑆𝑢𝑏(b)\mathit{Sub}(b) をモノ射の族として定義でき、それがbbからΩ\Omegaへの射の集合と同型だと分かる: 𝑆𝑢𝑏(b)𝐂(b,Ω)\mathit{Sub}(b) \cong \mathbf{C}(b, \Omega) 偶然にも、これは2つの関手の自然同型だ。言い換えれば、𝑆𝑢𝑏()\mathit{Sub}(-) は表現可能(反変)関手であり、その表現は対象Ω\Omegaだ。

29.2 トポス

トポスは次のような圏だ:

  1. デカルト閉である:積・終対象・(積の右随伴として定義される)冪のすべてを持つ。
  2. すべての有限図式について極限を持つ。
  3. 部分対象分類子Ω\Omegaがある。

これらの特性によって、トポスはほとんどの用途で𝐒𝐞𝐭\mathbf{Set}の代わりになる。さらに、定義から導かれる追加の特性もある。たとえば、トポスは始対象も含めてすべての有限の余極限を持つ。

部分対象分類子を終対象2つの余積(和)として――𝐒𝐞𝐭\mathbf{Set}におけるのと同様に――定義するのは魅力的だが、もっと一般化したい。これが当てはまるトポイはブーリアンと呼ばれる。

29.3 トポイと論理

集合論では、特性関数は、集合の要素の特性――述語 (predicate) を定義するものと解釈されうる。述語は、一部の要素に対しては真となり、他の要素に対しては偽となる。述語𝑖𝑠𝐸𝑣𝑒𝑛\mathit{isEven}は自然数の集合から偶数の部分集合を選択する。トポスでは、述語の概念を対象aaからΩ\Omegaへの射に一般化できる。Ω\Omegaが真偽対象 (truth object) と呼ばれることがあるのはこのためだ。

述語は論理の構成要素だ。トポスには、論理を研究するのに必要な全装備が揃っている。積は連言(conjunction, 論理)に対応し、余積は選言(disjunction, 論理)に対応し、冪は含意 (implication) に対応する。排中律(または、等価なものとして、二重否定除去)を除く論理学の標準的な公理すべてがトポスに含まれている。これが、トポスの論理が構成的論理 (constructive logic) あるいは直観主義論理 (intuitionistic logic) に対応する理由だ。

直観主義論理は計算機科学から予想外に支持され、着実に定着していっている。排中律という古典的な概念は、絶対的な真実があるという信念に基づいている。つまり、すべての記述は真か偽のいずれかであり、古代ローマ人が言ったように、tertium non datur(第3の道はない)ということだ。しかし、何かの真偽を知るための方法は、それを証明または反証できるかどうかだけだ。証明とはプロセスであり、計算だ――そして、知ってのとおり、計算には時間とリソースがかかる。場合によっては、停止しないこともある。有限の時間で証明できない命題を真だと主張するのは無意味だ。トポスにはより繊細な真偽対象があるので、より一般的な枠組みを提供して興味深い論理をモデル化できる。

29.4 課題

  1. 特性関数に沿った𝑡𝑟𝑢𝑒\mathit{true}の引き戻しである関数ffは単射でなければならないことを示せ。

30 ローヴェア・セオリー

最近では、モナドに言及せずには関数プログラミングについて語れない。しかし、並行宇宙においては、エウジニオ・モッジがモナドではなくローヴェアのセオリー (Lawvere theory) に着目した可能性もあっただろう。そのような宇宙を探検しよう。

30.1 普遍代数

代数を様々な抽象レベルで記述する方法はたくさんある。モノイド・群・環などを記述するための汎用言語を見つけたい。最も単純なレベルでは、これらのすべての構造によって、集合の要素に対する演算と、それらの演算によって満たされなければならないいくつかの規則が定義される。たとえば、モノイドは結合律を満たす二項演算によって定義できる。単位元と単位律もある。しかし、少し想像力を働かせれば、単位元は零項演算――引数を取らず集合の特別な要素を返す操作――に変えられる。群について述べたければ、受け取った要素の逆を返す単項演算子を追加する。それに対応する左逆元律と右逆元律がある。環では2つの二項演算子とさらにいくつかの規則が定義される。以下同様だ。

大枠としては、代数はさまざまな値のnnについてのnn項演算の集合と、恒等式の集合によって定義される。これらの恒等式はすべて全称量化されている。たとえば、結合律の式はすべての可能な3要素の組み合わせについて満たされる必要がある、などだ。

ちなみに、0(加算での単位元)が乗算での逆を持たないという単純な理由から、体 (field) は考慮から除外される。体の逆元律は全称量化できない。

この普遍代数の定義は、演算(関数)を射に置き換えると、𝐒𝐞𝐭\mathbf{Set}以外の圏にも拡張できる。集合の代わりに、(総称対象と呼ばれる)対象aaを選ぶ。単項演算はaaの準同型にすぎない。しかし、それ以外のアリティ(arity, 演算の引数の数)についてはどうだろう? 二項演算(アリティ2)は、積a×aa \times{}aからaaへ戻る射として定義できる。一般のnn項演算は、aann乗からaaへの射だ: αnana\alpha_n \Colon a^n \to a 零項演算は、終対象(aaの0乗)からの射だ。したがって、あらゆる代数を定義するために必要なのはある圏だけで、その圏の対象は1つの特別な対象aaの冪だ。特定の代数は、この圏のhom集合として表されている。これがローヴェア・セオリーの概略だ。

ローヴェア・セオリーの導出は多くのステップを経るため、道順を示しておく:

  1. 有限集合の圏𝐅𝐢𝐧𝐒𝐞𝐭\mathbf{FinSet}
  2. そのスケルトン𝐅\mathbf{F}
  3. その反対圏𝐅𝑜𝑝\mathbf{F}^\mathit{op}
  4. ローヴェア・セオリー𝐋\mathbf{L}: 圏𝐋𝐚𝐰\mathbf{Law}内のある対象。
  5. ローヴェア圏のモデルMM: 圏𝐌𝐨𝐝(𝐋,𝐒𝐞𝐭)\mathbf{Mod}(\mathbf{L},\mathbf{Set}) 内のある対象。

30.2 ローヴェア・セオリー

複数あるローヴェア・セオリーはすべて共通のバックボーンを共有している。ローヴェア・セオリーにおけるすべての対象は、ただ1つの対象から積を使って生成される(実際に、ただの冪だ)。しかし、一般の圏では、これらの積をどのように定義するのだろうか? より単純な圏からの写像を使って積を定義できることが知られている。実際には、より単純なこの圏は積ではなく余積を定義することもあり、その場合は反変関手を使ってそれらを行き先の圏に埋め込む。反変関手は、余積を積に変換し、単射を射影に変換する。

ローヴェア圏のバックボーンとして自然な選択肢は、有限集合の圏𝐅𝐢𝐧𝐒𝐞𝐭\mathbf{FinSet}だ。これは空集合\varnothing、単元集合11、二元集合22などを含む。この圏の対象はすべて、余積を使って単元集合から生成できる(空集合は零項余積の特殊なケースとして扱う)。たとえば、二元集合は2つの単元集合の和2=1+12 = 1 + 1だ。Haskellでも次のように表現される:

    type Two = Either () ()

しかし、空集合は1つしかないと考えるのが自然だとしても、単元集合は異なるものが多数あり得る。典型的な例として、集合1+1 + \varnothingと集合+1\varnothing + 111はどれも同型であるにもかかわらず、それぞれ異なる。集合の圏の余積は結合律を満たさない。この状況を解決するには、すべての同型集合を同一視する圏を構成すればよい。そのような圏はスケルトン (skeleton) と呼ばれる。言い換えれば、すべてのローヴェア・セオリーのバックボーンは𝐅𝐢𝐧𝐒𝐞𝐭\mathbf{FinSet}のスケルトン𝐅\mathbf{F}だ。この圏の対象は、𝐅𝐢𝐧𝐒𝐞𝐭\mathbf{FinSet}の要素数に対応した(0を含む)自然数と同一視できる。余積は加算の役割を果たす。𝐅\mathbf{F}内の射は有限集合間の関数に対応する。例として、\varnothingからnnへの(空集合を始対象とする)一意な射があり、nnから\varnothingへの射は(\varnothing \to \varnothingを除いて)なく、11からnnへの射(単射)はnnあり、nnから11への射は1つある、などが挙げられる。ここで、nn𝐅\mathbf{F}内のある対象を表し、同型によって同一視された𝐅𝐢𝐧𝐒𝐞𝐭\mathbf{FinSet}内のすべてのnn要素集合に対応する。

𝐅\mathbf{F}を使うと、ローヴェア・セオリーを、次の特別な関手を伴う圏𝐋\mathbf{L}として形式的に定義できる: I𝐋𝐅𝑜𝑝𝐋I_{\mathbf{L}} \Colon \mathbf{F}^\mathit{op}\to \mathbf{L} この関手は対象の全単射である必要があり、有限積を保存する必要がある(𝐅𝑜𝑝\mathbf{F}^\mathit{op}内の積と𝐅\mathbf{F}内の余積は同じだ): I𝐋(m×n)=I𝐋m×I𝐋nI_{\mathbf{L}} (m\times{}n) = I_{\mathbf{L}} m\times{}I_{\mathbf{L}} n この関手は対象における恒等関係と見なされることがある。𝐅\mathbf{F}内と𝐋\mathbf{L}内とで対象が同じであることを表すからだ。そこで、これらに同じ名前を使うことにし、自然数で表そう。ただし、𝐅\mathbf{F}内の対象は集合と同じではないことに注意してほしい(それらは同型集合のクラスだ)。

一般に、𝐋\mathbf{L}内のhom集合は、𝐅𝑜𝑝\mathbf{F}^\mathit{op}内のhom集合よりも豊穣だ。これらは𝐅𝐢𝐧𝐒𝐞𝐭\mathbf{FinSet}内の関数に対応する射(基本的な積操作 (basic product operation) とも呼ばれる)以外の射も含むことがある。ローヴェア・セオリーの等式的規則はそれらの射として表されている。

鍵となる観察は、𝐅\mathbf{F}内の単元集合11𝐋\mathbf{L}内で同じく11と呼ばれる対象に写され、𝐋\mathbf{L}内の他のすべての対象が自動的にこの対象の冪になることだ。たとえば、𝐅\mathbf{F}内の二元集合22は余積1+11+1であるため、𝐋\mathbf{L}内の積1×11 \times 1(または121^2)に写される必要がある。この意味で、圏𝐅\mathbf{F}𝐋\mathbf{L}の対数のように振る舞う。

𝐋\mathbf{L}内の射には、関手I𝐋I_{\mathbf{L}}によって𝐅\mathbf{F}から転送されたものもある。それらは𝐋\mathbf{L}で構造としての役割を果たす。特に、余積単射iki_kは積射影pkp_kになる。直観として有用なのは、射影: pk1n1p_k \Colon 1^n \to 1 を、nn個の変数のうち第kk番目以外のすべてを無視するような関数のプロトタイプとして想像することだ。逆に、𝐅\mathbf{F}内の定常射n1n \to 1𝐋\mathbf{L}内では対角射11n1 \to 1^nになる。これらは変数の複製に相当する。

𝐋\mathbf{L}内の射のうちで興味深いのは、射影以外のnn項演算を定義するものだ。それらの射によって、あるローヴェア・セオリーと別のものとが区別される。それらは乗算・加算・単位元の選択などで、代数を定義する。しかし、𝐋\mathbf{L}を完全な圏にするためには、複合操作nmn \to m(あるいは、等価なものとして、1n1m1^n \to 1^m)も必要だ。圏の構造が単純なため、これらは型n1n \to 1のより単純な射の積だと分かる。これは、積を返す関数は関数の積である(あるいは、前に見たように、hom関手は連続である)という言明を一般化したものだ。

ローヴェア・セオリー\mathbf{L}は\mathbf{F}^\mathit{op}に基づいており、積を定義する「退屈な」射はそこから継承されている。それはn項操作(点線の矢印)を記述するような「興味深い」射を追加する。

ローヴェア・セオリーは圏𝐋𝐚𝐰\mathbf{Law}を形成する。この圏の射は、有限積を保存し関手IIと可換な関手だ。そのような2つのセオリー (𝐋,I𝐋)(\mathbf{L}, I_{\mathbf{L}})(𝐋,I𝐋)(\mathbf{L'}, I'_{\mathbf{L'}}) が与えられたとき、それらの間の射は以下を満たす関手F𝐋𝐋F \Colon \mathbf{L} \to \mathbf{L'}となる: F(m×n)=Fm×FnFI𝐋=I𝐋 \begin{gathered} F (m \times n) = F m \times F n \\ F \circ I_{\mathbf{L}} = I'_{\mathbf{L'}} \end{gathered} ローヴェア・セオリーの間の射は、あるセオリーを別のセオリーの中で解釈するという概念を要約したものだ。たとえば、群の乗算は逆を無視すればモノイドの乗算として解釈できる。

ローヴェア圏の最も単純で自明な例は、𝐅𝑜𝑝\mathbf{F}^\mathit{op}自体だ(これはI𝐋I_{\mathbf{L}}についての恒等関手の選択に対応する)。演算や規則を持たないこのローヴェア・セオリーは、たまたま𝐋𝐚𝐰\mathbf{Law}の始対象でもある。

この時点でローヴェア・セオリーの自明でない例を示せば非常に有益だろうが、モデルとは何かを先に理解しておかなければ、説明が困難だ。

30.3 ローヴェア・セオリーにおけるモデル

ローヴェア・セオリーを理解するための鍵は、ある1つのセオリーは同じ構造を共有する多くの個別の代数を一般化している、と理解することだ。たとえば、モノイドのローヴェア・セオリーは、モノイドであることの本質を説明している。それはすべてのモノイドで有効である必要がある。特定のモノイドはそのようなセオリーのモデルとなる。あるモデルは、ローヴェア・セオリー𝐋\mathbf{L}から集合の圏𝐒𝐞𝐭\mathbf{Set}への関手として定義される。(モデルに他の圏を使う一般化されたローヴェア・セオリーもあるが、ここでは𝐒𝐞𝐭\mathbf{Set}だけに集中する。) 𝐋\mathbf{L}の構造は積に大きく依存するため、そのような関手は有限積を保存する必要がある。𝐋\mathbf{L}のモデルは、ローヴェア・セオリー𝐋\mathbf{L}の上の代数とも呼ばれ、次の関手によって定義される: M𝐋𝐒𝐞𝐭M(a×b)Ma×Mb \begin{gathered} M \Colon \mathbf{L} \to \mathbf{Set}\\ M (a \times b) \cong M a \times M b \end{gathered} 積の保存は同型を除いてのみ必要であることに注意してほしい。積を厳密に保存するとほとんどの興味深いセオリーが棄却されるので、これは非常に重要だ。

モデルによる積の保存とは、𝐒𝐞𝐭\mathbf{Set}内のMMの像が、集合M1M 1――𝐋\mathbf{L}からの対象11の像――の冪によって生成された一連の集合であることを意味する。この集合をaaと呼ぼう。(この集合はソート (sort) と呼ばれることがあり、このような代数はシングルソート (single-sorted) と呼ばれる。ローヴェア・セオリーのマルチソート代数 (multi-sorted algebra) への一般化も存在する。) 特に、𝐋\mathbf{L}からの二項演算は次の関数に写される: a×aaa \times a \to a あらゆる関手でそうであるように、𝐋\mathbf{L}内の複数の射が𝐒𝐞𝐭\mathbf{Set}内の同じ関数に潰されることがある。

ちなみに、すべての規則は全称量化された等しさであるという事実は、すべてのローヴェア・セオリーが自明なモデルを持つことを意味する。その自明なモデルは定関手で、すべての対象を単元集合へ、すべての射をその上の恒等関数へ写す。

𝐋\mathbf{L}内でmnm \to nという形式をとる一般の射は、次の関数に写される: amana^m \to a^n 2つの異なるモデルMMNNがある場合、それらの間の自然変換はnnで添字付けされた関数の族になる: μnMnNn\mu_n \Colon M n \to N n または、等価なものとして: μnanbn\mu_n \Colon a^n \to b^n ここで、b=N1b = N 1だ。

自然性条件によってnn項演算の保存が保証されることに注意してほしい: Nfμn=μ1MfN f \circ \mu_n = \mu_1 \circ M f ここで、fn1f \Colon n \to 1𝐋\mathbf{L}におけるnn項演算だ。

モデルを定義する関数はモデルの圏𝐌𝐨𝐝(𝐋,𝐒𝐞𝐭)\mathbf{Mod}(\mathbf{L}, \mathbf{Set}) を形成し、自然変換が射となる。

自明なローヴェア圏𝐅𝑜𝑝\mathbf{F}^\mathit{op}のモデルを考えてみよう。そのようなモデルは、11での値M1M 1によって完全に決定される。M1M 1は任意の集合でよいため、𝐒𝐞𝐭\mathbf{Set}が含む集合と同じ数のモデルがある。さらに、𝐌𝐨𝐝(𝐅𝑜𝑝,𝐒𝐞𝐭)\mathbf{Mod}(\mathbf{F}^\mathit{op}, \mathbf{Set}) 内の射(関数MMNNの間の自然変換)はどれもM1M 1における成分によって一意に決定される。逆に、関数M1N1M 1 \to N 1はどれも2つのモデルMMNNの間に自然変換を引き起こす。したがって、𝐌𝐨𝐝(𝐅𝑜𝑝,𝐒𝐞𝐭)\mathbf{Mod}(\mathbf{F}^\mathit{op}, \mathbf{Set})𝐒𝐞𝐭\mathbf{Set}と同値だ。

30.4 モノイドのセオリー

ローヴェア・セオリーの最も単純で重要な例は、モノイドの構造を記述することだ。それは、すべての可能なモノイドの構造を抽出した単一のセオリーだ。つまり、そのモデルはモノイドの圏𝐌𝐨𝐧\mathbf{Mon}全体に及ぶ。すでに述べた普遍的構成は、射の部分集合を同一視すれば、すべてのモノイドが適切な自由モノイドから得られることを示していた。したがって、1つの自由モノイドですべてのモノイドがすでに一般化されている。しかし、自由モノイドは無数に存在する。モノイドに対するローヴェア・セオリー𝐋𝐌𝐨𝐧\mathbf{L}_{\mathbf{Mon}}は、それらすべてを1つのエレガントな構成にまとめる。

すべてのモノイドは単位元を持つので、𝐋𝐌𝐨𝐧\mathbf{L}_{\mathbf{Mon}}内には00から11への特別な射η\etaが必要だ。これに対応する射は𝐅\mathbf{F}内には存在しないことに注意してほしい。仮にそのような射が存在するなら、逆向きの11から00への射になるので、𝐅𝐢𝐧𝐒𝐞𝐭\mathbf{FinSet}内では単元集合から空集合への関数になるだろう。しかし、そのような関数は存在しない。

次に、射212 \to 1を考える。これは𝐋𝐌𝐨𝐧(2,1)\mathbf{L}_{\mathbf{Mon}}(2, 1) のメンバーで、すべての二項演算のプロトタイプを含む必要がある。𝐌𝐨𝐝(𝐋𝐌𝐨𝐧,𝐒𝐞𝐭)\mathbf{Mod}(\mathbf{L}_{\mathbf{Mon}}, \mathbf{Set}) でモデルを構成するとき、これらの射はデカルト積M1×M1M 1 \times M 1からM1M 1への関数に写される。言い換えると、引数2つを取る関数になる。

問題は、モノイダル演算子のみを使って何種類の2引数関数を実装できるかだ。2つの引数をaabbと呼ぼう。両方の引数を無視してモノイダル単位元を返す関数が1つある。それから、それぞれaabbを返す2つの射影がある。さらに、ab,ba,aa,bb,aab,ab, ba, aa, bb, aab, \ldotsなどを返す関数が続く。実際には、このような2引数関数は、生成元aabbを伴う自由モノイドの要素と同じ数だけ存在する。𝐋𝐌𝐨𝐧(2,1)\mathbf{L}_{\mathbf{Mon}}(2, 1) はモデルの1つが自由モノイドであるため、これらすべての射を含む必要があることに注意してほしい。自由モノイドでは、これらは別々の関数に対応する。他のモデルでは𝐋𝐌𝐨𝐧(2,1)\mathbf{L}_{\mathbf{Mon}}(2, 1) の複数の射が1つの関数へと潰される場合があるが、自由モノイドではそうならない。

nn個の生成元n*n^*を使って自由モノイドを表せば、hom集合𝐋(2,1)\mathbf{L}(2, 1) をモノイドの圏𝐌𝐨𝐧\mathbf{Mon}内のhom集合𝐌𝐨𝐧(1*,2*)\mathbf{Mon}(1^*, 2^*) と同一視できる。一般に、𝐋𝐌𝐨𝐧(m,n)\mathbf{L}_{\mathbf{Mon}}(m, n)𝐌𝐨𝐧(n*,m*)\mathbf{Mon}(n^*, m^*) として選ぶ。言い換えると、圏𝐋𝐌𝐨𝐧\mathbf{L}_{\mathbf{Mon}}は自由モノイドの圏の逆だ。

モノイドに関するローヴェア・セオリーのモデルの圏𝐌𝐨𝐝(𝐋𝐌𝐨𝐧,𝐒𝐞𝐭)\mathbf{Mod}(\mathbf{L}_{\mathbf{Mon}}, \mathbf{Set}) は、モノイドすべての圏𝐌𝐨𝐧\mathbf{Mon}と等価だ。

30.5 ローヴェア・セオリーとモナド

覚えていると思うが、代数のセオリーはモナドを使って――典型的にはモナドの代数で――記述できる。ローヴェア・セオリーとモナドがつながっているのは驚くべきことではない。

まず、ローヴェア・セオリーからモナドがどのように導出されるかを見てみよう。導出は忘却関手と自由関手の間の随伴を経る。忘却関手UUは、ある集合を各モデルに割り当てる。その集合は、𝐌𝐨𝐝(𝐋,𝐒𝐞𝐭)\mathbf{Mod}(\mathbf{L}, \mathbf{Set}) からの関手MMを、𝐋\mathbf{L}内の対象11において評価することで得られる。

UUを導出するもう1つの方法は、𝐅𝑜𝑝\mathbf{F}^\mathit{op}𝐋𝐚𝐰\mathbf{Law}の始対象であるという事実を利用することだ。これは、すべてのローヴェア・セオリー𝐋\mathbf{L}について、一意な関手𝐅𝑜𝑝𝐋\mathbf{F}^\mathit{op}\to \mathbf{L}があることを意味する。この関手は、モデル上に逆向きの関手を引き起こす(モデルはセオリーから集合への関手だからだ): 𝐌𝐨𝐝(𝐋,𝐒𝐞𝐭)𝐌𝐨𝐝(𝐅𝑜𝑝,𝐒𝐞𝐭)\mathbf{Mod}(\mathbf{L}, \mathbf{Set}) \to \mathbf{Mod}(\mathbf{F}^\mathit{op}, \mathbf{Set}) ただし、すでに議論したように、𝐅𝑜𝑝\mathbf{F}^\mathit{op}のモデルの圏は𝐒𝐞𝐭\mathbf{Set}と同値だ。したがって、次の忘却関手が得られる: U𝐌𝐨𝐝(𝐋,𝐒𝐞𝐭)𝐒𝐞𝐭U \Colon \mathbf{Mod}(\mathbf{L}, \mathbf{Set}) \to \mathbf{Set} このように定義されたUUは左随伴としての自由関手FFを常に持つことが示せる。

このことは有限集合の場合には簡単に分かる。自由関手FFは自由代数を生成する。自由代数は𝐌𝐨𝐝(𝐋,𝐒𝐞𝐭)\mathbf{Mod}(\mathbf{L}, \mathbf{Set}) 内の特定のモデルであり、生成元の有限集合nnから生成される。FFは表現可能関手として実装できる: 𝐋(n,)𝐋𝐒𝐞𝐭\mathbf{L}(n, -) \Colon \mathbf{L} \to \mathbf{Set} これが本当に自由であることを示すには、忘却関手への左随伴であることさえ証明すればよい: 𝐌𝐨𝐝(𝐋,𝐒𝐞𝐭)(𝐋(n,),M)𝐒𝐞𝐭(n,U(M))\mathbf{Mod}(\mathbf{L}, \mathbf{Set})(\mathbf{L}(n, -), M) \cong \mathbf{Set}(n, U(M)) 右辺を単純化しよう: 𝐒𝐞𝐭(n,U(M))𝐒𝐞𝐭(n,M1)(M1)nMn\mathbf{Set}(n, U(M)) \cong \mathbf{Set}(n, M 1) \cong (M 1)^n \cong M n (射の集合が冪と同型であり、ここでは単に積の反復であるという事実を使った。) 随伴は米田の補題の結果だ: [𝐋,𝐒𝐞𝐭](𝐋(n,),M)Mn[\mathbf{L}, \mathbf{Set}](\mathbf{L}(n, -), M) \cong M n まとめると、忘却関手と自由関手によって𝐒𝐞𝐭\mathbf{Set}上のモナドT=UFT = U \circ Fが定義される。したがって、すべてのローヴェア・セオリーはモナドを生成する。

モデルに対するモナドの代数の圏はモデルの圏と同値だと分かる。

モナドを使って形成された式の評価方法はモナド代数によって定義されるのを思い出しただろう。ローヴェア・セオリーは、式の生成に使えるn項演算を定義する。モデルは、それらの式を評価する手段を提供する。

しかし、モナドとローヴェア・セオリーは双方向の関係にはない。有限的モナド (finitary monad) からのみローヴェア・セオリーが導出される。有限的モナドは有限的関手に基づく。𝐒𝐞𝐭\mathbf{Set}上のある有限的関手は、有限集合への作用によって完全に決定される。任意の集合aaへの作用は、次のコエンドを使って評価できる: Fa=nan×(Fn)F a = \int^n a^n \times (F n) コエンドは余積すなわち和の一般化なので、この式は冪級数展開の一般化になる。あるいは、関手は一般化したコンテナーだ、という直観に頼ってもよい。その場合、aaの有限的コンテナーは、形と内容の和として記述できる。ここで、FnF nnn個の要素を格納するための形の集合だ。また、内容はnn要素の組であり、それ自体がana^nの要素だ。たとえば、(関手としての)リストは有限的であり、アリティごとに1つの形を持つ。木はアリティごとにより多くの形を持つ、なども挙げられる。

まず第一に、ローヴェア・セオリーから生成されたすべてのモナドは有限的であり、コエンドとして表せる: T𝐋a=nan×𝐋(n,1)T_{\mathbf{L}} a = \int^n a^n \times \mathbf{L}(n, 1) 逆に、𝐒𝐞𝐭\mathbf{Set}上の任意の有限的モナドTTに対してローヴェア・セオリーを構築できる。まず、TTについてのクライスリ圏を構築する。覚えていると思うが、クライスリ圏内でのaaからbbへの射は台となる圏内での射によって与えられる: aTba \to T b 有限集合に限定すると、これは次のようになる: mTnm \to T n このクライスリ圏の反対圏𝐊𝐥T𝑜𝑝\mathbf{Kl}^\mathit{op}_{T}は、有限集合に限定されており、求めていたローヴェア・セオリーだ。典型的には、𝐋\mathbf{L}内のn項演算を記述するhom集合𝐋(n,1)\mathbf{L}(n, 1) はhom集合𝐊𝐥T(1,n)\mathbf{Kl}_{T}(1, n) によって与えられる。

プログラミングで遭遇するほとんどのモナドは有限的モナドであることが知られている。ただし、継続モナドという注目すべき例外は除く。また、ローヴェア・セオリーの概念は有限的演算を超えて拡張もできる。

30.6 コエンドとしてのモナド

コエンドの式について詳しく見ていこう。 T𝐋a=nan×𝐋(n,1)T_{\mathbf{L}} a = \int^n a^n \times \mathbf{L}(n, 1) まず初めに、このコエンドは、𝐅\mathbf{F}内で次のように定義されたプロ関手PPに引き継がれる: Pnm=an×𝐋(m,1)P n m = a^n \times \mathbf{L}(m, 1) このプロ関手は最初の引数nnについて反変だ。これが射をどうリフトするかを考えよう。𝐅𝐢𝐧𝐒𝐞𝐭\mathbf{FinSet}内の射は有限集合の写像fmnf \Colon m \to nだ。このような写像は、nn元集合1つからmm個の要素を(重複は許容して)選択することを表している。これは次のようにaaの冪の写像へリフトできる(方向に注意してほしい): anama^n \to a^m このリフトは単に、nn要素の組 (a1,a2,,an)(a_1, a_2, \ldots{}, a_n) からmm個の要素を(重複を許容して)選択する。

例として、fk1nf_k \Colon 1 \to nを考えよう。これはnn元集合からkk番目の要素を選択したものだ。これをリフトすると、aann要素の組を取ってkk番目の要素を返す関数となる。

あるいは、fm1f \Colon m \to 1を考えよう。これはmm要素すべてを1に写す定数関数だ。これをリフトすると、aaの要素を1つ取ってmm回複製する関数となる: λx(x,x,,xm)\lambda{}x \to (\underbrace{x, x, \ldots{}, x}_{m}) いま扱っているプロ関手が2番目の引数について共変であることは、自明だとすぐには言えないことに気付いたと思う。hom関手𝐋(m,1)\mathbf{L}(m, 1) ならmmについて反変なのは確かだ。しかし、いま扱っているコエンドは圏𝐋\mathbf{L}ではなく圏𝐅\mathbf{F}のものだ。このコエンドの変数nnは有限集合(あるいはそのスケルトン)に渡る。圏𝐋\mathbf{L}𝐅\mathbf{F}の反対圏を含む。したがって、𝐅\mathbf{F}内の射mnm \to n𝐋\mathbf{L}内の𝐋(n,m)\mathbf{L}(n, m) の元だ(埋め込みは関手I𝐋I_{\mathbf{L}}から得られる)。

𝐅\mathbf{F}から𝐒𝐞𝐭\mathbf{Set}への関手としての𝐋(m,1)\mathbf{L}(m, 1) について、関手性を確認しよう。関数fmnf \Colon m \to nをリフトしたいので、𝐋(m,1)\mathbf{L}(m, 1) から𝐋(n,1)\mathbf{L}(n, 1) への関数を実装するのが目標になる。関数ffに対応して、𝐋\mathbf{L}内にnnからmmへの射が存在する(方向に注意してほしい)。この射を𝐋(m,1)\mathbf{L}(m, 1) に前合成すると𝐋(n,1)\mathbf{L}(n, 1) の部分集合が得られる。

関数1n1 \to nをリフトすれば𝐋(1,1)\mathbf{L}(1, 1) から𝐋(n,1)\mathbf{L}(n, 1) に移れることに注目してほしい。この事実は後で使う。

反変関手ana^nと共変関手𝐋(m,1)\mathbf{L}(m, 1) の積はプロ関手𝐅𝑜𝑝×𝐅𝐒𝐞𝐭\mathbf{F}^\mathit{op}\times \mathbf{F} \to \mathbf{Set}だ。コエンドをプロ関手の対角要素すべての余積(非交和)として定義できることを思い出してほしい。それらの対角要素のうちいくつかは同一視できる。この同一視は余くさび条件に対応している。

ここで、コエンドはすべてのnnに渡る集合an×𝐋(n,1)a^n \times \mathbf{L}(n, 1) の非交和として始まる。同一視はコエンドを余等化子として表現することで生成できる。ある非対角項an×𝐋(m,1)a^n \times \mathbf{L}(m, 1) から始めよう。対角項を得るには、射fmnf \Colon m \to nを積の1番目か2番目の成分に適用すればよい。そして2つの結果は同一視される。

前に示したように、f1nf \Colon 1 \to nをリフトすると以下の2つの変換が得られる: anaa^n \to a および: 𝐋(1,1)𝐋(n,1)\mathbf{L}(1, 1) \to \mathbf{L}(n, 1) したがって、an×𝐋(1,1)a^n \times \mathbf{L}(1, 1) から始めて、以下の両方に到達できる: a×𝐋(1,1)a \times \mathbf{L}(1, 1)f,𝐢𝐝\langle f, \mathbf{id}\rangleをリフトして得られ、また: an×𝐋(n,1)a^n \times \mathbf{L}(n, 1)𝐢𝐝,f\langle \mathbf{id}, f \rangleをリフトして得られる。しかし、これはan×𝐋(n,1)a^n \times \mathbf{L}(n, 1) の要素すべてがa×𝐋(1,1)a \times \mathbf{L}(1, 1) と同一視できるという意味ではない。𝐋(n,1)\mathbf{L}(n, 1) の要素すべてが𝐋(1,1)\mathbf{L}(1, 1) から到達できるわけではないからだ。リフトできるのは𝐅\mathbf{F}からの射だけなのを思い出してほしい。𝐋\mathbf{L}内の非自明なnn項演算は射f1nf \Colon 1 \to nをリフトしても構築できない。

言い換えると、同一視できるのは、基本射 (basic morphism) を適用することによって𝐋(1,1)\mathbf{L}(1, 1) から𝐋(n,1)\mathbf{L}(n, 1) に到達できるようなコエンド式のすべての加数 (addend) だけだ。これらはすべてa×𝐋(1,1)a \times \mathbf{L}(1, 1) と同値だ。基本射は𝐅\mathbf{F}内の射の像だ。

ローヴェア・セオリーの最も単純なケースである𝐅𝑜𝑝\mathbf{F}^\mathit{op}自体で、これがどのように機能するかを見てみよう。そのようなセオリーでは、すべての𝐋(n,1)\mathbf{L}(n, 1)𝐋(1,1)\mathbf{L}(1, 1) から到達できる。なぜなら、𝐋(1,1)\mathbf{L}(1, 1) は恒等射だけを含む単元圏であり、𝐋(n,1)\mathbf{L}(n, 1)𝐅\mathbf{F}内の基本射である単射1n1 \to nに対応する射だけを含むからだ。したがって、余積に含まれる加数はすべて同値なので次が得られる: Ta=a×𝐋(1,1)=aT a = a \times \mathbf{L}(1, 1) = a これは恒等モナドだ。

30.7 副作用のローヴェア・セオリー

モナドとローヴェア・セオリーは非常に強く結びついているので、プログラミングでローヴェア・セオリーをモナドの代わりとして使えるかを自然と問いたくなる。モナドの大きな問題点は合成しにくいことだ。モナド変換子を作るための汎用のレシピはない。その面でローヴェア・セオリーは有利で、余積とテンソル積を使って合成できる。一方、有限的モナドしかローヴェア・セオリーに容易に変換できない。ここでの例外は継続モナドだ。この分野での研究が進んでいる(参考文献を参照のこと)。

ローヴェア・セオリーが副作用の記述にどう使えるか説明するため、単純な例として、例外について説明する。それらは伝統的にはMaybeモナドで実装される。

Maybeモナドは零項演算010 \to 1だけを伴うローヴェア・セオリーによって生成される。このセオリーのモデルは、11をある集合aaに写し、零項演算を関数に写す関手だ:

    raise :: () -> a

Maybeモナドはコエンド式を使って復元できる。零項演算を追加するとhom集合𝐋(n,1)\mathbf{L}(n, 1) にどんな影響があるか検討してみよう。新しく(𝐅𝑜𝑝\mathbf{F}^\mathit{op}にはない)𝐋(0,1)\mathbf{L}(0, 1) が作られる以外にも、𝐋(n,1)\mathbf{L}(n, 1) に新しい射が追加される。これらは、n0n \to 0の型の射を010 \to 1と合成した結果だ。このような寄与はすべて、コエンド式でのa0×𝐋(0,1)a^0 \times \mathbf{L}(0, 1) と同一視される。それらは: an×𝐋(0,1)a^n \times \mathbf{L}(0, 1)0n0 \to nによって2種類の方法でリフトすれば得られるからだ。

このコエンドを簡約すると: T𝐋a=a0+a1T_{\mathbf{L}} a = a^0 + a^1 あるいは、Haskellの記法では:

    type Maybe a = Either () a

となり、次と等価だ:

    data Maybe a = Nothing | Just a

このローヴェア・セオリーは例外の送出のみをサポートしており、例外の処理はサポートしていないことに注意してほしい。

30.8 課題

  1. 𝐅\mathbf{F}𝐅𝐢𝐧𝐒𝐞𝐭\mathbf{FinSet}のスケルトン)内の22から33への射をすべて列挙せよ。
  2. モノイドのローヴェア・セオリーについてのモデルの圏が、リストモナドについてのモナド代数の圏と同値であることを示せ。
  3. モノイドのローヴェア・セオリーはリストモナドを生成する。その二項演算は、対応するクライスリ射を使って生成できることを示せ。
  4. 𝐅𝐢𝐧𝐒𝐞𝐭\textbf{FinSet}𝐒𝐞𝐭\mathbf{Set}の部分圏であり、それを𝐒𝐞𝐭\mathbf{Set}に埋め込む関手が存在する。𝐒𝐞𝐭\mathbf{Set}の関手はすべて𝐅𝐢𝐧𝐒𝐞𝐭\mathbf{FinSet}に制約できる。有限的関手がそれ自身の制限の左カン拡張であることを示せ。

30.9 参考文献

  1. Functorial Semantics of Algebraic Theories115, F. William Lawvere
  2. Notions of computation determine monads116, Gordon Plotkin and John Power

31 モナド・モノイド・圏

区切り良く圏論の本を終わらせることはできない。学ぶべきことは常にある。圏論は広大な題材だ。それと同時に、同じテーマ・コンセプト・パターンが明らかに何度も繰り返されている。すべての概念はカン拡張である、という格言があるとおり、カン拡張を使えば極限・余極限・モナド・米田の補題などを導出できる。圏自体の概念は抽象化のあらゆるレベルに現れる。モノイドとモナドの概念も同様だ。最も基本となるものはどれだろうか? 結局のところ、それらはすべて相互に関連しており、抽象化の終わりのないサイクルで互いにつながっている。それらの相互関係を示すことが、この本を締めくくるのにふさわしいと判断した。

31.1 双圏

圏論で特に難しい側面の1つは、絶えず視点が切り替わることだ。たとえば、集合の圏を考えてみてほしい。我々は要素によって集合を定義することに慣れている。空集合には要素がない。単元集合には要素が1つある。2つの集合のデカルト積はペアの集合であり、以下同様だ。しかし、𝐒𝐞𝐭\mathbf{Set}圏について述べるとき、集合の内容を忘れて、代わりにそれらの間の射(矢)に集中するように頼んだ。時々、カバーの下を覗くことが許されたのは、𝐒𝐞𝐭\mathbf{Set}内の特定の普遍的構成が要素の観点で何を記述しているか知るためだった。そうして、終対象は1つの要素からなる集合であることなどが分かった。しかし、それらは健全性チェックにすぎなかった。

関手は圏の写像として定義される。写像を圏内の射と見なすのは自然だ。関手は、圏(サイズに関する問題を避けたいなら、小さい圏)の圏の射だと分かった。関手を射として扱うと、圏の内部(対象と射)に対するその作用についての情報を失うことになる。これは、集合の要素に対する関数の作用についての情報が、その関数を𝐒𝐞𝐭\mathbf{Set}内の射として扱うと失われるのと同じだ。しかし、任意の2つの圏の間の関手も圏を形成する。今度は、ある圏の射であったものを別の圏の対象と見なすことが求められる。関手圏では関手が対象であり、自然変換が射だ。同一のものが、ある圏では射であり、別の圏では対象であり得ることが発見された。対象を名詞とし射を動詞とする素朴な考え方は成り立たない。

2つの観点を切り替える代わりに、1つの観点に統合しようとしてもよい。そうすることで𝟐\mathbf{2}-圏の概念が得られる。この圏では、対象を00-セル、射を11-セル、射間の射を22-セルと呼ぶ。

0-セルa, b、1-セルf, g、および2-セル\alpha。

圏の圏𝐂𝐚𝐭\mathbf{Cat}は端的な例だ。圏が00-セル、関手が11-セル、自然変換が22-セルとなる。𝟐\mathbf{2}-圏の規則が示すのは、任意の2つの00-セル間の11-セルが圏を形成する(言い換えると、𝐂(a,b)\mathbf{C}(a, b) はhom集合ではなくhom圏だ)ということだ。このことは、任意の2つの圏の間の関手は関手圏を形成する、という以前の主張とよく一致している。

典型的には、任意の00-セルからそれ自体に戻る11-セルもhom圏𝐂(a,a)\mathbf{C}(a, a) という圏を形成する。だが、その圏はさらに豊かな構造を持っている。𝐂(a,a)\mathbf{C}(a, a) のメンバーは、𝐂\mathbf{C}内の射とも𝐂(a,a)\mathbf{C}(a, a) 内の対象とも見なせる。これらは射なので互いに合成できる。しかし、それらを対象として見ると、合成は対象のペアから対象への写像になる。それは実際に、積に――正確にはテンソル積に――非常によく似ている。このテンソル積は恒等11-セルという単位元を持つ。𝟐\mathbf{2}-圏では、hom圏𝐂(a,a)\mathbf{C}(a, a) は自動的にモノイダル圏になり、そのテンソル積は11-セルの合成として定義される。結合律と単位律は、対応する圏の規則から単に除かれる。

これが何を意味するのかを、𝟐\mathbf{2}-圏の例として正統な𝐂𝐚𝐭\mathbf{Cat}で見てみよう。hom圏𝐂𝐚𝐭(a,a)\mathbf{Cat}(a, a)aa上の自己関手の圏だ。その中では、自己関手の合成がテンソル積の役割を果たしている。恒等関手はこの積の単位元だ。自己関手がモノイダル圏を形成することは以前にも見た(その事実をモナドの定義で使った)ものの、これはもっと一般的な現象だと分かる。つまり、すべての𝟐\mathbf{2}-圏内の自己-11-セルはモノイダル圏を形成する。これについては、後でモナドを一般化するときに再度説明する。

覚えていると思うが、一般のモノイダル圏では、モノイド則がきちんと満たされていることは主張しなかった。多くの場合、単位律と結合律が同型を除いて満たされていれば十分だった。𝟐\mathbf{2}-圏では、𝐂(a,a)\mathbf{C}(a, a) のモノイド則は11-セルの合成律から導かれる。これらの規則は厳密なので、厳密なモノイダル圏が常に得られる。しかし、これらの規則も緩和できる。たとえば、恒等11-セル𝐢𝐝a% \mathbf{id}_{a}% と別の11-セルfabf \Colon a \to bの合成は、ffと等しいと言わずに、同型だと言える。11-セルの同型は22-セルを使って定義される。言い換えると、次のような22-セル: ρf𝐢𝐝af\rho \Colon f \circ % \mathbf{id}_{a}% \to f と、その逆が存在する。

双圏の恒等律は同型(可逆な2-セル\rho)を除いて成り立つ。

左恒等射と結合律についても同様にできる。このような緩和された𝟐\mathbf{2}-圏は、双圏 (bicategory) と呼ばれる(いくつか追加のコヒーレンシーの規則があるが、ここでは省略する)。

予想どおり、双圏での自己-11-セルは、規則が厳密でない一般化されたモノイダル圏を形成する。

双圏の興味深い例としてスパンの圏が挙げられる。2つの対象aabbの間のスパンは、対象xxおよび次の1対の射だ: fxagxb \begin{gathered} f \Colon x \to a \\ g \Colon x \to b \end{gathered}

圏論的な積の定義でスパンを使ったことを覚えているだろう。ここでは、スパンを双圏での11-セルと見なそう。最初のステップは、スパンの合成を定義することだ。隣接したスパンがあるとする: fybgyc \begin{gathered} f' \Colon y \to b \\ g' \Colon y \to c \end{gathered}

それらの合成は第3のスパンになり、あるzzを頂点とする。最も自然な選択は、ff'に沿ったggの引き戻しだ。引き戻しとは次の2つの射を伴う対象zzであることを思い出してほしい: hzxhzy \begin{gathered} h \Colon z \to x \\ h' \Colon z \to y \end{gathered} ただし: gh=fhg \circ h = f' \circ h' が、そのような対象すべてについて普遍だとする。

ここでは、集合の圏でのスパンに注目しよう。この場合、引き戻しはデカルト積x×yx \times yからのペア (p,q)(p, q) の集合にすぎず、次のようになる: gp=fqg\ p = f'\ q 同じ終点を共有する2つのスパンの間の射は頂点間の射hhとして定義され、適切な三角形を可換にする。

\mathbf{Span}内の2-セル。

要約すると、双圏𝐒𝐩𝐚𝐧\mathbf{Span}では、00-セルは集合、11-セルはスパン、22-セルはスパン間の射だ。恒等11-セルは、3つの対象すべてが同じで、2つの射が恒等射である退化スパン (degenerate span) だ。

双圏の別の例もすでに見た。それはプロ関手の双圏𝐏𝐫𝐨𝐟\mathbf{Prof}であり、00-セルは圏、11-セルはプロ関手、22-セルは自然変換だ。プロ関手の合成はコエンドによって与えられる。

31.2 モナド

ここまでで、自己関手の圏におけるモノイドとしてのモナドの定義にかなり精通しているはずだ。この定義を、自己関手の圏は双圏𝐂𝐚𝐭\mathbf{Cat}内の自己-11-セルの小さいhom圏のひとつにすぎない、という新たな理解に沿って再検討してみよう。自己関手の圏がモノイダル圏であることは知っており、テンソル積は自己関手の合成に由来する。モノイドは、モノイダル圏内のある対象――ここでは自己関手TT――が2つの射を伴ったものとして定義される。自己関手の間の射は自然変換だ。一方の射はモノイダル単位元――恒等自己関手――をTTに写す: ηIT\eta \Colon I \to T もう一方の射はテンソル積TTT \otimes TTTに写す。このテンソル積は自己関手の合成によって与えられるため、次が得られる: μTTT\mu \Colon T \circ T \to T

これらがモナドを定義する2つの操作である(Haskellではreturnおよびjoinと呼ばれる)ことと、モノイド則がモナド則に変わったことが分かる。

さて、この定義から自己関手に関する記述をすべて取り除こう。まず双圏𝐂\mathbf{C}から始めて、その中で00-セルaaを選ぶ。すでに説明したように、hom圏𝐂(a,a)\mathbf{C}(a, a) はモノイダル圏だ。したがって、𝐂(a,a)\mathbf{C}(a, a) のモノイドは、11-セル、TT、および2つの22-セルを、条件: ηITμTTT \begin{gathered} \eta \Colon I \to T \\ \mu \Colon T \circ T \to T \end{gathered} がモノイド則を満たすように選ぶことで定義できる。これをモナドと呼ぶ。

これは、00-セルと11-セルと22-セルだけを使って、はるかに一般的にモナドを定義する。双圏𝐂𝐚𝐭\mathbf{Cat}に適用すると、これは通常のモナドへと簡約される。だが、他の双圏では何が起こるか見てみよう。

𝐒𝐩𝐚𝐧\mathbf{Span}でモナドを構築しよう。ここでは00-セルを選択する。これは集合であり、すぐ明らかになる理由から、𝑂𝑏\mathit{Ob}と呼ぶことにする。次に、自己-11-セル(𝑂𝑏\mathit{Ob}から𝑂𝑏\mathit{Ob}へ戻るスパン)を選択する。その頂点には𝐴𝑟\mathit{Ar}と呼ばれる集合があり、次の2つの関数を伴う: 𝑑𝑜𝑚𝐴𝑟𝑂𝑏𝑐𝑜𝑑𝐴𝑟𝑂𝑏 \begin{gathered} \mathit{dom} \Colon \mathit{Ar} \to \mathit{Ob} \\ \mathit{cod} \Colon \mathit{Ar} \to \mathit{Ob} \end{gathered}

集合𝐴𝑟\mathit{Ar}の要素を「射」と呼ぼう。さらに、𝑂𝑏\mathit{Ob}の要素を「対象」と呼ぼう、と言えば、目的地の手掛かりになるだろう。2つの関数𝑑𝑜𝑚\mathit{dom}𝑐𝑜𝑑\mathit{cod}は、始域と終域を「射」に割り当てる。

スパンをモナドにするには2つの22-セルη\etaμ\muが必要だ。この場合のモノイダル単位元は、𝑂𝑏\mathit{Ob}から𝑂𝑏\mathit{Ob}への自明なスパンであり、頂点は𝑂𝑏\mathit{Ob}で、2つの恒等関数を伴う。22-セルη\etaは2つの頂点𝑂𝑏\mathit{Ob}𝐴𝑟\mathit{Ar}の間の関数だ。つまり、η\etaは「射」をすべての「対象」に割り当てる。𝐒𝐩𝐚𝐧\mathbf{Span}22-セルは可換条件を満たす必要がある。ここでは次のようになる: 𝑑𝑜𝑚η=𝐢𝐝𝑐𝑜𝑑η=𝐢𝐝 \begin{gathered} \mathit{dom} \circ \eta = \mathbf{id}\\ \mathit{cod} \circ \eta = \mathbf{id} \end{gathered}

成分としては、これは次のようになる: 𝑑𝑜𝑚(η𝑜𝑏)=𝑜𝑏=𝑐𝑜𝑑(η𝑜𝑏)\mathit{dom}\ (\eta\ \mathit{ob}) = \mathit{ob} = \mathit{cod}\ (\eta\ \mathit{ob}) ここで、𝑜𝑏\mathit{ob}𝑂𝑏\mathit{Ob}内の「対象」だ。言い換えると、η\etaが割り当てるのは、すべての「対象」と、始域と終域がその「対象」であるような「射」に対してだ。この特別な「射」が「恒等射」と呼ばれる。

2番目の22-セルμ\muは、それ自体とスパン𝐴𝑟\mathit{Ar}との合成に作用する。合成は引き戻しとして定義されているため、その要素は𝐴𝑟\mathit{Ar}からの要素のペア――「射」のペア (a1,a2)(a_1, a_2) だ。引き戻し条件は次のとおりだ: 𝑐𝑜𝑑a1=𝑑𝑜𝑚a2\mathit{cod}\ a_1 = \mathit{dom}\ a_2 a2a_2a1a_1が「合成可能」だと言われるのは、一方の始域が他方の終域だからだ。

22-セルμ\muは、合成可能な矢のペア (a1,a2)(a_1, a_2) を、𝐴𝑟\mathit{Ar}からの単一の矢a3a_3に写す関数だ。言い換えると、μ\muは矢の合成を定義する。

モナド則が射の恒等律や結合律に対応していることは簡単に確認できる。以上で圏を定義できた(対象と射が集合を形成する小さい圏であることは忘れないでほしい)。

つまり、まとめると、圏はスパンの双圏におけるモナドにすぎない。

この結果の驚くべき点は、圏がモナドやモノイドのような他の代数的構造と同じ基盤に置かれていることだ。圏であることは何も特別ではない。ただの2つの集合と4つの関数だ。実際に、対象ごとに個別の集合さえ必要ない。対象は恒等射と同一視できる(それらは1対1で対応している)からだ。つまり、実際にはひとつの集合といくつかの関数にすぎない。圏論がすべての数学において中心的役割を担っていることを考えると、これは非常に謙虚な認識だ。

31.3 課題

  1. 双圏における自己-11-セルの合成として定義されたテンソル積について、単位律と結合律を導出せよ。
  2. 𝐒𝐩𝐚𝐧\mathbf{Span}内のモナドについて、モナド則が、結果の圏内での恒等射と結合律に対応していることを確認せよ。
  3. 𝐏𝐫𝐨𝐟\mathbf{Prof}内のモナドが、対象における恒等射の関手 (identity-on-objects functor) であることを示せ。
  4. 𝐒𝐩𝐚𝐧\mathbf{Span}内のモナドについてのモナド代数とは何か?

31.4 参考文献

  1. Paweł Sobocińskiのブログ117

索引

謝辞

私の計算と論理をチェックしてくれたEdward KmettとGershom Bazermanに感謝したい。誤りを訂正し、本書を改善してくれた大勢のボランティアに感謝している。

Andrew Suttonには、自身とBjarne Stroustrupの最新の提案に沿ってC++のモノイドの概念コードを書き直してくれたことに感謝したい。

Eric Nieblerには、草稿を読み、C++14の高度な機能を用いて型推論を進めるcomposeの巧妙な実装を提供してくれたことに感謝している。昔ながらのテンプレートマジックを使って型トレイトと同じことをしていたセクションをすべてカットできた。 いい厄介払いだ!

Gershom Bazermanには、有益なコメントのおかげで、いくつかの重要な点を明確にできたことにも感謝したい。

ライセンス

この作品は、クリエイティブ・コモンズの 表示 - 継承 4.0 国際 ライセンスで提供されています。ライセンスの写しをご覧になるには、http://creativecommons.org/licenses/by-sa/4.0/deed.jaをご覧頂くか、Creative Commons, PO Box 1866, Mountain View, CA 94042, USA までお手紙をお送りください。