bitterharvest’s diary

A Bitter Harvestは小説の題名。作者は豪州のPeter Yeldham。苦闘の末に勝ちえた偏見からの解放は命との引換になったという悲しい物語

カレー風味のニョッキ

昨日、近くのカルディで三色ニョッキを手に入れた。ニョッキはジャガイモと小麦粉から作られるイタリア料理だ。そこに、トマトを加えると赤に、ほうれん草だと緑になる。これを利用したのが三色ニョッキだ。

ニョッキの由来をウィキペディアで調べてみると、その語源は、「塊」を意味するゲルマン語系の単語knokkaである。もともとは小麦粉を練って作っていた。アメリカ大陸からジャガイモが持ち込まれた後、これを用いるようになったそうだ。

ニョッキを見るとすいとん(水団)を連想することが多い。すいとんは小麦粉を練って作る。ニョッキもすいとんも元々は同じものであったかもしれない。ウィキペディアによれば、すいとん室町時代書物には表れてくるそうだ。これよりも古い歴史を持っているのがそばがき(蕎麦掻き)だ。これは小麦粉の代わりにそばを用いる。同じくウィキペディアによれば、そばがきは鎌倉時代には存在したそうだ。

すいとんもそばがきもあまりいいイメージがない。特に、そばがきには悪い印象がまつわりついている。食糧事情があまりよくなかった戦後の時代に、北海道の親せきから暮れになると半端でない量のそば粉が送られてきた。そばにでもと思って送ってきたのだろうが、そばの打ち方を知らない両親は、そばがきにしてくれた。一日か二日ならばおいしいと思ったのだろうが、何日も続けて食べさせられると、見るのも嫌になった。

すいとんも同じように何もない時に食べさせられた記憶があるので、すいとんもそばがきも出会いたくない料理だ(同じような理由で母は戦時中にこればかり食べさせられたというカボチャが嫌いだった)。

すいとんやそばがきを連想させるニョッキはこれまでは避けていたが、赤や緑の色が醸し出す茶目っ気さがこれまでの抵抗感を和らげてくれ、料理に使うことにした。

ニョッキが主張しすぎると、すいとんやそばがきにまつわる芳しくない思い出が呼び起こされるだろうから、少し強い味にしてみようと考え、カレー風味にすることとした。幸い、中村屋のカレー粉が冷蔵庫にしまってあるので、ちょっと高級なカレー味になるようにということで料理を始めた。

今日の食材の仲間たちは、次のとおりである。中央、左側に控えているのが、今日の主役の三色ニョッキである。そして、右側には脇役のシイタケとベーコンである。残りの連中は種類は多いが全て味付け役だ。
f:id:bitterharvest:20161123165802j:plain

まずは、イタリア料理の定番、ニンニク(2かけ)をオリーブオイル(大匙2杯)で炒める。弱火でニンニクの焼けたにおいが出てくるまで続ける。
f:id:bitterharvest:20161123170528j:plain

その後、ベーコン(100g程度)とシイタケ(2個)、さらに、日本酒(大匙2杯)を加えて、中火にして軽く炒める(本当は白ワインを使いたかったのだが、生憎、持ち合わせていなかった)。
f:id:bitterharvest:20161123170811j:plain

次に、牛乳(200CC)、カレー粉(大匙1杯)、パプリカ、クミンハウダー、塩、コショウ(それぞれ少々)を加えて、中火で煮詰める。
f:id:bitterharvest:20161123171100j:plain

鍋にお湯を沸かし、沸騰したところで、塩(大匙1杯)を加える。そして、三色ニョッキを鍋に入れ、水面に浮かび上がってくるまで煮立てる(大体3分程度)。
f:id:bitterharvest:20161123171326j:plain

ニョッキを網ですくって皿に移す。
f:id:bitterharvest:20161123171447j:plain

カレー風味をニョッキの上に注ぐ。
f:id:bitterharvest:20161123171620j:plain

イタリア料理らしくするために、イタリアンパセリとパルミジャーノを用意する。
f:id:bitterharvest:20161123171737j:plain

これを料理の上にパラパラとふってできあがりだ。
f:id:bitterharvest:20161123171823j:plain

食卓にはこのような感じで登場した。
f:id:bitterharvest:20161123171911j:plain

悪い思い出がよみがえることもなく、おいしくいただけた。

プログラムでの圏の構成を完成させる

4.7 C++プログラムから圏を作成する(続き)

前回の記事で、曜日を進めるプログラムの開発状況を手順を追いながら、説明した。このプログラムは、曜日を進める関数\(increase\)と遅らせる関数\(decrease\)から出発した。最初に作った関数は、値をどこにも記憶しない純粋な関数の集まりであった。

しかし、新たな要求が発生したために、呼ばれた関数をログとして出力することが要求された。最初に最も安易な実現方法を示した。それは、グローバル変数にログを記憶するというものであった。プログラム開発に馴染んでいない初心者が陥りやすいミスの一つだが、プログラムを開発するときに心掛けなければならない参照の局所性を犯している。これは重大な欠陥で、一つの事項の変更がプログラムのあちらこちらの変更に砂がるため、後々のプログラムの改変を困難なものにしてしまう。

上記の問題を改善するために、ログを局所変数化して、それを、関数の引数で引き渡すようにした。プログラムは、随分と改善されたのだが、しかし、引数として渡された変数は、その関数での直接的な関心事ではない。恐らく、プログラムが完成してからしばらくたつと、その変数がなぜ渡されてきたのだろうと疑問に思うことになるだろう。これは、関心の分離というもう一つのプログラム開発に当たって注意しなければならない事項を無視していたために生じた。

そこで、それぞれの関数は、不必要なものは一切有しない純粋な関数にして、関数の合成で、要求仕様の変更に対応した。この時の説明で変だなと思った人も多かったのではないかと思う。数学の力が強い人なのだろうと推察する。二つの関数\(f:a \rightarrow b,g:b \rightarrow c\)があった時、それらの関数の合成は、\(h=g \circ f : a \rightarrow c\)である。

これは、プログラムでは次のようになる。

function<c(a)> h(function<b(a)> f, function<c(b)> g) {}

ところが、関数の合成は次のようになっていた。

function<pair<int, string>(int)> compose(function<pair<int, string>(int)> f, function<pair<int, string>(int)> g)
{
	return [f, g](int x) {
		auto p1 = f(x);
		auto p2 = g(p1.first);
		return make_pair(p2.first, p1.second + p2.second);
	};
}

必要なところだけを切り出すと、\(f: int \rightarrow (Int,String), \ g: int \rightarrow (Int,String) \)となっている。

圏論での約束は、\(f\)のコドメインと\(g\)のドメインが一致していなければならないと謳っている。しかし、\(f\)のコドメインは\((Int,String)\)であるにもかかわらず、\(g\)のドメインは\(Int\)である。一致していない。このようなものを、どのようにして合成するのかという疑問がわく。これは、正しい疑問である。

3)クライスリ圏

数学は、公式に縛られる窮屈な世界だと思っている人も多いと思う。実は、そんなことはない。少し、例外的なものが発生したときは、それを含むように、概念を拡張してあげればよい。それによって、新しい体系の数学を作ることができる。

今回の場合も同じようなものだ。\(g\)のドメインは\(Int\)だ。\(f\)のコドメインには\(Int\)が含まれているので、これを利用して、射を合成できるように規則を拡張したらどうだろうかという発想が生まれる。

例えば、二つの対象\(A,B\)に対して(射\(f\)は元々は\(f: A \rightarrow B\)であったのだが)、何らかの要請で、\(f: A \rightarrow (B, C)\)とする必要が発生した。そこで、圏論での約束を少し緩めて、二つの対象\(A,B\)にして、射\(f: A \rightarrow (B, C)\)を射として認めるようにしたらどうであろうか。

この例で行くと、\(increase\)は、元々、\(Int\)から\(Int\)への関数であった。\(decrease\)も同じである。しかし、ログを作成できるようにという要求に合わせるために、これらは\(Int\)から\((Int, String)\)への関数へと変化した。

曜日を進めるプログラムがどのような圏になるかを直感的に理解してもらうために、まず、図を見てもらおう。
f:id:bitterharvest:20161118151412p:plain

そこで、\(increase\)のドメインを\(Int\)とし、整数(曜日)と文字列の対であるコドメインを\(\mathcal{M}(Int)\)で表すこととしよう。即ち、\(increase:Int \rightarrow \mathcal{M}(Int)\)とする。\(decrease,stay\)についても同様である。

そこで、これらの関数は合成するときは、\(\mathcal{M}(Int)\)を\(Int\)に変換する必要がある。関数の合成にはこの機能を持たせることにしよう。そうすると、\(compose(increase,decrease)\)は、図に示したように、\(increase\)を実行した後で、\(decompose\)を実行し、最後に\(decrease\)を実行すると読むことができるので、これらの、ドメインとコドメインの関係は
\begin{eqnarray}
increase: Int \rightarrow \mathcal{M}(Int) \\
compose: \mathcal{M}(Int) \rightarrow Int \\
decrease: Int \rightarrow \mathcal{M}(Int)
\end{eqnarray}
となり、関数の合成に関する約束(合成される関数のコドメインと合成する関数のドメインは一致する)を守ることができる。

次のstayの関数について考えよう。この関数は恒等射と考えているので、入力された曜日はそのままである。またこれまでに実行してきた関数の列に対しても変化を加えてはいけないので、ここでは関数の名前\("stay"\)を出力するのではなく、空の文字列""を出力する。

圏の形になってきたので、その構成を示そう。
1) 対象:\(Day=\{0,1,2,3,4,5,6\}\)
2) 射:\(increase, decrease\)
3) ドメイン:\(Day\)、コドメイン:\(\mathcal{M}(Day)\)
4) 恒等射:\(stay\)
5) 合成:\(compose\)
① 結合律:満足
② 単位律:満足

結合律、単位律は、プログラムの内容から満たされていると判断できるが、念のため、正しいかどうか調べてみよう。次のような\(main\)のプログラムを作成して検査してみることにした。
最初の2行は恒等射となっている\(stay\)に関するものである。次の4行は、\(compose\)の実行順序によらないことを示すために、左側から実行する場合とそれとは逆の場合で変化が生じるかを調べるものである。

int main()
{
	auto h = compose(stay, decrease);
	auto k = compose(decrease, stay);
	auto p = compose(decrease, compose(increase, decrease));
	auto r = compose(compose(decrease, increase), decrease);
	auto s = compose(decrease, compose(decrease, compose(increase, decrease)));
	auto t = compose(compose(compose(decrease, decrease), increase), decrease);
	auto a = h(3);
	auto b = k(3);
	auto c = p(3);
	auto d = r(3);
	auto e = s(3);
	auto f = t(3);
	cout << a.first << endl;
	cout << a.second << endl;
	cout << b.first << endl;
	cout << b.second << endl;
	cout << c.first << endl;
	cout << c.second << endl;
	cout << d.first << endl;
	cout << d.second << endl;
	cout << e.first << endl;
	cout << e.second << endl;
	cout << f.first << endl;
	cout << f.second << endl;
	return 0;
}

プログラムの実行結果を以下に示す。いずれの場合も予想通り正しい動きをした。
f:id:bitterharvest:20161118163202p:plain

このように純粋な関数と、その合成、および、恒等射としての役割を持つプログラムで構成された圏は、クライスリ(Kleisli)圏と呼ばれる。

また、クライスリ圏はモナド(Monad)と呼ばれるものに含まれる。これについては、また、後の記事で詳しく説明する。

なお、使用された関数のロゴを出力してくるところについてはあまり詳しく説明しなかったが、この部分はモノイド圏になっていることに注意してほしい。

追:\(compose\)については、Milewski氏が圏論の説明をする時に使っていたものをそのまま利用させてもらった。紙面を借りてお礼申し上げる。

C++のプログラムも圏に変えてしまおう

4.7 C++プログラムから圏を作成する

いろいろな圏を見てきたので、ここでは、プログラマーにとっては大事なプログラムを圏として構成することを考えよう。

1)曜日を進めるプログラム

ここで、作成するプログラムを曜日を進めたり遅らせたりするプログラムだ。曜日は数字で表わし、日曜日は0に、月曜日は1というように、日曜日からの昇順で表わすことにしよう。

プログラムに用いる言語は、多くのプログラマが馴染んでいる命令型言語(Imperative Programming Language)を用いることする。ここではC++を使ってみることにしよう。とても久しぶりだが、新しい発見があるかもしれない。そこで、急遽、パソコンにVisual Stadio Communityをインストールし、C++を使えるようにした。開発中の画面を紹介しよう。
f:id:bitterharvest:20161116171747p:plain
上の画面は、ここで紹介するプログラム開発の最終場面である。左側の大きなウィンドウが編集用の画面で、右下の黒いウィンドウが実行画面である。プログラムを編集した後に、[Build]→[Build Solution]で実行ファイルを作成し、[Debug]→[Start Without Debugging]でプログラムを実行できる。編集機能もしっかりしているので、初めてのVisual Studioであったが、プログラムは簡単に作成することができた。

プログラミングをするときには、必要な機能を定めることから始まる。曜日を進めたり、遅らせたりといっているので、一日だけ曜日を進める関数と遅らせる関数が必要になるだろう。これらは、\(increase\)と\(decrease\)という名前を付けて用意しよう。これらの関数は、曜日\(x\)に1を加え、あるいは1減じてそれを7進数で表せばよいので、以下のようなプログラムとなる。\(main\)には、これらの使用例を記述した。

#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;

int increase(int x)
{
	return (x + 1) % 7;
}

int decrease(int x)
{
	return (x - 1) % 7;
}

int main()
{
	cout << increase(3) << endl;
	cout << decrease(4) << endl;
	cout << increase(2) << endl;
	return 0;
}

簡単なプログラムなので問題はないだろう。ところで、プログラムを作成していると変更が生じるのは、日常茶飯事である。上記のプログラムを実現した後で、呼ばれた関数名のログを作成するようにと要求されたら、どのように応えたらよいであろうか。

次のようなプログラムで対応する魅力に駆られる人も多いことであろう。グローバル変数として\(log0\)を用意し、それぞれの関数の中で、呼ばれるたびに、自身の名前を\(log0\)に付け加える。プログラムで示すと次のようになる。

#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;

string log0 = "";

int increase(int x)
{
	log0 += "increase; ";
	return (x + 1) % 7;
}

int decrease(int x)
{
	log0 += "decrease; ";
	return (x - 1) % 7;
}

int main()
{
	cout << increase(3) << endl;
	cout << decrease(4) << endl;
	cout << increase(2) << endl;
	cout << log0 << endl;
	return 0;
}

上記のプログラムは、変更箇所が少ないので、将来の変更を考えない人は、この魅力に惑わされてしまう。しかし、このプログラムは、将来の変更には脆弱である。例えば、\(log0\)という名前がよくないので変更ということになると、全ての関数がその影響を受ける。名前の変更程度ならよいが、最近人気が出てきている並列処理で実行できるようにしたいとなった時、\(log0\)にアクセスしているすべての場所で変更が要求される。

これは、プログラムが参照の局所性(Locality of Reference)を守っていないことによる。そこで、引数で渡すことにして、次のように、プログラムを実現した。

#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;

pair<int, string> increase(int x, string log0)
{
return make_pair((x + 1) % 7, log0+"increase; ");
}

pair<int, string> decrease(int x, string log0)
{
return make_pair((x - 1) % 7, log0 + "decrease; ");
}

int main()
{
string log0 = "";
auto a = increase(3, log0);
auto b = decrease(4, a.second);
auto c = increase(2, b.second);
cout << a.first << endl;
cout << b.first << endl;
cout << c.first << endl;
cout << c.second << endl;
return 0;
}

かなり見やすくなってはいるのだが、それぞれの関数で\(log0+...\)とあるのが気になる。それぞれの関数で、\(log0\)がなんであるかは知らなくてもよいことのように思われる。関係ないものを分離することを関心の分離(Separation of Concerns)というが、個々の部分はこの原理を犯しているように思われる。

そこで、プログラムをさらに改善することにする。関数\(increase\)と\(decrease\)はそれぞれ、計算の結果と関数の名前を対にして出力するように改める。このようにすれば、それぞれの関数は、その関数に求められている必要最小限のことだけを行い、それ以外のことはしないようにすることができる。

2)圏に移す

これらの関数は次から次へとと呼ばれるが、これは、関数の合成で表すこととしよう。即ち、二つの関数\(f:a\rightarrow b,g:b\rightarrow c\)がこの順番で呼ばれた時、\(h=g \circ f : a \rightarrow c\)となるようにしよう。
プログラムは次のような形になる。

function<c(a)> h(function<b(a)> f, function<c(b)> g) {}

また、この関数の戻り値は、関数の本体である。これはラムダ関数として返すようにする。これに注意して、\(compose\)を実現してみよう。

function<pair<int, string>(int)> compose(function<pair<int, string>(int)> f, function<pair<int, string>(int)> g)
{
	return [f, g](int x) {
		auto p1 = f(x);
		auto p2 = g(p1.first);
		return make_pair(p2.first, p1.second + p2.second);
	};
}

プログラム全体を示すと以下のようになる。

#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;

pair<int, string> increase(int x)
{
	return make_pair((x + 1) % 7, "increase; ");
}

pair<int, string> decrease(int x)
{
	return make_pair((x - 1) % 7, "decrease; ");
}

function<pair<int, string>(int)> compose(function<pair<int, string>(int)> f, function<pair<int, string>(int)> g)
{
	return [f, g](int x) {
		auto p1 = f(x);
		auto p2 = g(p1.first);
		return make_pair(p2.first, p1.second + p2.second);
	};
}


int main()
{
	auto h = compose(increase, decrease);
	auto p = compose(decrease, compose(increase, decrease));
	auto r = compose(decrease, compose(decrease, compose(increase, decrease)));
	auto a = h(3);
	auto b = p(3);
	auto c = r(3);
	cout << a.first << endl;
	cout << a.second << endl;
	cout << b.first << endl;
	cout << b.second << endl;
	cout << c.first << endl;
	cout << c.second << endl;
	return 0;
}

mainで、関数をいくつか合成し、実験してみた。最初の合成は、
\(increase \circ decrease\)である。この合成関数は\(h\)という名前がつけれらている。値を入力すると実行される。
これに続いて\(decrease \circ increase \circ decrease\)と\(decrease \circ decrease \circ increase \circ decrease\)の合成関数を用意した。これらは\(p,r\)という名前がつけれらている。
実行結果は最初の画面の右下の黒いウィンドウである。

関数の合成がを用意したので、圏とするためには、恒等射が必要である。これは、曜日を進めもしなければ遅らせもしない関数である。\(stay\)という名前にして、もらった曜日をそのまま返すようにしよう。なお、関数名はブランクで返すこととする(恒等射にするためだが、説明は後にする)。即ち、

pair<int, string> stay(int x)
{
	return make_pair(x, "");
}


プログラムの全体を示すと次のようになる。

#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;

pair<int, string> increase(int x)
{
	return make_pair((x + 1) % 7, "increase; ");
}

pair<int, string> decrease(int x)
{
	return make_pair((x - 1) % 7, "decrease; ");
}

pair<int, string> stay(int x)
{
	return make_pair(x, "");
}

function<pair<int, string>(int)> compose(function<pair<int, string>(int)> f, function<pair<int, string>(int)> g)
{
	return [f, g](int x) {
		auto p1 = f(x);
		auto p2 = g(p1.first);
		return make_pair(p2.first, p1.second + p2.second);
	};
}

int main()
{
	auto h = compose(increase, decrease);
	auto p = compose(decrease, compose(increase, decrease));
	auto r = compose(decrease, compose(decrease, compose(increase, decrease)));
	auto s = compose(stay, compose(decrease, compose(decrease, compose(increase, decrease))));
	auto a = h(3);
	auto b = p(3);
	auto c = r(3);
	auto d = s(3);
	cout << a.first << endl;
	cout << a.second << endl;
	cout << b.first << endl;
	cout << b.second << endl;
	cout << c.first << endl;
	cout << c.second << endl;
	cout << d.first << endl;
	cout << d.second << endl;
	return 0;
}

ここまでできたので、次は、これを圏として構成してみよう。少し、説明が長くなりそうなので、記事を改めることにする。

二項演算子からモノイド圏を作る

前回の記事は、ホモトピー論まで含まれていたので、とても高度な数学的概念が含まれていた。それにもめげず、沢山の人が記事を読んでくださったようで、感謝している。今回は現実の世界に戻して、二項演算子の世界を圏にすることを考えよう。

4.6 二項演算子からの圏

単純な圏から複雑な圏へと話を進めてきたが、対象が一つで、射が複数あるいは無数の圏を考えてみよう。小学校で一番最初に学ぶ数学の概念は加算である。二つの自然数を足し合わせて自然数を得るという考え方を学んだのは、記憶の遠くにある小学校の1年生の頃だ。教育熱心の親に育てられた人であれば、すでに、幼稚園の時に学んでいる可能性もある。頭の中に定着している考え方を圏論という立場から整理しなおしてみよう。

小学校の1年生の時に習った足し算は自然数を相手にしていたが、ここでは、そうではなくて整数を相手にするととにしよう。そうすると、足し算は二つの自然数を得て、足した結果(これも自然数になる)を出力する。Haskellでもそのようになっている。

Prelude> 3 + 4
7

このように二つの計算しようとする数の真ん中に置かれる演算子二項演算子と呼ばれる。あるいは中置関数とも呼ばれる。しかし、関数を用いるときは、一般的には、関数名を書いた後で、変数を並べるのがふつうである。Haskellはこのように記述することも許されている。例えば、足し算の場合には、次のように書くことができる。

Prelude> (+) 3 4
7

1)加法演算子から圏を作成する

これをそのまま圏論の射にしようとすると、射のドメインは二つの整数の対、コドメインは一つの整数となってしまう。このため、演算子の合成(足し算を繰り返し行うこと)をうまく説明することができない。ドメインとコドメインを同じ対象とするためには、入力する整数の数を一つにする必要がある。これは、足される数を演算子の中に含めることで実現できる。Haskellもこれを用意していて、次のように利用することができる。このように多変数の関数を一変数のそれにすることをカリー化(Currying)という。

Prelude> (+3) 4
7

これは一つの圏をなしているが、図で表すと次のようになる。
f:id:bitterharvest:20161115102031p:plain

構成は次のようになっている。
1) 対象:整数\({\mathbb Z}\)
2) 射:\( (+i) \) ここで\(i \in {\mathbb Z}\)
3) ドメイン、コドメイン:整数\({\mathbb Z}\)
4) 恒等射:\( (+i) \)
5) 合成:\( (+i) \circ (+j) = (+ (i+j) ) \)
① 結合律:満足
② 単位律:満足

Haskellでの合成の例を示しておこう。

Prelude> (+3) . (+4) $ 2
9

二項演算子を圏で表現することに成功したが、他には、方法がないであろうか。二項演算子を関数の合成とし、足される数と足す数をそれぞれ射にするのはどうであろうか。暗黙の裡に整数を集合として考えていたと思うが、モノの見方を全く変えて、これを射と考える。圏論では、射の方が先に来るので、慣れてくるともっともなのだが、集合と言う枠組みの中で考えていると、なかなかこのような発想は浮かばない。

足される数と足す数をそれぞれ射にすると、対象はどうなるのであろうか。射には、ドメインとコドメインが存在しないといけないが、これはシングルトン(Haskellでは()であらわす)ということにしよう。それでは、このように表した圏を図に描いてみよう。
f:id:bitterharvest:20161115104853p:plain

構成は次のようになっている。
1) 対象:シングルトン
2) 射:\(i \in {\mathbb Z}\)
3) ドメイン、コドメイン:シングルトン
4) 恒等射:\( 0 \)
5) 合成:\( i \circ j = i+j \)
① 結合律:満足
② 単位律:満足

上の例では、合成を加算と見なしていたが、除算と見なせば引き算が、乗算と見なせば掛け算が定義されたことになる(但し、割り算の結果が整数にならない。そうでなくても、ゼロでの割り算が定義されないばかりでなく、計算の順序にもよるので、即ち、\( (24 / 4 )/ 2 \neq 24 / (4 / 2) \)なので、結合律を満たさず、圏とはならない)。

2)あみだくじから圏を作成する

もう少し、馴染みのある例を挙げてみよう。あみだくじだ。今、3人であみだくじを引くことにする。三本の線を引くことになるが、線の置換は以下の6種類に限られる。
f:id:bitterharvest:20161115113646p:plain

そこで、置換前とその後の位置を行列で表し、これを置換行列と呼ぶことにしよう。そして、それぞれの置換行列には名前を付けることにしよう。このようにすると、6種類の置換は次のように表現できる。

\begin{eqnarray}
p_0 &=&
\begin{pmatrix}
1 & 2 & 3 \\
1 & 2 & 3
\end{pmatrix}
,\ p_1 =
\begin{pmatrix}
1 & 2 & 3 \\
2 & 1 & 3
\end{pmatrix}
,\ p_2 =
\begin{pmatrix}
1 & 2 & 3 \\
3 & 2 & 1
\end{pmatrix}
, \\
p_3 &=&
\begin{pmatrix}
1 & 2 & 3 \\
1 & 3 & 2
\end{pmatrix}
,\ p_4 =
\begin{pmatrix}
1 & 2 & 3 \\
2 & 1 & 2
\end{pmatrix}
,\ p_5 =
\begin{pmatrix}
1 & 2 & 3 \\
3 & 2 & 1
\end{pmatrix}
\end{eqnarray}

また、あみだくじの置換は、次の図のように、接続できるものとしよう。
f:id:bitterharvest:20161115115542p:plain

上の図では、\(p_1\)と\(p_1\)の置換行列を接続して新しい置換行列を作成した。新しい置換行列を\(p_2 \circ p_1\)としよう。

それでは二つの置換行列を合成したものを他にも求めてみよう。どの二つを合成しても、6種類の置換行列のどれかに対応することが分かる。
\begin{eqnarray}
p_0 \circ p_0 &=& p_0, \ p_1 \circ p_0 = p_1, \ p_2 \circ p_0 = p_2, \ p_3 \circ p_0 = p_3, \ p_4 \circ p_0 = p_4, \ p_5 \circ p_0 = p_5, \\
p_0 \circ p_1 &=& p_1, \ p_1 \circ p_1 = p_0, \ p_2 \circ p_1 = p_5, \ p_3 \circ p_1 = p_4, \ p_4 \circ p_1 = p_3, \ p_5 \circ p_1 = p_2, \\
p_0 \circ p_2 &=& p_2, \ p_1 \circ p_2 = p_4, \ p_2 \circ p_2 = p_0, \ p_3 \circ p_2 = p_5, \ p_4 \circ p_2 = p_1, \ p_5 \circ p_2 = p_3, \\
p_0 \circ p_3 &=& p_3, \ p_1 \circ p_3 = p_5, \ p_2 \circ p_3 = p_4, \ p_3 \circ p_3 = p_0, \ p_4 \circ p_3 = p_2, \ p_5 \circ p_3 = p_1, \\
p_0 \circ p_4 &=& p_4, \ p_1 \circ p_4 = p_2, \ p_2 \circ p_4 = p_3, \ p_3 \circ p_4 = p_1, \ p_4 \circ p_4 = p_5, \ p_5 \circ p_4 = p_0, \\
p_0 \circ p_5 &=& p_5, \ p_1 \circ p_5 = p_3, \ p_2 \circ p_5 = p_1, \ p_3 \circ p_5 = p_2, \ p_4 \circ p_5 = p_0, \ p_5 \circ p_5 = p_4
\end{eqnarray}

そこで、置換行列を射にしてあみだくじの圏を構成してみよう。
構成は次のようになっている。
1) 対象:シングルトン
2) 射:置換行列\(p_0,p_1,p_2,p_3,p_4,p_5\)
3) ドメイン、コドメイン:シングルトン
4) 恒等射:\(p_0\)
5) 合成:置換行列の接続
① 結合律:満足
② 単位律:満足

図で示すと次のようになる。
f:id:bitterharvest:20161115152209p:plain

3)モノイド圏

あみだくじが出てきたので、可換群を想像した人は、群論に詳しい人だと思う。

群論の中で一番単純な群は半群である。半群は次のように定義されている。

集合\(S\)とその上の二項演算子\(\circ : S \times S \rightarrow S\)が与えられた時、組\((S, \circ) \)が結合律を満たす時、これを半群という。

さらに、半群単位元を有するならば、これをモノイド群という。

モノイド圏は、これまで見たきたように、集合の要素を射に、二項演算子を射の合成に、そして、単位元を恒等射にしたものである。このため、モノイド群から作られたものであるので、モノイド圏という。

モノイド群といわれると複雑そうに見えるが、二項演算子を圏にしたものと考えれば納得いくだろう。

様々な関係から圏を作成してみよう

4.様々な圏の作成

圏に馴染むためにいくつかの例を挙げることにしよう。

4.1 空圏

圏の中でもっとも単純な圏は、対象も射も持たない。このため、恒等射も存在しないことになる。これは空圏(圏0)と呼ばれる。その構成を挙げておこう。
1) 対象:なし
2) 射:なし
3) ドメイン、コドメイン:なし
4) 恒等射:なし
5) 合成:なし
① 結合律:満足
② 単位律:満足

4.2 終端圏

空圏に続いて単純な圏は、対象と射を一つずつ持つ終端圏(自明な圏、圏1)である。これの構成は次のようである。
1) 対象:一つ\(T\)
2) 射:一つ\(id_T\)
3) ドメイン、コドメイン:\(T\)
4) 恒等射:あり\(id_T\)
5) 合成:\(\circ\)
① 結合律:満足、即ち、\( (id_T \circ id_T) \circ id_T = id_T \circ (id_T \circ id_T) \)
② 単位律:満足、即ち、\(id_T \circ id_T=id_T\)

まだ、関手(functor)の話をしていないが、圏を対象とし、圏から圏への弧を射にすると新たな圏を構成することができる。この圏での始対象となるのが空圏で、終対象となるのが終端圏である。

4.3 二集合と関数からの圏

二つの集合\(A,B\)とその間の関数\(f\)は、それぞれの集合に恒等射を用意することで、圏にすることができる。
1) 対象:集合\(A,B\)
2) 射:関数\(f\) 、及び、恒等射と合成された射
3) ドメイン:\(A\)、コドメイン:\(B\)
4) 恒等射:\(id_A,id_B\)
5) 合成:\(\circ\)
① 結合律:関数が一つなので満足
② 単位律:\(f \circ id_A = f = id_B \circ f\)とする。

図で例を示そう(終端圏の例も同時に示す)。
f:id:bitterharvest:20161107173827p:plain

4.4 グラフからの圏

射が写像だけではなく、関係も表すことができることを説明しよう。最初はグラフである。

グラフには方向のあるものとそうでないものがあるが、ここでは方向のあるものについて扱う。グラフは、節点(node)とそれを結ぶ弧(arc)によって構成される。
グラフからは、次のようにして圏が作成できる。
1) 節点(\(V_1,V_2,V_3,..\))を対象にし、弧(\(f_1,f_2,f_3,..\))を射とする。
2) さらに二つの連続している弧(片方の弧の終点と他方の弧の始点が同じ節点である)に対しては、これらを接続した弧を新たに作成し、射の中に加える。この操作を、新たな弧が作れなくなるまで繰り返す。
3) 各節点に対して、自身へ向かう弧を作成し、これを恒等射とする。

それでは例で示そう。
f:id:bitterharvest:20161109053956p:plain
1) 左上は与えられたグラフである。
2) 右上は、二つの連続する弧をつなげて加えたものである。
3) 左下は三つの連続した弧をつなげて加えたものである。
4) 右下は恒等射を加えたものである。

出来上がった圏は次のようになっている。
1) 対象:節点\(U,V,W,X,Y,Z\)
2) 射:弧\(f,g,h,k,p,q,r\),及び、恒等射と合成された射
3) ドメインとコドメイン: \(f:V \rightarrow W, g:W \rightarrow X, h:X \rightarrow Y, k:U \rightarrow Z\), \( p:V \rightarrow X, q:W \rightarrow Y, r:V \rightarrow Y \)
4) 恒等射:\(id_U,id_V,id_W,id_X,id_Y,id_Z\)
5) 合成:\(p=g \circ f, \ q=h \circ g, \ r=h \circ g \circ f =h \circ p =q \circ f \)
① 結合律:\((h \circ g) \circ f = h \circ (g \circ f)\)であるので満足している。
② 単位律:\(f \circ id_V = f = id_W \circ f,...\)であるので満足している。

4.4 順序からの圏

射が関係を表す例の二番手は順序である。順序は、量や質の差によって、順序付けを行うときに使う数学の概念である。順序付けの仕方には強い場合も弱い場合もあるが、その強弱によって、順序には、前順序(preorder)、半順序(partial order)、全順序(total order)の三種類が数学の世界では用意されている。

1)前順序

ATP男子のテニスツアーも一昨日(11月6日)全て終わり、2016年度のシングルスレースの順位が確定した。錦織は、幸い、5位の位置におりロンドンで開催されるATPファイナルに出場できることとなった(上位の8人がファイナルに出場できる。今年は、ナダルが8位だが、怪我のため出場しないので、9位のティエムが繰り上げで参加になった。また、4大オープンに優勝すれば順位に関係なく出場できる)。上位5人の今年度のポイントは次のようになっている。

順位名前ポイント
1マレー\(11,185\)
2ジョコビッチ\(10,780\)
3ワウリンカ\(5,115\)
4ラオニッチ\(5,050\)
5錦織\(4,705\)
ポイントによって、上記のように順位がつけられているけれども、マレーとジョコビッチのどちらが強いかと聞かれた時、戸惑う人が多いのではないだろうか(先週までは、ジョコビッチが1位だった)。二人のこれまでの対戦成績は、29対29と拮抗している。甲乙つけがたい。

しかし、マレーとワウリンカを比べたとき、どちらが強いかと問われた時、マレーだと多くの人が答えるのではないだろうか。対戦成績を比較すると9対7だ。ジョコビッチとはさらにひらいて19対5だ。

だが、ワウリンカは次に続くラオニッチや錦織よりは強そうだ。4大タイトルの試合で2回も優勝している。

このように考えると、上位5人は3グループに分けられそうだ。最強の2人(マレー、ジョコビッチ)、追いかける1人の勇者(ワウリンカ)、次の世代を担う2人(ラオニッチ、錦織)という具合だ。

そこで、この強さの関係を前順序で表すことにしよう。前順序は、マレーとジョコビッチのように、二者間で順序をつけがたい時、即ち、どちらとも言えない時に、用いると便利である。

前順序は、量あるいは質の違いを二項関係\(\leq\)で表し、次の要件が成立することが必要とされる。ここで、\(P\)は集合とし、集合の要素同士を比較しているものとする。
1) 反射律(reflexivity):任意の\(a \in P\)に対して、\(a \leq a\)である。
2) 推移律(transitivity):任意の\(a,b,c \in P\)に対して、\(a \leq b\)かつ\(b \leq c\)であれば、\(a \leq c\)である。

\(a \leq b\)を\(a\)は\(b\)よりも強くはないということにしよう(弱いか一緒である)。このように定めると、先の5人のテニス選手の集まりを集合\(P\)とした時、彼らの関係を次図のように表すことができる。
f:id:bitterharvest:20161108055209p:plain

図で節点はテニス選手を表し、それぞれの姓の頭文字で表してある。また、強くはないという二項関係は弧で表してある。また、マレーとジョコビッチのように、強弱がつけがたい場合には、マレーはジョコビッチよりは強くないという関係と、ジョコビッチはマレーよりは強くないという関係の両方を用いて表した。

上の図は、先ほどのグラフと似ていないだろうか。そこで、グラフから圏を求めたときと同じ方法で、この前順序集合を圏に変えてみよう。図に示すような手順で求められる。

f:id:bitterharvest:20161108063954p:plain
f:id:bitterharvest:20161108064010p:plain

出来上がった圏は次のようになっている。
1) 対象:選手\(N,R,W,D,M\)
2) 射:強弱関係 \(f,g,h,p,q,r,s,t,u,v,w,z\)、及び、恒等射と合成された射
3) ドメインとコドメイン: \(f:N \rightarrow R, g:R \rightarrow N, h:R \rightarrow W,... \)
4) 恒等射:\(id_N,id_R,id_W,id_D,id_M\)
5) 合成:\(u=h \circ f, \ v=p \circ h, \ w=q \circ p,...\)
① 結合律:\((p \circ h) \circ f = p \circ (h \circ f),...\)であるので満足している。
② 単位律:\(f \circ id_N = f = id_R \circ f,...\)であるので満足している。

2)半順序

位相空間(topological space)を取り上げてみよう。これにはアレルギーがある人も多いことと思う。数学を得意としていた人が、大学に入ってつまずくきっかけとなりやすいのが位相空間だ。ここでは、単に集合と考えてもらっても構わないので、気楽に読んでほしい。

今、全体集合\(X\)と二つの部分集合\(A,B\)が図のように与えられたとしよう。
f:id:bitterharvest:20161108081927p:plain

これから位相空間を作ることにしよう。位相空間を作るためには、開集合あるいは閉集合を用意する必要がある。ここでは、開集合を用意することにしよう。定義は次のようになっている。

位相空間は順序付けられた対\((X,T)\)である。ここで、\(X\)は集合であり、\(T\)は\(X\)の部分集合の集まりで、次の命題を満たす。
1) 空集合\(\{\}\)と\(X\)は\(T\)に属する。
2) \(T\)のメンバーのいかなる(有限でも無限でもよい)論理和も\(T\)に属する。
3) \(T\)のメンバーのいかなる有限の数の論理積も\(T\)に属する。

そこで、先に与えられた集合\(X\)と部分集合\(A,B\)から、\(T\)のメンバーを作成してみよう。
与えられた集合と空集合はメンバーに属すので、\(T\)は取り敢えず\(T=\{\{\},X,A,B\}\)となる。そこで、その他のメンバーを探してみよう。

まず、論理和の方から考えると、\(A \cup B\)が得られる。これを\(C=A \cup B\)と名付けよう。他にはなさそうである。メンバーをどのように取ってきてそれらの論理和を取ったとしても、現在のメンバーのどれかになってしまう。そこで、ここまでに得られたメンバーは\(T=\{\{\},X,A,B,C=A \cup B\}\)である。

次に論理積について考えよう。\(A \cap B\)が得られる。これを\(D=A \cap B\)と名付けよう。他にはなさそうである。ここまでに得られたメンバーは\(T=\{\{\},X,A,B,C=A \cup B,D=A \cap B\}\)である。

新たなメンバーが増えたので、もう一度論理和を考える。新しく加わるものはないので、\(T=\{\{\},X,A,B,C,D\}\)となる。
f:id:bitterharvest:20161108081949p:plain
ここで、最初に与えられた集合から位相空間\((X,T)\)を作成することができた。位相空間では、\(T\)のメンバーは集合なので、含むか含まないかの関係を有する。そこで、\(A \leq B\)を\(A\)は\(B\)に含まれるということにすると、\(T\)のメンバーは順序付けすることができる。これは、以下のようなハッセ図となる。
f:id:bitterharvest:20161108082018p:plain

ハッセ図は、半順序集合になっているのだが、半順序をまだ定義していない。そこで、定めておこう。前順序の時と同じように、集合\(P\)の要素同士を比較しているものとする。半順序集合は次の要件を満たすものである。
1) 反射律(reflexivity):任意の\(a \in P\)に対して、\(a \leq a\)である。
2) 推移律(transitivity):任意の\(a,b,c \in P\)に対して、\(a \leq b\)かつ\(b \leq c\)であれば、\(a \leq c\)である。
3) 反対称律(asymmetry):任意の\(a,b \in P\)に対して、\(a \leq b\)かつ\(b \leq a\)であれば、\(a = b\)である。

反対称律は、二つの要素の間には、高々一つの関係しか存在しないことをうたっている。前順序集合では、二つの要素の間に二つの関係が成り立つことを認めていた。テニス選手の例でいえば、錦織とラオニッチの間は甲乙つけがたいということで、\(N \leq R\)と\(R \leq N\)という二つの関係を用意した。

しかし、半順序集合ではこのようなことは許されない。\(N \leq R\)と\(R \leq N\)であれば、\(N = R\)となり、錦織とラオニッチは同じということになる。二人の異なる人間が同じということは何となく気持ちが悪いので、テニス選手の強さを表す時は、半順序ではなく前順序で表した。

前順序では循環(サイクル)が生じることがあるが、半順序では循環が生じない。一方向に進むグラフとなる。それでは、先のハッセ図から圏を求めてみよう。ここでは、答えだけにすると、下図のようになる。
f:id:bitterharvest:20161108104136p:plain

出来上がった圏は次のようになっている。
1) 対象:集合\(X,E,A,B,C,D\)
2) 射:包含関係 \(f,g,h,k,p,q,r,s,t,u,v,w,x,y\),及び、恒等射と合成された射
3) ドメインとコドメイン: \(f:E \rightarrow D, g:D \rightarrow A, h:D \rightarrow B,... \)
4) 恒等射:\(id_X,id_E,id_A,id_B,id_C,id_D\)
5) 合成:\(u=g \circ f, \ v=p \circ g \circ f, \ w=r \circ p \circ g \circ f,...\)
① 結合律:\((p \circ g) \circ f = p \circ (g \circ f),...\)であるので満足している。
② 単位律:\(f \circ id_E = f = id_D \circ f,...\)であるので満足している。

今、対象\(A\)から\(B\)への写像の集合を\({\rm Hom}(A,B)\)で表すことにしよう。
半順序の場合、任意の二つの接点を結ぶ弧は、最初に説明したように高々一つである。従って、任意の二対象\(A,B\)に対して\({\rm Hom}(A,B)\)の要素の数は高々一つである(空集合かシングルトン)。このように、任意の二対象に対して射の集合の要素の数が高々一つである圏を薄い圏(thin category)という。

薄い圏でかつ循環(サイクル)を有しないとき、全ての射はエピ射であるとともにモノ射である(証明は簡単なので試みること)。集合の世界では、単射全射である時は、逆関数があることが保証されていた(これを双射(bijective)という)。しかし、循環(サイクル)を有しない薄い圏においては、それぞれの弧はエピ射でありモノ射であるにもかかわらず、逆向きの弧は存在しない(循環することはないため)。即ち、逆射に相当するものは存在しない。このため、エピ射・モノ射と全射単射は完全に一致する概念ではない。

3)全順序

最後は全順序である。
全順序集合は次の要件を満たすものである。
1) 反射律(reflexivity):任意の\(a \in P\)に対して、\(a \leq a\)である(この要件は全順序律に含まれるので省いてもよい。ここでは、要件を増やしながら説明しているので、そのまま載せておいた)。
2) 推移律(transitivity):任意の\(a,b,c \in P\)に対して、\(a \leq b\)かつ\(b \leq c\)であれば、\(a \leq c\)である。
3) 反対称律(asymmetry):任意の\(a,b \in P\)に対して、\(a \leq b\)かつ\(b \leq a\)であれば、\(a = b\)である。
4) 全順序律(totality):任意の\(a,b \in P\)に対して、\(a \leq b\)か\(b \leq a\)である。

このように、全ての二つの要素に対して\(\leq\)の関係がつけられるものを全順序と呼ぶ。

建物では、どの二つの階もどちらが高いか比較できるので全順序になっている。二つの階\(A,B\)に対して、\(A \leq B\)を\(A\)は\(B\)よりも高くはないということにすると、図の階\(A,B,C,D\)について、圏を作成すると以下のようになる。
f:id:bitterharvest:20161108104159p:plain

出来上がった圏は次のようになっている。
1) 対象:階\(A,B,C,D\)
2) 射:高低関係 \(f,g,h,u,v,w\),及び、恒等射と合成された射
3) ドメインとコドメイン: \(f:A \rightarrow B, g:B \rightarrow C, h:C \rightarrow D,... \)
4) 恒等射:\(id_A,id_B,id_C,id_D\)
5) 合成:\(u=g \circ f, \ v=h \circ g, \ w=h \circ g \circ f\)
① 結合律:\((h \circ g) \circ f = h \circ (g \circ f)\)であるので満足している。
② 単位律:\(f \circ id_A = f = id_B \circ f,...\)であるので満足している。

4.5 ホモトピー論からの圏

薄い圏が出てきたので、厚い圏(thick category)の例を挙げよう。二つの対象の間に複数の射があるものが厚い圏になるが、どのようなものがあるだろうか。

位相空間が出てきたので、それより、もっと近代的な数学の一分野であるホモトピー論に登場してもらおう。実は、ホモトピー論とコンピュータ科学の関係は、私事になるが、私の研究分野である。これまでにいくつかの論文を執筆してきた。当初は関心をあまり引かなかったようだが、最近では、少しずつではあるが、読んでくれる人が増えてきて喜んでいる。

ホモトピー論にまともに立ち向かうと大変なことになるので、その考え方が伝わるように図を多用して説明しよう。下の図は公園である。公園には池が二つある。それらは青色で示してある。
f:id:bitterharvest:20161108113141p:plain

ある人が黒い地点(ここでは基点と呼ぶことにしよう)にいるとしよう。そして、一回りして基点に戻ってくることにする。例えば、池を巡らない方法は、いくつか考えられるが、例えば、図で示したように赤色と黄色の路がある。
f:id:bitterharvest:20161108113256p:plain

二つの路(Path)は、連続的に変化したときに相手側に行くことができれば、同値な路ということにする。少し工夫すると赤色から黄色の路に連続的に変形して移れることが分かる。
f:id:bitterharvest:20161108113337p:plain

これから、池を巡ってこない路は全て同値ということになる。そこで、池を巡らない路を赤色の路で代表させることにする。

同じように考えると、左側の池を巡ってくる路はすべて同値になる。そこで、池をめぐる路の代表を茶色の路にしよう。
f:id:bitterharvest:20161108114636p:plain

さらに、右の池を巡ってくる路の代表を灰色の路としよう。
f:id:bitterharvest:20161108114733p:plain

これらの路は、お互いに連続な変形をして移ることができないので、独立であるという。この他に独立な路はないであろうか。二つの池をめぐる青色の路はどうであろうか。
f:id:bitterharvest:20161108120851p:plain

結論から言うと、青色の路は独立ではない。茶色の路を辿った後で灰色の路を辿ることで構成される合成された路を考えてみよう。この合成された路からは、下図に示すように、青色の路に連続的に変形できる路が存在する。従って、青色の路は独立とはならない。
f:id:bitterharvest:20161108121443p:plain

それでは、この公園でのホモトピー的な路を圏にしてみよう。図で示すと次のようになる。
f:id:bitterharvest:20161108130748p:plain
図において、池を巡らない赤色の路は恒等射となる。これは、赤色の路は縮めていくと基点と一致してしまう。これは、基点から動かないことと同じなので、恒等射となる。独立な茶色と灰色の路は、射となる。これらは\(f,g\)で表すことにする。また、赤色の路と灰色の路を何回かたどるのも基点から基点へ至る路であるので、\(f\)と\(g\)を合成したもの(何回用いてもよい)も射である。


1) 対象:基点\(X\)
2) 射:路 \(f, g, s,t,u,.., \)
3) ドメインとコドメイン: \(X\)
4) 恒等射:池を巡らない路(赤色の路)\(id_X\)
5) 合成:\(\circ\)(基点を軸にして何度も路を回る)
① 結合律:満足している(路の周りかたは、結合の仕方によらない)。
② 単位律:\(f \circ id_X = f = id_X \circ f,g \circ id_X = g = id_X \circ g\)であるので満足している。

路から作られる圏は、無数の射を有していることが分かる。これは、厚い圏の例である。

次の図はトーラスである。
f:id:bitterharvest:20161108134404p:plain
このトーラスと先ほどの公園とは全く違うものに見えるかもしれない。しかし、トーラスの一点を基点とし、そこで、路を考えてみよう。路の一つは、トーラスの穴に沿ってぐるりと回ってくるもの(図では\(f\))である。他の一つは、それとは直角方向にぐるりと回るもの(図では\(g\))である。恒等射となる路は、一周せずに戻ってくるもの(図では\(id_X\))である。他の路はこれらの合成により作られる。従って、トーラスと先ほどの公園は、同じ圏となる。トーラスは3次元で、公園は2次元であるにもかかわらず同じものとみなされる。何とも面白い世界をホモトピー論は扱う。

トーラスだけでなく、下図に示すカップクラインの壺も同じ圏になる。
f:id:bitterharvest:20161109071318p:plain

ホモトピー論の世界では基本群などを用いて物体の性質を表す。トーラスやクラインの壺の基本群は\({\mathbb Z} \times {\mathbb Z}\)である。\({\mathbb Z}\)は整数を表すが、ここでの意味は、回ってくる回数を表す。

公園の場合には、池の周りをまわる回数となる。逆回りの時は回数を減らすものと考える。従って、回数は、負を含むので、自然数ではなく整数となる。左の池と右の池を回ることは独立であったので、独立な回り方が二通りあるということで、\({\mathbb Z} \times {\mathbb Z}\)と表す。

最近のホットな数学の分野はホモトピー・タイプ論(homotopy type theory)である。この分野でタイプの研究が進むと、Haskellは多大な恩恵を受けるものと期待している。

集合から圏論へ:ミクロな世界からの宇宙への旅立ち(2)

3.2 集合の要素を射に

集合の世界は要素を中心に考えるが、圏論の世界は射を中心に考える。そこで、集合を考えるとき、その要素を射に変えるものが必要となる。
その役割を果たしてくれるのが、一つだけの要素からなる集合である。この集合はシングルトンと呼ばれる。Haskellではデータ型()として用意している。

確認してみよう。まず、()のデータ型を調べるてみよう。

Prelude> :i ()
data () = () 	-- Defined in ‘GHC.Tuple’
instance Bounded () -- Defined in ‘GHC.Enum’
instance Enum () -- Defined in ‘GHC.Enum’

instance Eq () -- Defined in ‘GHC.Classes’
instance Ord () -- Defined in ‘GHC.Classes’
instance Read () -- Defined in ‘GHC.Read’
instance Show () -- Defined in ‘GHC.Show’
instance Monoid () -- Defined in ‘GHC.Base’

ちなみに、\(Int\)と\(Bool\)のデータ型を調べると次のようになっている。

Prelude> :i Int
data Int = GHC.Types.I# GHC.Prim.Int# 	-- Defined in ‘GHC.Types’
instance Bounded Int -- Defined in ‘GHC.Enum’
instance Enum Int -- Defined in ‘GHC.Enum’
instance Eq Int -- Defined in ‘GHC.Classes’
instance Integral Int -- Defined in ‘GHC.Real’
instance Num Int -- Defined in ‘GHC.Num’
instance Ord Int -- Defined in ‘GHC.Classes’
instance Read Int -- Defined in ‘GHC.Read’
instance Real Int -- Defined in ‘GHC.Real’
instance Show Int -- Defined in ‘GHC.Show’
Prelude> :i Bool
data Bool = False | True 	-- Defined in ‘GHC.Types’
instance Bounded Bool -- Defined in ‘GHC.Enum’
instance Enum Bool -- Defined in ‘GHC.Enum’
instance Eq Bool -- Defined in ‘GHC.Classes’
instance Ord Bool -- Defined in ‘GHC.Classes’
instance Read Bool -- Defined in ‘GHC.Read・f
instance Show Bool -- Defined in ‘GHC.Show’

データ型\(Bool\)は\(data \ Bool = False | True\)となっているので、\(False\)と\(True\)二つの要素を取ることが分かる。また、\(Int\)については、その要素はGHC.Typesで定義されていることが分かる。
遡って、シングルトンは、\(Data () = ()\)となっていたので、要素も()であることが分かる。確認してみよう。

Prelude> :t ()
() :: ()

である。::の左側は要素の()で、右側はデータ型の()である。

シングルトンを用いると、集合の要素を射に変えることができる。それは、シングルトンから集合の要素\(a\)へ向かう矢印\(f_a\)を用意することで実現できる。下図は、自然数の集合の各要素を射で表したものである。この場合、射の数は無数になる(但し、数え上げることはできる)。
f:id:bitterharvest:20161105091506p:plain

シングルトンは要素の数が一つだけの集合である。それでは、要素の数がゼロの集合はあるのだろうか。これは、数の議論に似ている。建物で、地面に接している階を、日本では1階にしているが、海外に行くと0階(あるいはGround)となっていることに戸惑った人は多いことだろう。普段の癖で思わず1階のボタンを押してしまい、エレベータのドアーが空いたときそこがロビーでないことにビックリした経験を誰しも持っているのではないだろうか。

数学の世界では、要素の数がゼロの集合が存在する。これは空集合と呼ばれる。前の記事で述べたが、Haskellの世界では\(Void\)として用意している(但し、これを用いるときは、Data.Voidというモジュールをインポートする必要がある)。それでは確認してみよう。

Prelude> import Data.Void
Prelude Data.Void> :i Void
data Void 	-- Defined in ‘Data.Void’
instance [safe] Eq Void -- Defined in ‘Data.Void’
instance [safe] Ord Void -- Defined in ‘Data.Void’
instance [safe] Read Void -- Defined in ‘Data.Void’
instance [safe] Show Void -- Defined in ‘Data.Void’

\(Void\)と呼ばれるデータ型があることが分かる。

\(Void\)は要素が何もないので、真空の状態と見なすことができる。宇宙が真空の状態から埋めれたように、空から有が生じると考えることができる。あるいは、論理の世界では、\(Void\)は偽(\(false\))、シングルトンは真(\(true\))と見なす。論理の世界では、嘘の上にどのようなことを述べたとしても正しいと考えるので、偽からはどのような事でも生みだせると考える(ここでは生み出すものはデータ型)。その役割をしているのが\(absurd\)という関数である。確認してみよう。

Prelude Data.Void> :t absurd
absurd :: Void -> a

\(absurd\)という関数は、任意のデータ型を出力していることが確認できた。\(absurd\)の意味は、常識に反した、理屈に反する、非条理ななどだ。このようなわけで、この関数は用意してあるだけで使うことはまずない。ここでは、それぞれのデータ型はこれから生み出されたと考えておくことにしよう。

要素の数が0と1の集合について話したので、ついでに要素数が2である集合は何であろうか。勿論、偽と真の値を有するブール集合である。Haskellではデータ型\(Bool\)として用意しているが、有用性が高いことは言うまでもない。

集合から圏論へ:ミクロな世界からの宇宙への旅立ち(1)

3.集合の世界から圏論の世界へ

ごちゃごちゃしたミクロの世界をかなぐり捨てて雄大なマクロの世界(宇宙)へ旅立つことにしよう。数学でのミクロな世界は集合で、雄大な宇宙は圏論である。集合は、要素の集まりとして定義される。また、集合を論じるときは、それと一緒に関数(あるいは写像)も学んだことと思う。関数は二つの集合を結びつけるものだ。

圏論では、集合に相当するものを対象(object)と呼び、関数に相当するものを射(morphism)と呼ぶ。集合を論じるときは、ミクロな世界なので、その中に含まれている要素がとても重要な役割をする。しかし、圏論では、マクロな世界を論じるので、対象の中身は気にしないことにする。Haskellでは、対象をタイプとして表すが、常に、タイプを気にし、その構成要素に対してはたいして注意を払わない。

今回の記事では、集合を捨てて対象に、関数を捨てて射に、乗り移れるための準備をしよう(Haskellでは、射を関数と呼んでいるので、少しごちゃごちゃするのだが、Haskellで関数といっているときは、圏論での射と思って欲しい)。

3.1 関数とは

物事を始めるにあたっては、その中で用いる用語の意味をはっきりさせておく必要がある。そこで、ここでは、関数とは何かをはっきりさせておこう。これは、射との違いを考えるうえで助かるはずだ。

少し、基礎的な概念から始めてみよう。プログラムを書くからには、そこに何らかの意思あるいは意図があるはずである。出来上がったプログラムは、そこに、作者の意思あるいは意図を実現しているはずである。そこで、プログラムを書く際に、どのようにして、意思や意味を埋め込んでいくのかが重要になる。

JavaやCのような命令型(imperative)のプログラミング言語を用いれば、意思や意図は、「プログラムのシークエンス(how)」として埋め込まれることになる。HaskellPrologのような宣言型(declarative)のプログラミング言語を用いれば、それらは「どのようなものであるか(what)」として埋め込まれることになる。

宣言型の説明のところが分かりにくいので、補足しておいた方がよいと思う。プログラムはある特定の領域あるいは特定の問題を対象にして記述される。特定の領域には、それを細かく分解していくと、それ以上は分解できない構成要素(atom)が出てくる。そこで、構成要素の性質あるいは性格をプログラムとして記述する。分解したのとは逆に構成要素を用いて組み立てていくと元の領域に戻るはずである。この作業は構成要素を記述したプログラムの合成で実現する。

例えば、量子力学の世界を考えると、最小の単位は粒子である。これは、数学的にはケットとブラと表すことができる。そこで、ケットとブラを対象(データ型)として表す。ケットとブラに成り立っている関係を射(関数)とその合成で表すことで、量子力学の世界をプログラムで提供できるようになる。

通常、コンピュータ科学の世界では、意思や意図は、意味という。命令型のプログラミング言語を用いて意味を表す方法を操作的意味論(operational semantics)といい、宣言型に対しては表示的意味論(denotational semantics)という。表示的という言葉が使われているのは、構成要素をその特定領域での言葉を用いて表現しているということによる。

1)関係

それでは、表示的意味論での実際的な例を考えることにしよう。代表的なものは、そして、最も多くの場面で使われるのは、関係(relation)だろう。通常の文章でもよく出てくるし、コンピュータの世界でも、データベースでは基本的な概念になっている。例えば、合コンをした時に、望ましいと思った異性をあげることは一つの関係を表している。

ある男性は、一人の女性を望ましいと思ったかもしれない。また、別の男性は、気が多いのか、複数の女性を考えたかもしれない。また、さらに別の男性は、好みがうるさいのか、誰もいなかったというかもしれない。逆に女性側から見たときも、同じようなことが起こる。とても運がよい場合には、二人の好みが合致する場合もある。このような関係を示したのが次の図だ。
f:id:bitterharvest:20161102151424p:plain

数学では、関係は直積集合(Cartesian product)で表される。例えば、次のようになる。男性の方から女性を望ましいと思う関係は、例えば、次のように表される。
\begin{eqnarray}
(Peter, Marry) \\
(Peter, Betty) \\
(Mike, Ellen) \\
(Tom, Michell)
\end{eqnarray}

コンピュータでは、関係は、多くの場合、前述したようにデータベースとして表される。

2)関数

関係は、とても一般的で、二つのグループの関わり合いを形式的に記述するのに都合がよい。しかし、あまりにも自由度が大きすぎるため、数学的な枠組みで使おうとするときは、もう少し、制限の強い概念を用いたほうが都合のよいことが多い。これが関数と呼ばれる。関数は、ドメイン(domain)とコドマイン(codomain)の関係を表すが、そこには、いくつかの約束事がある。
1) 関数には方向性がある。ドメインとコドメインの関係は対等ではなく、矢印がドメインからコドメインの方に向かっている。
2) ドメインの全ての要素に対して、コドマインのある一つの要素が対応する。先ほどの合コンでの例で、好ましい女性がいなかったという男性がいたが、このようなことは許されず、全ての男性は好ましい女性がいなくてはならない。また、複数の女性を好ましいと思う男性がいたがこのようなことも許されない。但し、一人の女性に対して、複数の男性が好ましいと思うこと(多対一対応)は許される。
f:id:bitterharvest:20161102162603p:plain

次の例は、全ての男性が女性一人だけを望ましいといっているので、この関係は関数となる。コドメインの中で、相手のある要素の集まりをイメージという。これらの関係を図で示すと次のようになる。
f:id:bitterharvest:20161102163604p:plain
ドメインの全ての要素に対して相手が割り当てられているような関数を全関数(total function)という。これに対して、ドメインのあるものに対しては相手が割り当てられていないような関数を部分関数(partial function)という。これから扱うすべての関数は全関数である。関数でないものの例を以下に示す。
f:id:bitterharvest:20161103153858p:plain

また、関数の値は変わることがない。このような関数を純粋関数(pure function)という(日本語では純粋関数という用語は使わないようだがここでは便宜的に使う。純粋関数型言語という使い方はあるが、それ以外で使われる例はないようである)。人間の脳は純粋関数ではない。相手に対する好き嫌いは、様々な経験を通して、変化することが往々にしてある。このため、一定であるという保証をすることはできない。一般に、記憶を持つようなものは純粋関数にはならない。

3)逆関数

先ほどは、男性の方から好ましい女性を示す関数\(f\)について説明した。それでは、女性の方から好ましい男性を示す関数\(g\)を考えてみよう。

そこで、関数\(f\)を使って望ましい女性を得た後、関数\(g\)を使ってその女性が、好ましいと思っている男性を求めてみることにしよう。もし、その結果が自身に戻ってくるんであれば、お互いに好ましいと思っているので、こんなに嬉しいことはない。もしすべての男性について、自身に戻ってくるようであれば、男性たちはとても幸せだろう。

逆に、女性の方からも考えてみよう。その場合には、関数\(g\)を施した後で、関数\(f\)を施すことになる。やはり、全ての女性について、自身に戻ってくるようであれば、女性たちはとても幸せだろう。

このようなことが、男性にも、女性にも起こっているとすると、この合コンは理想的な結果をもたらしたといえるだろう。

このことを数学的に記述すると、男性の場合には、
\begin{eqnarray}
g \circ f = id_M
\end{eqnarray}
女性の場合には、
\begin{eqnarray}
f \circ g = id_F
\end{eqnarray}
となる。ここで、\(id_M\)は男性のグループに対する恒等写像、\(id_F\)は女性のグループに対する恒等写像である。

圏論での用語を用いて上記を記述しなおすと、
1) 対象:\(M\)(男性のグループ)、\(F\)(女性のグループ)
2) 射:\(f:M \rightarrow F\)(好ましい女性への射)、\(g:F \rightarrow M\)(好ましい男性への射)
3) 恒等射:\(id_M\)(男性のグループでの恒等射)、\(id_F\)(女性のグループでの恒等射)
4) 合成:\(g \circ f\)、\(f \circ g\)

圏論の世界での同型写像の定義】
一般に、
\begin{eqnarray}
f:M &\rightarrow & F\\
g:F &\rightarrow & M\\
g \circ f &=& id_M \\
f \circ g &=& id_F
\end{eqnarray}
が成り立つとき、\(f\)と\(g\)は同型写像であるという。数学の概念の中では、重要な概念の一つである。

ここでのポイントは、合成\(\circ\)と恒等射\(id\)を用いて、同型写像が定義されていることである。

上記では、\(M,F,f,g\)について具体的な例を挙げて説明したが、これらはどのようなもので置き換えてもよい。その時、上記のような関係が成り立つとき、\(f\)と\(g\)は同型写像という。また、\(M\)と\(F\)は同型という。

圏論による同型写像の定義の重要性は、対象の要素については何も問うていないことである。細かいことは気にしなくてもよいといっている。また、写像が射に変わっている点である。射は写像を超えるもっと普遍的な概念である。このため、より抽象的な同型を定義するのに役立つ(後々、関手(functor)の話をするがこれは写像をより一般化したものである)。

同型写像は、対象同士が対称(symmetric)であると教えてくれる。あるいは、行って戻れる、すなわち、可逆的(invertible)であるとも知らせてくれる。

同型写像は変化のないモノトーンの世界なのだが、世の中でこのようなことが成り立つことは少ない。同型写像にならない条件は次の通りである。
1) 対象のサイズが異なる。先の合コンの場合、男性と女性の数が異なる場合には、1対1のペアを作ろうとしても、数の多い性の人の誰かが余ってしまう。
2) 多対一対応のものがある。即ち、コドメインのある要素が、二つ以上のドメインの要素から写像される。即ち、\(\exists a, \exists b, a \neq b, f(a) = f(b)\)である。この場合には、二人以上の男性からある女性が望ましいと思われても、その女性は一人の男性しか望ましいと思えないので、破綻してしまうこととなる。

圏論の世界での逆写像の定義】
関数\(f\)と\(g\)が同型写像である時、\(g\)は\(f\)の逆関数であるという。\(f\)の逆関数は\(f^{-1}\)と書く。同様に、\(f\)は\(g\)の逆関数であるという。

逆関数の例を挙げておこう。今、自然数(0から始まる)と奇数(1から始まる)のグループを考えることにする。自然数のグループの方が奇数のそれよりも2倍だけサイスが大きいと考えがちだが、そんなことはない。
f:id:bitterharvest:20161103105346p:plain
自然数\(x\)から奇数\(y\)への写像\(f\)を\(y=f(x)=2x+1\)とし、奇数から自然数への写像\(g\)を\(x=g(y)=(y-1)/2\)とすると、
\begin{eqnarray}
g \circ f &=& id_\mathbb{ N } \\
f \circ g &=& id_{Odd}
\end{eqnarray}
が成り立つ。

このことから、自然数と奇数は同型であることが分かる。奇数は自然数の部分集合だと思うとなんか変な気分になる。要素の数が無限の時、往々にして、直感が働かない時があるが、これはその例の一つである。

4)全射単射

圏論の世界から、もう一度、集合の世界に戻ってみよう。この世界では、集合の要素同士がどのように写像されているかが、重要な話題だ。その中から、全射(surjective)と単射(injective)に登場してもらおう。

Surjectiveという単語は、日常会話で用いることはまずないので、初めて見たという人も多いことと思う。この単語は、surとjectiveをつなげたもので、surは、surfaceからも連想できるように、「上」という意味を持つ。語源は古フランス語だ。また、jectは「投げる(の過去分詞)」を意味し、ラテン語を語源としている。

従って、Surjectiveは形容詞「上に投げられた」となる。これに対応した全射という日本語訳もあまり好きではないのだが、関数\(f:A \rightarrow B\)のイメージ\(f(A)\)とコドメイン\(B\)が一致するとき、即ち\(f(A) = B\)のとき、関数\(f\)は全射であるという。

Injectiveは同じようにinとjectiveの合成語である。inは「中へ」という意味だが、この場合には、「そのまま中へ」という意味が含まれているのであろう。別の要素は別の要素へ移すというニュアンスで用いられている。従って、ドメインの任意の二要素\(a,b\)に対して\(a \neq b\)の時、\(f(a) \neq f (b) \)が成り立つならば、関数\(f\)は単射であるという。

単射全射の例をいくつか示しておこう。
f:id:bitterharvest:20161103153213p:plain

集合の世界では、この二つの用語を用いて、先ほどの同型写像が定義される。

【集合の世界での同型写像の定義】
関数\(f:A \rightarrow B \)が、全射単射である時、\(f\)は同型写像であるという。

【集合の世界での逆写像の定義】
関数\(f\)が同型写像である時、その矢印の向きを反対にした写像を\(f\)の逆写像という。これを\(f^{-1}\)と記す。

皆さんは、集合の上に定義された同型写像と、圏論の上でのそれとではどちらが好きだろうか。集合の上での定義はアセンブリ言語による定義で、圏論の上での定義は高級言語でのそれに相当すると思うのだが、どうだろうか。

5)エピ射とモノ射

同型写像と逆写像圏論の世界と集合の世界でそれぞれ定義したので、全射単射についても圏論の世界で定義してみよう。

まず、全射の方から始める。集合の世界はラテン系の言語を語源としていたので、圏論ではギリシャ系の語源を用いることで、用語も変えてみることとする。ラテン系のチャラチャラした世界からギリシャ系の哲学の世界へ移ろうというと物議を醸しそうだが、気分としてはそんなところだ。surに対応するギリシャ語はepiである。そこで、全射をエピ射(epimorphism)ということにしよう。

また、単射の方は、単一を表すギリシャ語monoを用いてモノ射(monomorophism)ということにする。

それでは、エピ射を定義してみよう。

エピ射でないとすると下図のようなことが発生する。
f:id:bitterharvest:20161103165130p:plain

今、射\(f:A \rightarrow B \)がエピ射でないとする。この時、
\begin{eqnarray}
(g_1 \circ f = g_2 \circ f) \land (g_1 \neq g_2)
\end{eqnarray}
を成り立たせるある対象\(C\)と二つのある射\(g_1: B \rightarrow C\)と\(g_2: B \rightarrow C\)が存在する。
(上の記述を論理式で念のために表しておこう。
\begin{eqnarray}
\exists C,\exists g_1(g_1: B \rightarrow C),\exists g_2(g_2: B \rightarrow C) \\
f(f:A \rightarrow B) \notin Epimorphisms \Rightarrow \\
(g_1 \circ f = g_2 \circ f) \land (g_1 \neq g_2)
\end{eqnarray}
)

そこで、この対偶を取ってみよう。次のようになる(飛躍しすぎだと思う人は、上記の論理式より導き出してほしい)。
全ての対象\(C\)と全ての二つの射\(g_1\)と\(g_2\)に対して
\begin{eqnarray}
(g_1 \circ f \neq g_2 \circ f) \lor (g_1 = g_2)
\end{eqnarray}
の時、射\(f:A \rightarrow B \)はエピ射となる。

そこで、
\begin{eqnarray}
(g_1 \circ f \neq g_2 \circ f) \lor (g_1 = g_2)
\end{eqnarray}
の部分は次のように変えることができる。
\begin{eqnarray}
(g_1 \circ f = g_2 \circ f) \Rightarrow (g_1 = g_2)
\end{eqnarray}
これを利用して次のように定義することができる。

【エピ射の定義】
射\(f:A \rightarrow B \)が次の条件を満たす時、エピ射という。
条件:全ての対象\(C\)と全ての二つの射\(g_1: B \rightarrow C\)と\(g_2: B \rightarrow C\)に対して、
\begin{eqnarray}
g_1 \circ f = g_2 \circ f
\end{eqnarray}
が成り立つとき、
\begin{eqnarray}
g_1 = g_2
\end{eqnarray}
である。[条件終了]


モノ射については、同じように考える。
f:id:bitterharvest:20161104000815p:plain

今、射\(f:A \rightarrow B \)が上図に示すようにモノ射でないとする(上図では異なる矢印のみを記してある。記してない部分は、二つの矢印とも同じ場所から出発して同じ場所にたどり着くので、適当に補って考えること)。この時、
\begin{eqnarray}
(f \circ g_1 = f \circ g_2) \land (g_1 \neq g_2)
\end{eqnarray}
を成り立たせるある対象\(C\)と二つのある射\(g_1: C \rightarrow A\)と\(g_2: C \rightarrow A\)が存在する。

そこで、この対偶を取る。次のようになる。
全ての対象\(C\)と全ての二つの射\(g_1\)と\(g_2\)に対して
\begin{eqnarray}
(f \circ g_1 \neq f \circ g_2) \lor (g_1 = g_2)
\end{eqnarray}
の時、射\(f:A \rightarrow B \)はモノ射となる。

そこで、
\begin{eqnarray}
(f \circ g_1 \neq f \circ g_2) \lor (g_1 = g_2)
\end{eqnarray}
の部分は次のように変えることができる。
\begin{eqnarray}
(f \circ g_1 = f \circ g_2) \Rightarrow (g_1 = g_2)
\end{eqnarray}
これを利用して次のように定義することができる。

【モノ射の定義】
射\(f:A \rightarrow B \)が次の条件を満たす時、モノ射という。
条件:全ての対象\(C\)と全ての二つの射\(g_1: C \rightarrow A\)と\(g_2: C \rightarrow A\)に対して、
\begin{eqnarray}
f \circ g_1 = f \circ g_2
\end{eqnarray}
が成り立つとき、
\begin{eqnarray}
g_1 = g_2
\end{eqnarray}
である。[条件終了]

エピ射、モノ射とも対象の要素を使うことなく、関数の合成だけで定義しているところが大きな特徴である。集合でのそれと比べてどうであろうか。すっきりと見えるのではないだろうか。

追伸

エピ射もモノ射も全ての\(C\),\(g_1\),\(g_2\)に対してといっている。宇宙に存在するものすべてからだと、いつ証明が済むのかなと思ってします。これに代わるものを用意しておいて、この対象とこの射について調べてくれればいいよというものがあると助かる。そこで、これらを用意することにしよう。

モノ射:対象はシングルトンの空間、即ち、\(C=()\)とする。射は、\(A\)の各要素への写像、即ち、\(g_a:() \rightarrow a \in A\)とする。

エピ射:対象は2要素からなる空間、即ち、\(C=\{c_1,c_2\}\)とする。射は、\(B\)を2分し、一方を対象の一つに、他方を対象の残りに写像する、即ち、\(g_{B'}: (B' \subset B \rightarrow c_1 \lor B-B' \rightarrow c_2) \)とする。これの目的は次のようである。対象を2分し、別々の値に写像することで、全ての関数が異なるようになる。なお、\(C\)はブール値の集合\(\{true,false\}\)と考えてもよい。

射は閉じている:どれだけ合成しても大丈夫

2.4 合成

1)圏の構成要素

ここでは、これから説明する合成も含めて、圏を構成する要素についてまとめておこう。圏は次の構成要素から成り立つ。
1) 対象\(A,B,C,..\)を有する。
2) 射\(f,g,h,..\)を有する
3) 射はドメインとコドメインを有する。なお、ドメイン、コドメインは対象である。例えば\(f: A \rightarrow B, g: B \rightarrow C, h: A \rightarrow C,..\)
4) すべての対象に対して恒等射が存在する。\(1_A: A \rightarrow A, 1_B: B \rightarrow B, 1_C: A \rightarrow C,..\)
5) コドメインドメインが一致している射は合成でき、それも射となる。\(f: A \rightarrow B\)で \(g: B \rightarrow C\)ならば、\(g \circ f : A \rightarrow \)が存在する。

なお、圏は次の二つの法則を満たさなければならない。
① 結合律:合成が複数あるとき、結果は、その計算の順序によらず、どれから計算しても同じである。即ち、\( (f \circ g) \circ h = f \circ (g \circ h) \)である。
② 単位律:恒等射とある射の合成は、ある射となる。即ち、\(f \circ 1_A = f = 1_B \circ f\)である。この法則があるため、恒等写像であっても、恒等射とはならないものがある。これについては後で述べる。

2)合成

今回の話題は、圏を構成する要素の中で、合成と呼ばれるものである。合成は、射と射とを結びつけるものである。

射のところで話をしたように、射は、ドメインとコドメインを有する。一つの射のコドメインと他の一つの射のドメインが同じ対象である時、この二つの射は合成できる。

例えば、射\(f:A \rightarrow B,g:B \rightarrow C\)があった時、射\(g \circ f : A \rightarrow C\)が存在する。この時、\( \circ \)を合成という。

いくつか例を挙げよう。

① 対象\(A\)を実数とし、対象\(B\)を整数とし、対象\(C\)を自然数(0から始まる)とし、射\(f:A \rightarrow B\)は小数点以下を四捨五入する関数であるとし、射\(g:B \rightarrow C\)は絶対値をとる関数とする。この時、射\(g \circ f : A \rightarrow C\)は、中心(0)からの整数値による距離(負の値はとらない)を表す。

② 少し変わった合成を定義してみよう。射は自然数(1から始まる)としよう。対象は星とし、すべての射のドメインであり、コドメインであるとする (星なので、対象はあまり意味を持たないと考えてよい)。即ち、任意の自然数\(i\)に対して、\(i : \star \rightarrow \star \) である。
f:id:bitterharvest:20161011123105p:plain

合成は、\(i \circ j : \star \rightarrow \star \)となる。そこで、合成\( \circ \)を乗算とみなしてみよう。即ち、\(i \circ j = i \times j\)とする。この時、合成された射は、これらの射を掛けたものと同じになる。例えば、射3と5の合成は射15と同じである。即ち、\(5 \circ 3 = 15\)である。すべての射は、星自身への射(恒等写像)であるが、単位律を満たすのは1だけなので、これらの射の中で、1だけが恒等射となる。

③ 射を0から始まる自然数とし、合成を加算とみなすと、合成された射は、これらの射を加えたものとなる。例えば、射3と5の合成は射8と同じである。上記と同様の理由で、これらの射の中で、0だけが恒等射となる。

④ 射を\(+n\),\(n\)は自然数(0から始まる)としよう。また、対象を自然数(0から始まる)とし、すべての射のドメインであり、コドメインであるとする。(図参照)射\(+m\)と\(+n\)の合成は\(+(m+n)\)となる。即ち、\(+m \circ +n = +(m+n)\)である。また、恒等射は\(+0\)である。

⑤ 同様に射\(*n\)を考えることができる。この時は、\(n\)は 1から始まる自然数である。この時、恒等射は\(+1\)である。

3)Haskellで実現する。

それでは、Haskellで実現しよう。①から始めよう。0から始まる自然数を必要とするが、これは、前の記事で説明したものを利用する。

data Nat0 = Zero | Succ Nat0 deriving (Read, Eq, Ord, Show)

toNat0 :: Int -> Nat0
toNat0 m
  | m == 0 = Zero
  | m > 0  = Succ $ toNat0 (m - 1)
  | otherwise = error "Not Natural Number"

fromNat0 :: Nat0 -> Int
fromNat0 Zero   = 0
fromNat0 (Succ n) = 1 + fromNat0 n

それでは、四捨五入する関数\(round’\)を定義しよう。Haskellにも\(round\)という関数が用意されているが、この関数は、小数点の数が0.5の時、整数部が偶数のときは切り捨てを、奇数の時は切り上げをする。このため、四捨五入とはなっていないので、次のように定義する。

round' :: Double -> Int
round' m 
  | m >= 0 && m - fromIntegral (floor m) >= 0.5 = 1 + floor m
  | m >= 0 = floor m
  | m - fromIntegral (floor m) > 0.5 = 1 + floor m
  | otherwise = floor m

ここで、\(floor\)は小数点の切り捨てを行う。ただし、負の数の時は、整数部から1引いたものとなる。即ち、\(floor (-4.*)=-5\)である。では、\(round’\)を調べてみよう。

*Main> round' (-3.6)
-4
*Main> round' (-3.5)
-4
*Main> round' 3.6
4
*Main> round' 3.5
4
*Main> round' 3.4
3
*Main> round' 2.6
3
*Main> round' 2.5
3
*Main> round' 2.4
2
*Main> round' (-2.4)
-2
*Main> round' (-2.5)
-3
*Main> round' (-2.6)
-3
*Main> round' (-3.4)
-3
*Main> round' (-3.5)
-4
*Main> round' (-3.6)
-4

それでは、絶対値を求める関数を求めよう。整数を自然数に移すことに注意して、次のようにしよう。

abs' :: Int -> Nat0
abs' m
  | m >= 0 = toNat0 m
  | otherwise = toNat0 (-m)

大丈夫だと思うが、確認してみよう。

*Main> abs' 3
Succ (Succ (Succ Zero))
*Main> abs' 2
Succ (Succ Zero)
*Main> abs' 1
Succ Zero
*Main> abs' 0
Zero
*Main> abs' (-1)
Succ Zero
*Main> abs' (-2)
Succ (Succ Zero)
*Main> abs' (-3)
Succ (Succ (Succ Zero))

それでは、\(round’\)と\(abs’\)の合成関数\(distance\)を次のように定義しよう。

distance = abs' . round'

少し動かしてみよう。

*Main> distance 3.6
Succ (Succ (Succ (Succ Zero)))
*Main> distance 3.5
Succ (Succ (Succ (Succ Zero)))
*Main> distance 3.4
Succ (Succ (Succ Zero))
*Main> distance 2.6
Succ (Succ (Succ Zero))
*Main> distance 2.5
Succ (Succ (Succ Zero))
*Main> distance 2.4
Succ (Succ Zero)
*Main> distance (-2.4)
Succ (Succ Zero)
*Main> distance (-2.5)
Succ (Succ (Succ Zero))
*Main> distance (-2.6)
Succ (Succ (Succ Zero))
*Main> distance (-3.4)
Succ (Succ (Succ Zero))
*Main> distance (-3.5)
Succ (Succ (Succ (Succ Zero)))
*Main> distance (-3.6)
Succ (Succ (Succ (Succ Zero)))

このままだとわかりにくいので、\(fromNat0\)を用いて、整数に変換してみてみよう。

*Main> fromNat0 . distance $ 3.6
4
*Main> fromNat0 . distance $ 3.5
4
*Main> fromNat0 . distance $ 3.4
3
*Main> fromNat0 . distance $ 2.6
3
*Main> fromNat0 . distance $ 2.5
3
*Main> fromNat0 . distance $ 2.4
2
*Main> fromNat0 . distance $ -2.4
2
*Main> fromNat0 . distance $ -2.5
3
*Main> fromNat0 . distance $ -2.6
3
*Main> fromNat0 . distance $ -3.4
3
*Main> fromNat0 . distance $ -3.5
4
*Main> fromNat0 . distance $ -3.6
4

次に、②と③は脇において、④について考えよう。④では、対象を自然数としていたが、ここでは数に変えてみよう(数には整数、有理数無理数などが含まれる。分かりにくければ、整数と考えてもここでは問題ない)。このように変えると、Haskellで用意している\(+n\)を用いることができる。

(+3)という関数は次のようになっている。さらに、この関数に4を入力する。結果は、4に3くわえられた値7が出力される。

*Main> :t (+3)
(+3) :: Num a => a -> a
*Main> :t (+3) 4
(+3) 4 :: Num a => a
*Main> (+3) 4
7

それでは、合成関数を作ってみよう。+3と+4を合成した関数に、値4を入力してみる。

*Main> (+3) . (+4) $ 4
11

いくつもの関数を合成してみよう。

*Main> (+1) . (+2) . (+3) . (+4) . (+5) $ 4
19

同じことが⑤にも言える。

量子力学とHaskell - 目次 

1.身近な存在としての量子力学(1):MRI(磁気共鳴画像診断装置)(2):ヨーロッパコマドリ(3):進化論(4):光合成(5):光合成(続き)(6):光合成(続き)(7):ケット(8):ケットをHaskellで表現する(9):重ね合わせ(10):重ね合わせ(続き)、(11):ブラ(12):ブラをHaskellで記述する(13):ブラでの重ね合わせをHaskellで表現する(14):重ね合わせの内積
2.量子力学の初歩をHaskellで学ぶ(1):ブラとケット(2):重ね合わせ
3.量子力学の世界を垣間見る(1):粒子が相互に交換されるモデルの概略(2):粒子が相互に交換されるモデル:有限系(3):粒子が相互に交換されるモデル:周期系(4):ボーズ粒子とフェルミ粒子(5):離散系から連続系へ(6):ハミルトニアンの活用(7):シュレディンガー方程式

ハロウィン・パーティーで闊歩するバターナッツかぼちゃのポタージュ

月末はハロウィンだ。もともとは古代ケルト人の収穫を祝うお祭りのようだが、いつの間にか、日本にも定着したようで、テレビでは、連日、渋谷での警戒態勢を伝えている。

ハロウィンが近づくと、日本からルイジアナ州バトンルージュへ留学していた高校生が、仮装姿で間違って別の家に足を踏み入れたことで起きた悲しい事件がいつも思い出される。招待されている家だと思い込んでいた彼は、「フリーズ」と言われたのに「プリーズ」と聞き間違えたのだろう。銃を向けられていたにもかかわらず、歩き続けてしまい、射殺されてしまった。

この話は、もう4半世紀も前のことなので、ハロウィンにまつわる嫌な思い出は若いお母さんたちにはないようである。昨日出かけたショッピングセンターでは、幼稚園あるいは小学校の低学年の子供たちを伴った幾組みもの母と子が、狂騒的な声を張り上げながら、ハロウィン用の帽子を買い求めていた。

このような光景に出会って、ハロウィンが印象付けられたのだろう、食品売り場を物色している時に、カボチャ売り場で思わず足が止まった。ひときわ輝いているカボチャに出会ったからだ。他のカボチャとの差を見せつけようと、少しだけ高い場所に、しかも、竹で編んだざるの中に神々しく鎮座させられているものを発見した。

カボチャ売り場に置かれていなければ、瓢箪と間違えたであろう。形だけではなく、色も乾燥させた瓢箪とそっくりな薄い黄色だ。なんだろうと思いながら、また、どのようにして食べるのだろうと思って、あたりを見回してみたら、なんと、口上書きが添えられていた。
f:id:bitterharvest:20161025075158j:plain
名前は、バターナッツかぼちゃ。ピーナッツかぼちゃとも呼ばれるらしい。ピーナッツの殻と同じような形をしているので、このような名前がついたのだろう。しかし、ナッツというにはあまりにも大きすぎる。名はバターナッツよりも瓢箪の方がよさそうな気もする。しかし、自分の子供に可愛らしい名前を付けたがる人たちが増えた昨今では、余りにも工夫がない名前は嫌われるのであろう。そればかりではなく、味も何となく悪そうで、お客さんを惹きつけそうもない。カボチャの仲間たちに君臨するには、何とも愛らしい名前がふさわしいのであろう。

口上書きには、名前の紹介だけではなく、この後の身の振り方にも触れてあった。いたって簡単に記述されているのだが、ポタージュに変わりたいとのことだ。その儀式も一緒に書かれていた。それほど手間のかかるものではなさそうに見えたので、ハロウィンの時期の思い出として、バターナッツかぼちゃのポタージュに挑戦することとした。

買い求めたバターナッツかぼちゃはとてもいい形をしていて、コロコロとよく転がる。レジ袋に入れるときは、とても素早い動きで、テーブルから床へと逃げて行った。
f:id:bitterharvest:20161025071919j:plain
嫌がるバターナッツかぼちゃを強引に我が家に連れてきて、その美しい中身を拝見することにした。少し残酷だが、真っ二つに切り裂くことにした。かぼちゃの方は切られるのを嫌がって包丁を跳ね返すであろうから、軽く切り込みを入れて、横に滑らないように細心の注意を払いながら、垂直に力をかけて包丁に体重を乗せた。かぼちゃは、悲鳴を上げたが、その中身は、オレンジ色に近い鮮明な黄色であった。下の方は胎内なのだろう。未来の子孫が柔らかい綿でくるまれていた。
f:id:bitterharvest:20161025072001j:plain
かぼちゃの味を引き出すためには、玉ねぎをお供にするのがよい。優しく刻んで仲間に加える。
f:id:bitterharvest:20161025072040j:plain
かぼちゃと玉ねぎのペーストを作るので、かぼちゃの方は、皮をむいた後、
f:id:bitterharvest:20161025072108j:plain
1cm角程度に切り刻んだ。
f:id:bitterharvest:20161025072227j:plain
多数のコンポーネントに分かれてしまったかぼちゃと玉ねぎをお鍋に移し、さらに、美しくするために、化粧にマギーブイヨン2個と水500ccほど加えて、25分間煮込んだ。
f:id:bitterharvest:20161025072247j:plain
沸騰しているので、少し、冷ましてから、ミキサーにかけ、
f:id:bitterharvest:20161025072316j:plain
滑らかな肌触りのペーストへと変装させた。
f:id:bitterharvest:20161025072349j:plain
およそ半分ほどの牛乳(ペーストが1200ccほどあったので600ccの牛乳)を加え、塩と黒コショウで最後の化粧を整えた後、沸騰するまで、煮たてた。
f:id:bitterharvest:20161025081346j:plain

見事に仮装したので、オーストラリアから来た牛を伴って、ハロウィン・パーティーへ、いざ、出かけよう。
f:id:bitterharvest:20161025083013j:plain
ホテルのレストランのポタージュよりおいしかったと、歌舞いた姿が絶賛されたのは言うまでもない。

自己を表現する恒等射

2.3 恒等射

恒等射は自らを自らに写す射だ。これだけのことなので、今回の記事は簡単に済ますつもりでいた。

しかし、沢山ある射の中で恒等射を特別に圏の定義の中に含める必要がなぜあったのだろうか。恒等射は英語ではidentity(あるいはidentity morphism)である。

アイデンティティという用語は、自然科学で使っているときは軽やかに通り過ぎてしまうが、社会科学で用いるときは何とも重い感じがする。最近の英国のEUからの離脱や、米国でのトランプ氏の大統領候補選出などを観察していると、英国や米国の本来のアイデンティティがむき出しで表れてきたのではないかと感じさせられる。

世界が大きく変わってきているなと感じていたさなか、昨日、あるカルチャセンターで、本郷和人東大教授から鎌倉時代の話を伺った。その日の話題とは関係なかったのだが、冒頭でエマニュエル・トッドの家族型の説明を受けた。トッドは、ソ連邦の崩壊を予想し、実際にそのようになったことから注目され、今またEUの崩壊を予想している人口学の研究家である。

彼は、家族型を8つに分類し、それぞれの型と社会とがどのような関係にあるかを論じている。大きな分類としては、核家族、直系家族、共同体家族である。核家族とは一組のカップルが、直系家族とは二組(親と子の夫婦)あるいは三組(孫の夫婦まで一緒の場合)のカップルが、共同体とは多数(親族)のカップルが共に住むことが慣習となっているような家族制度である。

核家族は、さらに、絶対核家族(平等には無関心)と平等主義核家族に分かれ、前者にはイングランド、米国が含まれ、後者にはフランス北部(パリ)が含まれる。直系家族にはドイツや日本が含まれる。共同体家族は、さらに三つに分類されるが、外婚制共同体家族(いとこ婚は禁止)はロシア、旧ユーゴスラビア、中国が、内婚制共同体社会(いとこ婚は優先)には西アジア中央アジアが含まれる。

家族制度は、共同体社会→直系家族→核家族へと進化していくと考えがちだが、そうではないとエマニュエル・トッドは主張している。中心となっている地域が多くのカップルを含む家族制度に移行すると、その周辺地域には中心地域の家族制度が押し出され、さらに、その辺縁地域には周辺地域の家族制度が押し出されるとのことである。ユーラシア大陸には六つの中心地域があり、それらはロシア、中国などである。それらの国は共同体家族制度である。その周辺国であるドイツ、日本は直系家族制度であり、さらにその辺縁国であるイングランド核家族である。とても、面白い考え方で魅了される。

この研究成果は、『家族システムの起源』(2分冊)として出版されているので、機会を見つけて読んでみたいと思っている。
f:id:bitterharvest:20161015094017j:plain

エマニュエル・トッドは家族制度と基本的価値観やイデオロギーとの関係を見い出し、EU(具体的にはユーロ)の分裂、押し寄せる難民など、現代社会が抱える問題の結末を大胆に予測している。

家族制度は生活や考え方そして倫理観まで規定するであろうから、家族制度はそれぞれの人にアイデンティティを与えているといってよい。そして、人々がアイデンティティを大事にするとき、その結果として、現代社会が内包している危険な社会問題を引き起こすことになる。かといって、問題を解決するために、アイデンティティを捨てる訳にもいかないであろうから、今日の問題の解決は極めて難しい(今日のグローバリゼーションはアイデンティティの喪失を伴う。アイデンティティを失ったとき、人は何を夢見て生きていくのだろうか。経済合理性だけだとすると寂しすぎる)。

アイデンティティは、自分を赤裸々にするということともいえるが、圏論の恒等射の役割もここにある。圏論は、射だけでいろいろな性質を表現しようとするものだが、自身を表現しようとすると恒等射が必要になる。このため、射の中でも恒等射は特別な意味を有している。決して忘れてはならない射である。アイデンティティを必ず持ちましょうという射である。

恒等射は、一言でいうと、この記事の冒頭で述べたように、自らを自らに移す射である。従って、ドメイン(domain)とコドメイン(codomain)は同じ対象になるが、ドメインとコドメインで要素が入れ替わっているような射は恒等射ではない。
f:id:bitterharvest:20161015130942p:plain
あくまでも同じ要素への射でなくてはならない。
f:id:bitterharvest:20161015131002p:plain

恒等射をHaskellで実現してみよう。とても簡単で、全ての要素に対して同じ要素を返すようにすればよい。そこで、恒等射でのドメイン側の要素とコドメイン側の要素を対にしたリストで出力するプログラムを作成すると、次のようになる。

identity :: [a] -> [(a,a)]
identity a = [(x,x) | x <- a]

実行結果をいくつか示そう。

Prelude> :load "Identity.hs"
[1 of 1] Compiling Main             ( Identity.hs, interpreted )
Ok, modules loaded: Main.
*Main> identity [1..4]
[(1,1),(2,2),(3,3),(4,4)]
*Main> identity ['a'..'f']
[('a','a'),('b','b'),('c','c'),('d','d'),('e','e'),('f','f')]
*Main> identity ['a','g','f','k']
[('a','a'),('g','g'),('f','f'),('k','k')]

圏論は射から

2.2 射

圏論の中で一番重要な役割を担うのは射(arrowあるいはmorphism)である。射は対象を対象へと移す(写像という用語を使ったときは「写す」といったほうがよさそうだが、射は矢を射るという意味で用いているので「移す」という漢字を使う)。射は、他の分野では写像と呼ばれたり、関数と呼ばれたりする。しかし、圏論では、写像や関数の概念を抽象化して用いるので、射という特別な用語を用いる。

1)射とは

射\(f\)は、\(f:A \rightarrow B\)と記述される。\(A,B\)は対象で、\(A\)をドメイン(domain)、\(B\)はコドメイン(codomain)と呼ばれる。また、\(f(A)\)、即ち、ドメイン\(A\)が射\(f\)によって移される先をイメージ(Image)と呼ぶ。
f:id:bitterharvest:20161011120648p:plain

2)部分写像は射ではない

射の定義はかなり自由だが、それでもいくつか制限がある。その一つは、ドメインのいずれの要素も射によってコドメインのある要素に移されなければならない。しかし、反対に、コドメインの全ての要素が射のイメージになっている必要はない。射によって、全部であったり一部であったりしてよい。

関数では、このような関数を全関数(total function)と呼んでいる。しかし、我々が使っている写像の中には、全写像とはならないものがあることに注意しておく必要がある。例えば、お見合いパーティーをしたとしよう。パーティーに参加する人の情報をあらかじめ与えておいて、一番気に入っている人を当日会場の入り口で提出することにしよう。そして、相互に名前が一致した人達だけが、最初にお見合いをすることとする。この時、男性をドメインに、女性をコドメインにして、お見合いの相手を男性の方から女性の方に写像したとする。しかし、残念なことに、お見合いの相手がいない男性もいるであろう。このような場合には、ドメインのある要素に関数が定義されない。このようなものは部分写像(partial mapping)あるいは部分関数(partial function)と呼ばれるが、これは、集合と(全域)写像の圏の射ではない。
部分関数は、あるもの同士の関係を表す時によく現れる。例えば、データベースではこのようになっている場合が多い。
f:id:bitterharvest:20161011120742p:plain

また、関数は、一つの要素に複数の要素が対応することを許してはいないので、このようなものも圏論での射とはならない(ただし、関数の逆関数はこのようになることがある)。
f:id:bitterharvest:20161011120822p:plain

3)倍数を求める射

それでは、射の例をいくつか挙げてみよう。ドメインもコドメインも1で始まる自然数とする。射は2倍にする関数*2とする。この時の射は次のようになる。イメージは偶数となり、コドメインより小さい領域となる。
f:id:bitterharvest:20161011120928p:plain

それでは、上の射をHaskellで実現してみよう。Haskellでは射は関数という用語を使っているので、慣習に従って、Haskellのプログラムを説明しているときは、関数という用語を用いる。また、対象もデータ型という用語を用いる。

Haskell自然数のデータ型を用意していないので、自分で用意する必要がある。そこで、次のように定めよう。

data Nat1 = One | Succ Nat1 deriving (Read, Eq, Ord, Show)

とても短い定義だ。\(One\)は自然数1に対応する。\(Succ \ Nat1\) は自然数\(Nat1\)に続く自然数を表す。従って、\(Succ \ One\)は自然数1に続く自然数ということで2を表す。以下同様に、\(Succ \ (Succ \ One)\)は3である。
ところで、これではわかりにくいので、正の整数から対応する自然数を作り出す関数\(toNat1\)を用意しよう。これは以下のようにする。なお、記述\(toNat1 :: Int \rightarrow Nat1\)は次のことを意味する。関数\(toNat1\)はデータ型Intをデータ型\(Nat1\)へ移す。

toNat1 :: Int -> Nat1
toNat1 m
  | m == 1 = One
  | m > 0  = Succ $ toNat1 (m - 1)
  | otherwise = error "Not Natural Number"

また、逆に自然数から対応する整数を作り出す関数\(fromNat1\)も次のように用意しよう。

fromNat1 :: Nat1 -> Int
fromNat1 One   = 1
fromNat1 (Succ n) = 1 + fromNat1 n

それでは少し使ってみよう。自然数1から順番に求めてみよう。また、逆への変換も行ってみる。

Prelude> :load "Nat1.hs"
[1 of 1] Compiling Main             ( Nat1.hs, interpreted )
Ok, modules loaded: Main.
*Main> let v1 = toNat1 1
*Main> v1
One
*Main> let v2 = toNat1 2
*Main> v2
Succ One
*Main> let v3 = toNat1 3
*Main> v3
Succ (Succ One)
*Main> let v4 = toNat1 4
*Main> v4
Succ (Succ (Succ One))
*Main> let v8 = toNat1 8
*Main> v8
Succ (Succ (Succ (Succ (Succ (Succ (Succ One))))))
*Main> fromNat1 v1
1
*Main> fromNat1 v2
2
*Main> fromNat1 v8
8

それでは与えられた自然数の2倍を求める関数を求めてみよう。名前は、*2ではなく\(times2\)とする(*2はHaskellで既に使われているので、これとの混用を避ける)。2倍を求めるためには、与えられた数同士を加えればよい。そのためには、自然数を足し合わせる関数plusが必要になるので、これも一緒に定義することにしよう。次のようになる。

plus :: Nat1 -> Nat1 -> Nat1

m `plus` One = Succ m
m `plus` Succ n = Succ (m `plus` n)

times2 :: Nat1 -> Nat1
times2 m = m `plus` m 

関数\(plus\)は少し説明が必要である。自然数\(m\)と\(One\)を加えると、\(m\)に続く自然数\(Succ \ m\)となる。また、自然数\(m\)と\(Succ \ n\) (\(n\)に続く自然数)を加算すると、自然数\(m\)と\(n\)を加算して得られた自然数に続く自然数となる。これらを表したのが、上の定義である。なお、\(plus\)を中置関数として利用するときは、関数の前後に\(`\)をつけて、\(`plus`\)とする。さて、完成したので、実験してみよう。同じように自然数1から始めて、だんだんに数を大きくしてみる。

Prelude> :load "Nat1.hs"
[1 of 1] Compiling Main             ( Nat1.hs, interpreted )
Ok, modules loaded: Main.
*Main> let v1 = toNat1 1
*Main> let v1_2 = times2 v1
*Main> v1
One
*Main> v1_2
Succ One
*Main> let v2 = toNat1 2
*Main> let v2_2 = times2 v2
*Main> v2
Succ One
*Main> v2_2
Succ (Succ (Succ One))
*Main> let v3 = toNat1 3
*Main> let v3_2 = times2 v3
*Main> v3
Succ (Succ One)
*Main> v3_2
Succ (Succ (Succ (Succ (Succ One))))
*Main> let v7 = toNat1 7
*Main> let v7_2 = times2 v7
*Main> fromNat1 v7
7
*Main> fromNat1 v7_2
14

なお、上の定義で、関数\(times2\)は、\(times2 :: Nat1 \rightarrow Nat1\)という記述があったが、ここれは次のことを意味する。写像\(times2\)がデータ型\(Nat1\)をデータ型\(Nat1\)へ移す。これは圏論の用語を用いて説明すれば、射\(times2\)が対象\(Nat1\)を対象\(Nat1\)へ移すということになる。

4)剰余を求める射

それでは、ドメイン、コドメイン自然数だが、0始まりとする。そして、3で割った時の余りを求める関数を\(mod3\)とした時の射を求めてみよう。イメージは0,1,2となり、コドメインよりもずっと小さい領域である。
f:id:bitterharvest:20161011121030p:plain

それではHaskellで実現してみよう。0から始まる自然数\(Nat0\)を、先ほどの1から始まる自然数と同様に、定義してみよう。その時、一緒に、自然数と整数の間で変換する関数\(toNat0,fromNat0\)も定義しよう。以下のようになる。

data Nat0 = Zero | Succ Nat0 deriving (Read, Eq, Ord, Show)

toNat0 :: Int -> Nat0
toNat0 m
  | m == 0 = Zero
  | m > 0  = Succ $ toNat0 (m - 1)
  | otherwise = error "Not Natural Number"

fromNat0 :: Nat0 -> Int
fromNat0 Zero   = 0
fromNat0 (Succ n) = 1 + fromNat0 n

それでは、関数\(mod3\)を実現してみよう。次のようになる。

mod3 :: Nat0 -> Nat0

mod3 Zero                   = Zero
mod3 (Succ Zero)            = Succ Zero
mod3 (Succ (Succ Zero))     = Succ (Succ Zero)
mod3 (Succ (Succ (Succ m))) = mod3 m

使ってみよう。自然数0から始めて少しずつ数を大きくして調べてみる。

Prelude> :load "Nat0.hs"
[1 of 1] Compiling Main             ( Nat0.hs, interpreted )
Ok, modules loaded: Main.
*Main> let v0 = toNat0 0
*Main> v0
Zero
*Main> mod3 v0
Zero
*Main> let v1 = toNat0 1
*Main> v1
Succ Zero
*Main> mod3 v1
Succ Zero
*Main> let v2 = toNat0 2
*Main> v2
Succ (Succ Zero)
*Main> mod3 v2
Succ (Succ Zero)
*Main> let v3 = toNat0 3
*Main> v3
Succ (Succ (Succ Zero))
*Main> mod3 v3
Zero
*Main> let v4 = toNat0 4
*Main> v4
Succ (Succ (Succ (Succ Zero)))
*Main> mod3 v4
Succ Zero
*Main> let v15 = toNat0 15
*Main> v15
Succ (Succ (Succ (Succ (Succ (Succ (Succ (Succ (Succ (Succ (Succ (Succ (Succ (Succ (Succ Zero))))))))))))))
*Main> mod3 v15
Zero

5)複数の射

今度は、射が二つある場合を考えてみよう。対象を\(A={f,g,h},V={a,b,c,d}\)とする。また、射\(arc,trg\)はドメインを\(A\)とし、コドメインを\(V\)とし、下図のように移すとする。
f:id:bitterharvest:20161011121127p:plain
圏は抽象化されたものなので、それに対応させて様々な具体例を作ることができるが、上の射を実現したような実例を挙げることができるであろうか。下図はどうであろうか。
f:id:bitterharvest:20161011121141p:plain
上の図からわかるように、グラフは矢印をドメインとし、接続点をコドメインとし、矢印の元の方を接続点に移す射\(src\)と、矢印の先の方を接続点に移す射\(trg\)を用意することで圏となる。

5)可能な射をすべて求める

それでは、\(A={1,2,3}\)と\(B={a,b}\)の二つの対象があり、\(A\)をドメイン、\(B\)をコドメインとする射はいくつあるだろうか。ただし、同じ射は含めないとする。正解は下図のように\(2^3=8\)である。
f:id:bitterharvest:20161011122628p:plain

上の結果をHaskellで確かめてみよう。
ドメインとコドメインを次のデータ型\(Domain,Codomain\)で定義しよう。

data Domain = D1 | D2 | D3 deriving (Read, Show)
data Codomain = CA | CB deriving (Read, Show)

可能な射をリストとして出力されるようにしよう。即ち、リストの要素は一つの射に対応するものとする。また、それぞれの射は、ドメインの要素とコドメインの要素の対とする。それぞれの対では、ドメインの要素はその射によって移されるコドメインの要素に移されるものとする(リストが入れ子になっていることに注意)。これを実現したのが、下記のプログラムである。

morphism :: [((Domain, Codomain), (Domain, Codomain), (Domain, Codomain))]
morphism = [((D1,d1), (D2,d2), (D3,d3)) | d1 <- [CA, CB], d2 <- [CA, CB], d3 <- [CA, CB]]

この関数を実行してみよう。

Prelude> :load "Morphism.hs"
[1 of 1] Compiling Main             ( Morphism.hs, interpreted )
Ok, modules loaded: Main.
*Main> morphism
[((D1,CA),(D2,CA),(D3,CA)),((D1,CA),(D2,CA),(D3,CB)),((D1,CA),(D2,CB),(D3,CA)),((D1,CA),(D2,CB),(D3,CB)),((D1,CB),(D2,CA),(D3,CA)),((D1,CB),(D2,CA),(D3,CB)),((D1,CB),(D2,CB),(D3,CA)),((D1,CB),(D2,CB),(D3,CB))]

上記の結果で、リストの要素が一つの射を表しているので、可能な射は、
\(( (D1,CA),(D2,CA),(D3,CA) )\)
\(( (D1,CA),(D2,CA),(D3,CB) )\)
\(( (D1,CA),(D2,CB),(D3,CA) )\)
・・・
の8個である。これは、先ほど示したものと一致する。
また、上記の射は、ドメインの要素とそれによって移されるコドメインの要素を示している。最初の射では、\(D1\)を\(CA\)に、\(D2\)を\(CA\)に、\(D3\)を\(CA\)に移す。

6)個数\(m\)のドメインから個数\(n\)のコドメインへの射

一般にドメインの要素数が\(m\)でコドメインの要素数が\(n\)のとき、異なる射の数は\(n^m\)となる。

いくつか確認してみよう。コドメインの要素数が1の時は、異なる射の数は1個である。例えば、ドメイン自然数とすると、射は次のようになる。
f:id:bitterharvest:20161011122747p:plain

ドメインの要素数が0の時は、射は存在しない。

それでは、ドメインの要素数を1としよう。この時は異なる射の数はコドメインの要素の数となる。例えば、コドメイン自然数であったとすると次のようになる(図では\(one,two,three\)まで書いたが、そのあと無限に続く)。
f:id:bitterharvest:20161011122850p:plain

それでは、ドメインの要素数が0の時は、どうであろうか。式からは1となる。そこで、ドメイン空集合の時は、コドメイン全体に移す射が一つあることにする。
f:id:bitterharvest:20161011122919p:plain

それでは、Haskellドメインの要素数が\(m\)でコドメインのそれが\(n\)の時の関数をすべて求めてみよう。

このプログラムは相当に手ごわいので、少しずつ部品を用意しながら、完成に導こう。今、ドメインの要素数が3であり、要素は順番に\(a1,a2,a3\)となっていったとしよう。これをリストで表すと\([a1,a2,a3]\)となる。一方、コドメインの要素数は2であり、順番に\(b1,b2\)であったとする。ドメインの要素に対してコドメインの要素を対応させることになるが、ドメインの右の方から処理していくことにする。

最初に、行うのは、\(a3\)となる。そこで、\(a3\)を\(b1,b2\)で置き換えたリストを\([[b1],[b2]]\)とする。次に\(a2\)を置き換えて、それを先ほど得たリストの頭にくっつけることにしよう。そうすると、\([[b1,b1],[b1,b1],[b2,b1],[b2,b2]]\)をえる。さらに、\(a1\)についても同じことを行えば、ドメインからコドメインに移す異なる射をすべて得ることができる。

そこで、置き換えられたリストから次の要素も置き換えたリストを作る関数\(addRep\_\)を定義しよう。

addRep_ :: [b] -> [[b]] -> [[b]]
addRep_ [] _ = []
addRep_ _ [] = []
addRep_ (y:ys) (l:ls) = (y:l) : addRep_ [y] ls ++ addRep_ ys (l:ls)

確認してみよう。

*MorphismA> addRep_ [1,2] [[1],[2]]
[[1,1],[1,2],[2,1],[2,2]]

うまく動いているようである。それでは、この操作を左端から右端まで再帰的に順番に行う関数\(morphism\_\)を定義しよう。

morphism_ :: [a] -> [b] -> [[b]]
morphism_ _ [] = []
morphism_ [] ys = [ys]
morphism_ (x:[]) (y:ys) = [y] : morphism_ (x:[]) ys
morphism_ (x:xs) ys = addRep_ ys $ morphism_ xs ys

上の関数で、左端についての処理を行うのが、以下の部分である。

morphism_ (x:[]) (y:ys) = [y] : morphism_ (x:[]) ys

途中の処理は次の部分である。

morphism_ (x:xs) ys = addRep_ ys $ morphism_ xs ys

すでに置き換えの済んでいるリスト\(morphism\_ \ xs \ ys\)に新しい置き換え\(addRep\_ \ ys\)を付け加えている。

正しく動いているかどうか確認してみよう。

*MorphismA>  morphism_ [1,2,3] [1,2]
[[1,1,1],[1,1,2],[1,2,1],[1,2,2],[2,1,1],[2,1,2],[2,2,1],[2,2,2]]
*MorphismA>  morphism_ ['a', 'b', 'c'] [1,2]
[[1,1,1],[1,1,2],[1,2,1],[1,2,2],[2,1,1],[2,1,2],[2,2,1],[2,2,2]]

うまく動いているようだ。

また、プログラムで

morphism_ [] ys = [ys]

は、空集合に対する射である。次のようになる。

*MorphismA>  morphism_ [] [1,2]
[[1,2]]

全体への射となる。

さて、部品が完成したので、これはモジュール\(MorphingA\)としておこう。

module MorphismA (morphism_) where

morphism_ :: [a] -> [b] -> [[b]]
morphism_ _ [] = []
morphism_ [] ys = [ys]
morphism_ (x:[]) (y:ys) = [y] : morphism_ (x:[]) ys
morphism_ (x:xs) ys = addRep_ ys $ morphism_ xs ys

addRep_ :: [b] -> [[b]] -> [[b]]
addRep_ [] _ = []
addRep_ _ [] = []
addRep_ (y:ys) (l:ls) = (y:l) : addRep_ [y] ls ++ addRep_ ys (l:ls)

要素の数が与えられた時は、配列を自動的に作り出して、\(morphism\_\)を呼び出して、可能な関数をリストの列で与える関数\(morphism\)を次のように定義する。

morphism :: (Num b, Num a, Enum b, Enum a) => a -> b -> [[b]]
morphism m n = morphism_ [1..m] [1..n]

これの動作を確認しよう。

*Main>  morphism 3 2
[[1,1,1],[1,1,2],[1,2,1],[1,2,2],[2,1,1],[2,1,2],[2,2,1],[2,2,2]]

うまく動いているようである。

上記では、ドメインからコドメインへの射は配列、例えば[1,1,1]、で与えられているが、これをタプル、即ち(1,1,1)、で表すことにしよう。

これは、コドメインの要素数ごとに関数を用意する。要素数が0に対しては\(morphism0、1\)に対しては\(morphism1、2\)に対しては\(morphism2\)のように定義しよう。次のようになる。

morphism0 :: (Num a, Enum a) => a -> [[a]]
morphism0 n = morphism 0 n

morphism1 :: (Num a, Enum a) => a -> [a]
morphism1 n = toPair1 $ morphism_ [1..1] [1..n]
  where 
    toPair1 [] = []
    toPair1 (f:fs) = (f !! 0) : toPair1 fs 

morphism2 :: (Num a, Enum a) => a -> [(a, a)]
morphism2 n = toPair2 $ morphism_ [1..2] [1..n]
  where 
    toPair2 [] = []
    toPair2 (f:fs) = (f !! 0, f !! 1) : toPair2 fs 

morphism3 :: (Num a, Enum a) => a -> [(a, a, a)]
morphism3 n = toPair3 $ morphism_ [1..3] [1..n]
  where 
    toPair3 [] = []
    toPair3 (f:fs) = (f !! 0, f !! 1, f !! 2) : toPair3 fs 

morphism4 :: (Num a, Enum a) => a -> [(a, a, a, a)]
morphism4 n = toPair4 $ morphism_ [1..4] [1..n]
  where 
    toPair4 [] = []
    toPair4 (f:fs) = (f !! 0, f !! 1, f !! 2, f !! 3) : toPair4 fs 

morphism5 :: (Num a, Enum a) => a -> [(a, a, a, a, a)]
morphism5 n = toPair5 $ morphism_ [1..5] [1..n]
  where 
    toPair5 [] = []
    toPair5 (f:fs) = (f !! 0, f !! 1, f !! 2, f !! 3, f !! 4) : toPair5 fs 

morphism6 :: (Num a, Enum a) => a -> [(a, a, a, a, a, a)]
morphism6 n = toPair6 $ morphism_ [1..6] [1..n]
  where 
    toPair6 [] = []
    toPair6 (f:fs) = (f !! 0, f !! 1, f !! 2, f !! 3, f !! 4, f !! 5) : toPair6 fs

分かりやすいところから、実行してみよう。ドメインとコドメインとも要素数が2の場合は次のようになる。

*Main>  morphism2 2
[(1,1),(1,2),(2,1),(2,2)]

これは次のように解釈される。射は4個ある。それらは、(1,1),(1,2),(2,1),(2,2)である。最初の射(1,1)は、ドメインの最初の要素をコドメインの最初の要素に、そして、ドメインの最後の要素をコドメインの最初の要素に移す。ここで、ドメインの要素の順番は適当でよい。コドメインについても同じである。

ドメインの要素数が3になるとどうなるであろうか。

*Main>  morphism3 2
[(1,1,1),(1,1,2),(1,2,1),(1,2,2),(2,1,1),(2,1,2),(2,2,1),(2,2,2)]

4では、

*Main>  morphism4 2
[(1,1,1,1),(1,1,1,2),(1,1,2,1),(1,1,2,2),(1,2,1,1),(1,2,1,2),(1,2,2,1),(1,2,2,2),(2,1,1,1),(2,1,1,2),(2,1,2,1),(2,1,2,2),(2,2,1,1),(2,2,1,2),(2,2,2,1),(2,2,2,2)]

だいぶ関数が多くなってきた。関数の数を数えてみよう。今得た結果に\(length\)を施すと求めることができる。

*Main> length $ morphism4 2
16

素数が5と6の時についても求めてみよう。

*Main> length $ morphism4 2
16
*Main> length $ morphism5 2
32
*Main> length $ morphism6 2
64

それでは、ドメインの要素数が1の時の射を求めてみよう。

*Main>  morphism1 2
[1,2]

本当は、[(1),(2)]と表示されることを望んだのだが、()がなくても同じということで、省かれている。
ドメインの要素数が1の時の射を求めてみよう。

*Main>  morphism0 2
[[1,2]]
*Main> length $ morphism0 2
1

全体に移され、射の総数は1である。

それでは、コドメインの要素数を変えてみよう。1個の場合はどうであろうか。

*Main>  morphism0 2
*Main> morphism6 1
[(1,1,1,1,1,1)]
*Main> morphism5 1
[(1,1,1,1,1)]
*Main> morphism4 1
[(1,1,1,1)]
*Main> morphism2 1
[(1,1)]
*Main> morphism1 1
[1]
*Main> morphism0 1
[[1]]

射の数はどれも1であることが確認できる。

それでは0の場合はどうであろうか。

*Main> morphism6 0
[]
*Main> morphism5 0
[]
*Main> morphism4 0
[]
*Main> morphism3 0
[]
*Main> morphism2 0
[]
*Main> morphism1 0
[]
*Main> morphism0 0
[]

個数は0だ。

Haskellでは空集合をデータ型Data.Voidで定義している。その中に、\(absurd\)という関数があって、\(Void\)を任意の型に移している。

Prelude> import Data.Void
Prelude Data.Void> :i Void
data Void 	-- Defined in ‘Data.Void’
instance [safe] Eq Void -- Defined in ‘Data.Void’
instance [safe] Ord Void -- Defined in ・eData.Void’
instance [safe] Read Void -- Defined in ‘Data.Void’
instance [safe] Show Void -- Defined in ‘Data.Void’
Prelude Data.Void> :t absurd
absurd :: Void -> a

論理学では\(Void\)は偽の値を持つ。\(Absurd\)の関数を\(if \ Void \ then \ a\)とみなすならば、\(Void\)が偽であることから、\(a\)は何であったもよいことになる。即ち、任意の型でよいことになる。このことを表しているのが、\(absurd\)だが、使う機会はなさそうである。

7)星の上の対象

圏論を勉強しているときに、下記の図を見てびっくりした。驚愕したという方が適切かもしれない。
f:id:bitterharvest:20161011123105p:plain
この図を見るまでは自然数は対象にしかならないと思い込んでいたが、なんとここでは射になっている。全く異質な世界を見せられた。射であるからには、自然数はあるものをあるものに移さなければならない。図では星を星に移していた。明らかに対象なのだが、なぜ、星で表しているのであろうか。

圏論の入門書を読むと、最初の方に対象の詳細を知ることなしに、論じることができる数学の体系を考えようという趣旨のことが書かれている。対象は集合やグラフなど様々な構造を持っている。その構造に重点を置いて論じると、集合論グラフ理論位相空間論などの様々な数学の分野に落ち込んでしまう。そこで、構造を気にしないで論じてみようというのが圏論だ。

しかし、圏論の入門書を読んでいると、集合から初めてグラフへと説明が移行していくので、圏論の本来の趣旨をすっかり忘れて、対象の構造にどっぷりしたって考える癖がついてしまう。そのような時に出会ったのが、上の図である。

星が対象の本体の趣旨を思い出させてくれた。星が表しているのは対象なのでその中身は気にしないでねというメッセージを送ってくれている。とても高尚な表現である。

この図が持つ正確な意味は、射と射の合成を論じてからでないと、説明できない。従って、ここでは問題の提起だけにして、楽しい話はしばらく後にしよう。

境を明確にする対象

2.圏の定義

圏論の核となる圏の定義はとても単純である。抽象化は、細かいことを切り捨て、本質となている部分を取り出していく作業である。別の言い方をすると、数学の様々な分野での固有な部分を切り捨て、それらに共通する部分を抜きだすことである。圏論は抽象化の結果なので、その定義が単純なのは当たり前だとも思えるが、抽象化された後に得られた概念が大切なので、それらを少し詳しく説明する。

2.1 対象

数学で一番重要な概念は、等しいという概念だと思う。等しいという概念を最初に理解するのは、おそらく、三角形の合同だろう。二つの三角形を重ね合わせることができるなら、二つの三角形は等しいと学ぶ。しかし、これでは、数学的な厳密性を欠くので、一片とその両端の角がそれぞれ等しい時、二つの三角形は合同であるなどというように定義される。

三角形の合同は、それぞれの三角形の細かな違いを無視している。もしかすると、それぞれの三角形には、模様がついていて、それぞれで異なっているかもしれない。あるいは、三角形を作り出している素材が異なっているかもしれない。しかし、このような細部での違いを無視して、別の言葉を使うならば、抽象化をして、三角形の合同を定義している。

同じ性質を有するものを型という。型は自由に決めることができる。相互に合同である三角形を下図のように一つの型と決めてもよい。この型には無数の三角形が存在することになる(但し、全てが相互に合同である)。
f:id:bitterharvest:20161003095854p:plain

合同を学んだあと、相似な三角形を学ぶ。その後で、射影幾何学を学んだ人もいるだろう。ここでは、相似な四角形を定義できる。図のように一つの光源から光を四角形を当てると後ろに張った暗幕に影ができる。同一の影を作るような四角形は射影的合同と呼ばれるが、これに対しても型を作ることができる。
f:id:bitterharvest:20161004090228p:plain

もっと抽象化を行い、一つだけ穴が空いているものという型を作ることもできる(穴の定義が曖昧だが、例えば、これはホモトピー基本群が無限巡回群になる物体で、その次元を問わない)。
f:id:bitterharvest:20161003103336p:plain

型は、文学的に記述すればミウチとそうでない者との間に境目をつけることだ。少し前から読んでいた、永井路子著『美貌の女帝』は、蘇我氏の血筋を引くものを守り、新興勢力として伸びつつある藤原氏を排除することを政権の課題とする氷高皇女(ひたかひめみこ、後の元正天皇)の苦渋に満ちた人生を伝えている。実世界の中で、ミウチとそうでない者との間に境を設けようとすると、とても厳しい事態となるが、数学の世界では合理的に割り切って、境をつけることにしよう。

型は、圏論では対象(an object)という。対象はある性質を共有しているもの(数学的に等しいもの)である。対象には、いくつかの要素が含まれる(まったく含まれない場合もあるがこの話は後でする)。その数は、有限であるかもしれないし、無限であるかもしれない。先ほどの幾何学的な例に加えて、いくつかを紹介しよう。なお、対象を定義するときは、どのような性質を共有しているのかをよく考えながら定義することが大切である。曖昧に定義すると後でとんでもないしっぺ返しを食らう。

よく利用するのは、有限な単純な値の集まりであると思う。例を挙げてみよう。なお、ここでは、集合と言う言葉ではなく、集まりという言葉を使う(集合と言う用語を用いると、いわゆるラッセルのパラドックスに陥るので、ここではそれを避けるために集まりとする)。

1)単純な値の集まり(有限な個数)

・月={1月,2月,..,12月}
・曜日={日,月,火,水,木,金,土}
・苗字={青井,安藤,..,渡辺}

個数が有限でない場合、即ち無限の時は、数え上げられる場合とそうでない場合とに分けられる。まず数え上げられる場合を考えてみよう。

2)単純な値の集まり(無限だが数え上げできる個数)

自然数={1,2,3,..} (0を含める場合もある)
・整数={..,-1,0,1..}

数え上げられない場合は次のようである。

3)単純な値の集まり(数えきれないほどの無限な個数)

・半径1の円内の点
・実数
複素数

単純な値を組み合わせて、構造的な値を有するものもある。

4)構造的な値の集まり

・三角形
・正多角形
・合同な三角形
ホモトピー基本群が無限巡回群になる物体
・野菜
・車

Haskellでは対象はデータ型として表す。データ型は自由に定義することができるが、使用頻度の高いものはシステムであらかじめ用意している。それらは、

データ型説明
Int整数(有界\(-2^{63} \sim 2^{63}-1\))
Integer整数(無限)
Float単精度浮動小数点数
Double倍精度浮動小数点数
Bool真理値
Char文字(Unicode)

また、文字列\(String\)はデータ型ではないが、頻繁に用いるので、同じような感じで使えるようになっている。

また、新しいデータ型を用意することもできる。その時は、\(data\)を用いる。例えば、曜日のデータ型\(Day\)は次のようになる(なお、データ型の名前の、この場合\(Day\)の、先頭は必ず大文字である。また、データ型の名前は型コンストラクタと呼ばれる)。

data Day = Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday deriving (Show, Read)

これをファイルDay.hsに書き込んでおこう。そして、このプログラムを動かしてみよう。Haskellは今年の5月21日に版の改正があり、最新のバージョンはGHC8.0.1である。Windows用には、WinGHCiを用いると便利である。動かした結果は次の通りである。
f:id:bitterharvest:20161003152753p:plain

ファイルを読み込んだ後で、データ型\(Day\)が正しく処理されているかを見るために、検査(inspection)してみる。これにはコマンド\(:i\)を用いる。
正しく定義されているので、次は変数\(a\)の値を月曜日にしてみる。\(a\)の型(\(type\))を調べてみる。これには\(:t\)を用いる。ちゃんとなっている。最後に\(a\)の値を求めると\(Monday\)と出力される。

なお、データ型\(Day\)を定義するときに\(deriving (Show,Read)\)とした。これについては射のところで詳しく説明するが、ここでは、読込み、かつ、出力できるようにしたと理解して欲しい。

図形を扱えるようにするために、ユークリッド座標上の点に関するデータ型\(Point\)を用意しよう。

data Point = Point (Double, Double) deriving (Show, Read)

この定義をファイルPoint.hsに書き込み、これを読みだして使ってみよう。点\(p1=(2.4,4.9)\)と\(p2=(3,4)\)を作成してみよう。

Prelude> :load "Point.hs"
[1 of 1] Compiling Main             ( Point.hs, interpreted )
Ok, modules loaded: Main.
*Main> :i Point
data Point = Point (Double, Double) 	-- Defined at Point.hs:1:1
instance [safe] Read Point -- Defined at Point.hs:1:53
instance [safe] Show Point -- Defined at Point.hs:1:47
*Main> let p1 = Point (2.4, 4.9)
*Main> p1
Point (2.4,4.9)
*Main> :t p1
p1 :: Point
*Main> let p2 = Point (3, 4)
*Main> p2
Point (3.0,4.0)
*Main> :t p2
p2 :: Point


次はこれを用いて三角形のデータ型\(Triangle\)を定義してみよう。定義に当たっては重心と2頂点を入力することにしよう。定義は次のようになる。

data Triangle = Triangle Point Point Point deriving (Show, Read) -- the gravity center, two vertexes

この定義をファイルPoint.hsに追加して使ってみよう。
以下では、重心が\((0,0)\)、頂点が\((0,1), (\sqrt{3}/2,-1/2), (-\sqrt{3}/2,-1/2)\)の三角形\(t1\)を作ろう。作成に当たっては重心と最初の2頂点を用いる。
f:id:bitterharvest:20161004180048p:plain

Prelude> :load "Point.hs"
[1 of 1] Compiling Main             ( Point.hs, interpreted )
Ok, modules loaded: Main.
*Main> :i Triangle
data Triangle = Triangle Point Point Point
  	-- Defined at Point.hs:3:1
instance [safe] Read Triangle -- Defined at Point.hs:3:60
instance [safe] Show Triangle -- Defined at Point.hs:3:54
*Main> let t1 = Triangle (Point (0,0)) (Point (0,1)) (Point (sqrt 3, (-0.5)))
*Main> t1
Triangle (Point (0.0,0.0)) (Point (0.0,1.0)) (Point (1.7320508075688772,-0.5))
*Main> :t t1
t1 :: Triangle

さらに正多角形のデータ型\(RegularPolygon\)を定義してみよう。定義に当たっては辺の数と重心と頂点の一つを入力することにしよう。定義は次のようになる。

data RegularPolygon = RP Int Point Point deriving (Show, Read)  -- edges, the gravity center, a vertex point

\(data\)の次にある単語はデータ型に与えられた名前で、前述したように、型コンストラクタと呼ぶ。これまで、\(Point, Triangle\)を定義してきた。ところで、等号の左側にも、これまでは型コンストラクタと同じ名前を用いてきた。しかし、等号の左側はデータコンストラクタと呼ばれ、型コンストラクタと同じ名前である必要はない。そこで、今回は、型コンストラクタに\(RegularPolygon\)を、データコンストラクタに\(PR\)という名前を用いることにした。

この定義をファイルPoint.hsに追加して使ってみよう。
以下では、重心が\((2,2)\)、頂点の一つが\((2,4)\)の五角形\(p1\)を作ろう。
f:id:bitterharvest:20161004192942p:plain

Prelude> :load "Point.hs"
[1 of 1] Compiling Main             ( Point.hs, interpreted )
Ok, modules loaded: Main.
*Main> :i RegularPolygon
data RegularPolygon = RP Int Point Point
  	-- Defined at Point.hs:5:1
instance [safe] Read RegularPolygon -- Defined at Point.hs:5:58
instance [safe] Show RegularPolygon -- Defined at Point.hs:5:52
*Main> let p1 = RP 5 (Point (2,2)) (Point (2,4)) 
*Main> p1
RP 5 (Point (2.0,2.0)) (Point (2.0,4.0))
*Main> :t p1
p1 :: RegularPolygon

図形につかれたので、車についてのデータ型を定義てみよう。メーカー名(\(company\))と車種(\(style\))と製造年(\(production\))を属性に持たせることにしよう。これには、レコード構文を使うと便利である。レコードはいくつかのフィールドからなり、各フィールドは型を指定できるようになっている。そこで、\(company\)と\(style\)を文字列で、\(production\)を整数の型とし、次のように\(Car\)を定義する。

data Car = Car { company :: String , style :: String , production :: Integer} deriving (Show, Read)

この定義をファイルCar.hsに追加して使ってみよう。
ここ10年使用している車\(c1\)を得てみよう。若者用に売り出されたのだが、現在は、残念ながら製造されていない。
f:id:bitterharvest:20161004202405j:plain

Prelude> :load "Car.hs"
[1 of 1] Compiling Main             ( Car.hs, interpreted )
Ok, modules loaded: Main.
*Main> :i Car
data Car
  = Car {company :: String, style :: String, production :: Integer}
  	-- Defined at Car.hs:1:1
instance [safe] Read Car -- Defined at Car.hs:1:95
instance [safe] Show Car -- Defined at Car.hs:1:89
*Main> let c1 = Car {company="Honda", style="Air Wave", production=2006}
*Main> c1
Car {company = "Honda", style = "Air Wave", production = 2006}
*Main> :t c1
c1 :: Car

動作を確認できたと思う。野菜についても同じように定義できるが、ここは、読者の方で考えてみよう。

これまで、型を作るためのいくつかの方法を述べたが、代数的な計算を利用してデータ型を作りたい場合には、代数的データ型がある。量子力学の世界をHaskellで表現しようとするとき、最も基本的な記述であるブラを代数的データ型を用いて定義した。再帰的な構造を持つ型を定義しようとするとき、代数的データ型は便利である。

最後にまとめです。圏論での対象は型である。型はある性質を表すものの集まりである。Haskellは対象をデータ型として表す。

なぜHaskell、なぜ圏論なのか

1.初めに

これまで、さまざまな言語でプログラミングしてきたが、一番満足しているのはHaskellである。なぜという問いに一言で答えるならば、バグが入りにくい、あるいは、プログラムが信用できるということだろう。
f:id:bitterharvest:20161002140825p:plain
この記事の前に、量子力学の世界をHaskellで構築することを試みた。連続系についてはまだ説明の途中であるが、その基本となる離散系については完成している。量子力学は物理の世界でも難しい分野の一つだ。概念的に複雑な世界を記述しようとすると、とても、抽象度の高いプログラミング言語を必要とする。

現在のHaskellは、圏論(category theory)という数学を応用したプログラミング言語である。他の学問と比較すると、数学は抽象度が高い。圏論は、その中で、最も抽象度が高い分野の一つである(圏論よりも抽象度が高いのは最近話題になることが多いホモトピー型理論だ。この理論がプログラミングの世界で使われるようになると、プログラムの証明はもっと簡単になると予想している)。

Haskellは抽象度がとても高いので、応用分野に適した枠組みを提供するのにとても適している。量子力学のような高度な物理概念も、Haskellを用いることで、量子力学用のフレームワークを割合と簡単に提供することができる。とはいっても、量子力学圏論Haskellの3つを理解していなければならないのでハードルは高い。それでも、それぞれに精通していれば合理的な時間の中で完成させることができる。しかし、他の言語、C++Javaでと言われると二の足を踏む。取り掛かる気にもならない。本格的な作業を始める前に準備するものが多すぎて、時間もかかりそうだし、良いものができる保証もない。

ブログを見ていると、Haskellに挫折した人が多い。恐らくは、その基本となっている概念、即ち、圏論を理解していないためだと思われるので、圏論について、もう一度説明してみたいと思う。

Bartosz Milewskiという方を知っているだろうか。 Haskell圏論に関連するブログを検索すると時々見かける名前だ。

ブログのプロファイルによれば、ポーランドで教育を受け、理論物理で博士の学位を授与されている。その後、欧米でポスドクを経験し、マイクロソフト検索エンジンの開発を行ったそうだ。会社の方がインターネットビジネスに消極的であったこともあり、うまくいかなかったそうである。その後、Reliable Softwareという会社を立ち上げ、現在は、ワシントン大学情報科学の大学院に在籍しているとのことである。写真を見る限りでは、ある程度、歳が行っているように見えるが

彼が、シアトルで、プログラマーのために、10週間のコースを開催中である。講義名は、Category Theory for Programmersである。講義の内容はYouTubeで紹介されている。また、書籍での出版も望んでいるようで、途中までの原稿もブログに掲載されている。

さて、Haskellを皆さんに勧める理由をもう少し詳しく説明しよう。

プログラミングを学んで最初に戸惑う記述は
\begin{eqnarray}
a=a+1
\end{eqnarray}
だろうと思う。方程式の記述に慣れてきた身には
\begin{eqnarray}
3x=2x+1
\end{eqnarray}
という記述は抵抗なく受け入れることができる。左辺の\(3x\)と右辺の\(2x+1\)は等しいと訴えている。この時、\(x=1\)だと理解できる。しかし、同じように考えたとき、\(a=a+1\)を満たす\(a\)はないよということになる。

プログラミングしていて出会うこの悩ましい記述を理解するためには、コンピュータのことをもう少し詳しく知っている必要がある。コンピュータにはプロセッサとメモリがあって、プロセッサは計算し、メモリはデータを記憶する(正確に言うと、プログラムもここに記憶される)。メモリには、番地がついていて、データを取り出す時も、データを格納するときも、この番地を用いる。プロセッサはコンビニエンスストア―のキャッシュレジスターと同じ役割をする。プロセッサには、レジスターがあり、データを一時的に記憶することができる。また、レジスターのデータに対して四則演算を行うことができる。その時、足す数、引く数、掛ける数あるいは割る数などは、メモリーより読みだす。メモリから読みだされたデータを使って、レジスタにあるデータに対して指定された演算を行う。その結果は番地を指定してメモリに記憶することができる。

\(a=a+1\)は、\(a\)という名前で呼ばれている番地から、そこに格納されているデータを取り出し、それをプロセッサのレジスタに移動する。次にレジスタの内容に\(1\)を加える(\(1\)という値はメモリのどこかにしまわれていて、ここから取り出して用いる)。ここまでの手続きが、右辺で示されている。右辺の処理が終了すると、左辺の処理に入る。レジスタの内容を\(a\)という名前で呼べれている番地に記憶する。

プログラミングで用いる記述は、通常は、数学で用いるものと異なっていることが多い。このため、コンピュータの構造やコンピュータの内部での処理を素直に記述するアセンブリ言語を学ばないと、初学者は独特の記述をなかなか理解することができない。

\(a\)は変数と呼ばれるが、変数の値が変わることが許されているものをミュータブル(mutable)という(そうでないものをイミュータブル(immutable)という)。多くのプログラミング言語は、変数がメモリの格納場所を表しているために、ミュータブルである。

しかし、コンピュータが広く使われるようになるにしたがって、アセンブリ言語ではプログラミングしにくいので、いわゆる高級言語と呼ばれるものが開発された。プログラミングの世界でも抽象化が行われるようになる。抽象化とは、異なっていると思われる複数の世界の中から、共通する性質を見つけ出す作業である。抽象化することによって、それまで別々のプログラムとして実装されていたものが、共通する部分は同じプログラムで実現できるようになり、開発費や信頼性の向上につながる。

各種の高級言語が出る中で、オブジェクト指向の概念は大きな画期であった。そこでは、プログラムはオブジェクトの集まりで、オブジェクト間でメッセージを受け渡すことで処理が進行する。オブジェクトはメッセージを受け取ると、その処理を行う。その処理は外部に対しては隠蔽されている。このため、もっと良い方法が見つかれば、処理の中身を自由に変更できるという素晴らしい利点を有している。オブジェクト指向の概念は、技巧的なプログラミングの世界を科学的・工学的にするという一翼を担った。僕自身もとても良いプログラミングスタイルだなと考え、情報科学部を設立したときに、1年生からJavaを学べるようにした。

しかし、変数はミュータブルであったために、並行処理が盛んになるに従ってその問題点が浮き彫りになってきた。オブジェクト指向では、オブジェクトは状態を有する。そして、状態はミュータブルである。状態の変更はオブジェクト内部の処理であるため、隠蔽され、外部からは見ることができない。並行処理では、オブジェクトが複数から同時にアクセスされることを許す。即ち、複数のものが状態を共有する。その結果、同時に状態の変更を要求されることがある。これは、競合と呼ばれる問題で、適切に解決しようとすると大変面倒なことになる。解決を提案した論文がたくさん出版されていることからも難しさが分かる。

実際、辛い経験もした。Webを用いて、経理にかかわる社員のだれもがアクセスできる会計システムを2000年の初めごろに開発した。それは、時間遅れのない会計情報が得られるということを売りにしたシステムであった。このため、トランザクションが生じたとき、会計データが瞬時にそれを反映することが要求された。その結果、データ更新の競合を避けるという面倒くさい作業が付与された。データの書き換えは一瞬であるのにもかかわらず、書き換えるための安全性を保障するために、余分な沢山のプログラムを必要とした。無事に完成はしたが、もう二度と関わりたくはないとも思った。

Haskellはイミュータブルである。従って、オブジェクト指向言語が持っていた並行処理を行う上での問題点は回避される。ただ、変数の値を変えることに馴染んでいるプログラマーは最初は戸惑うことと思う。変数を便利に多用していたことと思われるので、手足をもがれたように最初は身動きできないかもしれない。プログラムを再帰的に記述するというのが、イミュータブルな言語での解決策である。しかし、これに慣れるには時間のかかる人が多いであろう。

しかし、抽象化はとても大切な概念である。抽象化することによって、必要ではなさそうな細かすぎる問題から回避され、その本質だけに集中できるようになる。これにより、開発の時間は短縮され、信頼性は一段と高まる。デバッグのいらないプログラム開発を目指すこともできるようになる。

それでは、次回から圏論の話から始めよう。

上総国分尼寺を訪れる

古代史の勉強の一休みに、永井路子の『美貌の女帝』を読んでいる。

f:id:bitterharvest:20160929050548j:plain

主人公は氷高皇女(ひたかひめみこ、680-748)だ。女性の持統天皇の孫娘で、元正天皇となった(彼女の弟の軽皇子文武天皇になる。この姉弟の母の阿閇(あへ)皇女は、文武天皇がなくなった後、元明天皇となる。息子から母への継承である。彼女の母は蘇我姪娘(めいのいらつめ)である。氷高皇女は元明天皇の後をついで天皇になる。母から娘への継承である。なお、氷高皇女の父親は、草壁皇子であるが、夭折する。また、氷高皇女は結婚することはなかった)。この時代、女帝天皇が相次ぐ。皇太子が若いために、成人するまでの中継ぎとして、皇后が天皇になったといわれている(氷高皇女の場合は例外で娘)。永井路子のこの小説は、そうではなく、蘇我の娘として、男子の天皇に劣ることなく国家を統治してゆくという設定になっているようだ。まだ、読み始めなので、断定的なことは言えないのだが、これまでの視点とは違った見方に立って氷高皇女が描かれている。40ページほど読み進んだが、関心を持ちながら読んでいる。

元正天皇は、甥の聖武天皇(701-756)に譲位する(聖武天皇文武天皇の子)。聖武天皇が在位していた時期(724-749)、天然痘(737)が発生し、藤原の四兄弟(不比等の子供)を始めとして、要職についている人たちの多くが死亡するという惨事や、災害や世の乱れ(729年に長屋王の変、740年には藤原広嗣の乱)などもあり、聖武天皇は救いを求めて仏教に深く帰依する。741年には、国分寺建立の詔を出す。これにより、全国60余りの国にそれぞれ、国分寺が建立される。

国分寺は、男の僧のための国分僧寺と尼僧のための国分尼寺とがあり、それぞれの国に、対となって建立された。千葉県の市原市には、市役所を挟むようにして、上総(かずさ)国の国分僧寺国分尼寺の遺跡がある。今回(9月28日)は、この二つの寺の跡を訪ねることにした。
f:id:bitterharvest:20160929093819p:plain

市原まではかなり遠いのだが、4月に新宿に大きなバスターミナルが完成したので、そのバスタ新宿を見がてら、市原市役所行きの高速バスを利用することとした。

バスタ新宿新宿駅南口にある。改札を出ると目の前にその建物が見える。
f:id:bitterharvest:20160929101207j:plain

バスターミナルは4階にあるので、エスカレータで上り、少し進むと切符売り場に行きつく。
f:id:bitterharvest:20160929102253j:plain
最近の日本の盛り場はどこもそうなのだが、ここも例外ではなく、日本語以外の言語の方が優勢である。長距離バスの方が安いこともあって、利用する人も多いのだろう。先日、長野から出てきた同級生は、バス代がなんと片道1500円だったと、教えてくれた。高速バスの低価格に馴染んでいる人にとっては、このような価格は当たり前のようになっているのだろうが、新幹線の料金しか知らない身には、驚愕の価格である。全く競争にならないと思う一方で、やはり、事故も多いのだろうと昨今のバス事故を思い出して改めて考えさせられた。

案内所を出ると、長距離の高速バスが客を待ち構えている。
f:id:bitterharvest:20160929103207j:plain

僕のバスの乗り場はA3である。そこに行ってみると、ひとつ前の東京ディズニーランド行きのバスが丁度お客さんを乗せるところであった。人気がある場所への便ということで満席で出発した。市原市役所行きのバスを待つ場所も指定してあったが、そこには誰もいなかった。込み合うこともあるまいと思い、発車時間までしばらく間があったので、バスタ新宿からの景色を楽しむため窓に沿って散策した。

バスタ新宿側から新宿駅南口を見るとこんな感じである。
f:id:bitterharvest:20160929103724j:plain

立派になったものだと思う。高校生の頃はこの南口をほぼ毎日利用した。改札を出て甲州街道を四谷の方に下り、明治通りを渡ったところに僕たちの高校の正門があった。その頃の南口には駅舎が唯一つあるだけで、賑やかなところではなかった。

駅を出るとすぐに陸橋に差し掛かるが、橋の下には、まだ、蒸気機関車を見ることもできた。現在はこぎれいな電車が出入りしている。
f:id:bitterharvest:20160929104409j:plain

さて、出発時間の10時を少し遅れて到着したバスに乗り込んだ。乗客は6人、寂しいぐらいの乗客数であったが、示し合わせたかのように、ばらばらに適当な間隔をあけて席に着いた。途中、渋滞も少しあったが、1時間半ほどで目的地の市原市役所に着いた。

降車した後、市役所、国分寺中央公園を右側に見ながら国分尼寺へと向かう。しばらく行くと、次のような看板に行きついた。
f:id:bitterharvest:20160929111303j:plain

歩いている方向に間違いがないことを確認し、安堵する。さらにしばらく進むと、展示館にたどり着いた。ここを見るのは後回しにして、まずは、寺の跡を見ることにする。展示館のそばからの国分尼寺跡の風景は次のような感じ。
f:id:bitterharvest:20160929111800j:plain

綺麗に整備されていて、とても、のどかな感じである。周りには、掃除をしている人がいるだけで、観光客は一人も見つからない。一人でこの風景を独占でき、とても、いい気分になってさらに奥へと進んでみる。回廊がズームアップされる。
f:id:bitterharvest:20160929112206j:plain

さらに進むと中門が表れる。立派な門である。
f:id:bitterharvest:20160929112337j:plain

門の復元だけで2億円、回廊には12億円要したとのことであった。さらに回り込んで、回廊を外側から見る。
f:id:bitterharvest:20160929112941j:plain

金堂院は金堂基壇だけが復元されている。基壇から中門を望んだ風景は次のようである。
f:id:bitterharvest:20160929113339j:plain

回廊の内側は法隆寺を真似て復元したとのことであった。
f:id:bitterharvest:20160929113451j:plain

回廊で囲まれた庭は整然と石が敷き詰められていた。
f:id:bitterharvest:20160929113725j:plain
f:id:bitterharvest:20160929113950j:plain

展示館に戻って、テレビでの説明20分、模型を使っての説明10分を受けた。
f:id:bitterharvest:20160929114239j:plain

上総国分尼寺は、全国の国分尼寺の中でも最も規模が大きいそうで、その総面積は12万平米にも及ぶとのことである。現在、その1/3を市が所有し保護に努めているそうだ。

国分尼寺が建立されたころの日本は、律令制のもとでの中央集権国家で、天皇が強い力を持っていた時代である。地方は国ごとに支配され、それぞれの国には国府が置かれ、行政官である国司は中央から派遣されていた。また、それぞれの国は郡に分かれており、郡司には地方の豪族が就いていた。このため、実際の実務は豪族によって仕切られていた。

この当時の民衆たちは、律令制の基本である公地公民制で縛られていた。班田収授法に基づいて土地が割当てられたものの、租庸調によって一定の税金や使役が課せられた。

民衆たちは、豪族の監督の下で、国分寺建立の作業に従事させられたと思うが、随分と辛い仕事だったのではないかと考えるのだが、これを立証するような資料は残されていないので、想像の域を出ない。国分尼寺は、完成するまでに、20年の歳月を要したそうである。

尼寺が出来上がった当初は20人の尼僧が住み、その後、倍の40人になったそうである。雑用などをする人を含めて、全体では百人程度の人が住み、政務所と呼ばれるところで、日常の業務をしていたとのことである。

尼寺の概略は、次の看板によって知ることができる。
f:id:bitterharvest:20160929204405j:plain

尼寺を後にして、僧寺の遺跡に向かった。正面には、上総国分寺の石碑があった。
f:id:bitterharvest:20160929121254j:plain

中に入ると仁王門がある。
f:id:bitterharvest:20160929121728j:plain

坂東だなと思わせる塔もあった。将門塔である。将門伝説の一つなのだろう。
f:id:bitterharvest:20160929121904j:plain

さらに奥に進むと薬師堂がある。1716年に完成したとのこと。古風な趣のある建物である。
f:id:bitterharvest:20160929122123j:plain

少し外れたところに、国分僧寺の遺跡がある。70メートルの高さはあったであろうと推察されている七重塔の心礎が残っている。
f:id:bitterharvest:20160929134807j:plain
とても大きな石のため、このあたりでは採石できないので、50Km離れた鋸山から持ってきたのではと推察されている。心礎を囲むように、土台となる石が周りに配置されている。
f:id:bitterharvest:20160929135015j:plain

この周囲も市が所有しているのだろう。緑の野原になっていた。
f:id:bitterharvest:20160929135127j:plain

国分寺の遺跡が見つかっているのだから、国府の遺跡もあるだろうと尼寺の展示館で質問したところ、残念ながら見つかっていないとのことであった。

律令制の下では、税を都に納めることが課せられていた。納入するのは、民衆の役目である。当時、都と上総を結ぶのは東海道であった。当時の東海道は現在とは異なっている。相模国から、武蔵国に入るのではなく、海路で安房国を経由し、その後、陸路で上総国に向かっていた(武蔵国へは東山道を利用して上野国経由で至った)。市原のあたりにも古道らしきものは見つかっているようだが、当時の古道はたいそう立派な道路であったと予想される。しかし、そのような整備された古道を使ったとしても、上総から都まで、税を民衆が運ぶのは大変な夫役だったのではと思われる(納入する人夫は運脚と呼ばれた)。何人もの人が故郷に戻れなかったことであろう。

律令制を行き渡らせる中で、国府、郡衙国分寺、街道などの建設が行われたわけだが、当時の王朝の権力がいかに強かったかが、これらの遺構を見るたびに、認識させられる。

遺跡見学に満足し、バス停でおにぎりをほおばった後、帰路に着いた。高速道路で事故があり長い渋滞になっているという情報をチケット売り場で運よく入手したので、内房線五井駅から電車を利用した。快い疲れの中で、電車の中で軽い睡眠をとっての帰宅となった。

追:乙巳の変(645年)で蘇我入鹿は殺害され、親の蝦夷は自殺する。これにより、蘇我本宗家は滅びる。しかし、この変で、蘇我家の一族である蘇我倉山田石川麻呂(そがのくらのやまだのいしかわまろ)は、蝦夷・入鹿の滅亡を企てる側に立つ。この変の後、この一族は天皇家外戚(母方の親戚)となり、蘇我倉山田石川麻呂の血は氷高皇女(元正天皇)まで引き継がれる。(元正天皇が譲位した)聖武天皇の母は藤原不比等の娘の宮子、また、妻は同じ不比等の娘の光明子である(宮子とは異母姉妹)。これを機に、天皇家外戚蘇我から藤原へと移り変わる。