bitterharvest’s diary

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

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;
}

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