このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

モニャドセミナー4の補足:有界カウンタとそのオペレーション・モノイド

[追記]おまけとして、ホワイトボード絵算の写真(短い説明付き)を付けました。[/追記]

モニャドセミナー4で、有限個の値を取るカウンタを話題にしました。upはカウンタ値を1増やす命令、downは1減らす命令です。カウンタの上限値まで達すると、upしても何も起こりません(エラーではありません)。同様に、下限値でdownしても何も起こりません。

とりあえず、カウンタそのものは忘れて、オペレーション(操作命令)であるup, downだけに注目します。いくつかのオペレーションを連続して実行することを示すのに、[up, up, down]のようなリスト形式を使います。リストの左から右に向かってオペレーションが順次実行されるとしましょう。

[up, up, down] に続けて、[down, down] を実行すると、[up, up, down, down, down] を実行することになるので、リストの連接が、オペレーションの逐次実行に対応しています。空リスト[]は連接の単位であり、何もしないこと(NOP)を表します。

表記を簡単にするために、upを'u', downを'd'として、リスト[up, up, down]を"uud"という文字列で書き表しましょう。空列は""です。混乱の心配がないなら、ダブルクォーテーションも省略して、uud のようにも書きます。

こうしてみると、カウンタオペレーションの全体は、'u'と'd'の2文字から構成される文字列に連接演算を入れたモノイドとなります(単位元は空文字列)。このモノイドを、以下Mと書きます。eは空文字列を表す定数です。

x, y, z∈M (つまり、x, y, zは、'u', 'd'からなる任意の文字列)、連接演算を「・」として:

  1. (x・y)・z = x・(y・z)
  2. e・x = x
  3. x・e = x

もちろん、up, downを含むリストのまま扱っても何も変わりません。命令を1文字に短縮して文字列にすると、手書きが楽になるのと、親しみが持てる雰囲気になるだろう、と思っただけです。(プログラミング上はリストのままのほうが扱いやすいでしょう。)

--

プログラムの計算単位(関数と思っていいです)が、カウンタを直接触るのではなくて、up, downのオペレーション(変更リクエスト、作業依頼メッセージ)を経由する前提なら、計算単位は、'u', 'd'からなる文字列(空文字列でもいい)を、計算結果である戻り値とは別な出力チャンネルに吐き出せばいいわけです。

ほんとのカウントアップ、カウントダウンを「いつ誰がやるのか?」を気にしないなら(気にする必要はない)、複数の計算単位が順に吐き出した文字列を、連接によって累積していけば、カウンタ操作に必要な情報は確実に保持されます。


var __counter = 0; // 直接触らないでね!
var _counterOperations = ""; // これも直接触らないでね!

// カウンタを操作した *つもりになる* 関数
function operateCounter(s) {
_counterOperations += s;
}

// 足し算のついでにカウンター操作
function sum(x, y) {
var ans = x + y; // 純計算
operateCounter("uud") // 副作用
return ans;
}

// 1たすついでにカウンター操作
function succ(x) {
var ans = x + 1; // 純計算
operateCounter("dd"); // 副作用
return ans;
}

// カウンタ値に累積された操作を反映させる
function execCounterOperations() {
// 後で考えましょう
}

ほんとは、_counterOperationsを直接見るのは好ましくないのだけど、_counterOperationsを覗きながら、sumとsuccを実行してみると、こんなです。


js> _counterOperations

js> sum(2, 3)
5
js> _counterOperations
uud
js> succ(5)
6
js> _counterOperations
uuddd
js> succ(sum(2, 3))
6
js> _counterOperations
uuddduuddd
js>

succ(sum(2, 3)) はまだいいのですが、sum(succ(2), sum(1, 1)) のような呼び出しだと、succ(2), sum(1, 1) のどちらが先に実行されるかがハッキリしません(決まっていればそれでいい*1とも言えますが)。カウンタ操作の順序も予測しにくくなります。複数引数をタプル化して、「1-引数 1-戻り値」にすると話が単純になります。理論的な考察では話が単純なほうがいいので、「1-引数 1-戻り値」の状況で考えることが多いようです。

--

カウンタ操作の列(リストでも文字列でもいいです)をもとにして、カウンタ値をほんとに変更(破壊的代入)をするところを考えましょう。実はこれが本題です。つっても、たいしたハナシじゃないのですが、僕がセミナー中に「整数の足し算/引き算じゃダメかな?」とかトチ狂ったことを言ってしまったので反省の意味で。

素朴に実装するのは簡単です。


var UPPER_BOUND = 9;
var LOWER_BOUND = 0;

function doUp() {
if (__counter < UPPER_BOUND) __counter++;
}
function doDown() {
if (__counter > LOWER_BOUND) __counter--;
}

var UP = 117; // 'u'
var DOWN = 100; // 'd'

function execCounterOperations() {
for (var i = 0; i < _counterOperations.length; i++) {
var op = _counterOperations.charCodeAt(i);
switch (op) {
case UP:
doUp(); break;
case DOWN:
doDown(); break;
default: throw "Unbelievable!";
}
}
// 累積されたカウンタ操作リクエストをクリアする
_counterOperations = "";
}

モニャドセミナー4のときは、「0から9までの10個の値を取る」としたのですが、全部書き並べるには10個でも多いので、0, 1, 2 の3個の値にします。上のJavaScripコードでは、var UPPER_BOUND = 2; とすることになります。

0→1, 1→2, 2→2 を (1, 2, 2)のように書くことにして、u(up)、d(down)の組み合わせがどうなるか実際に手作業で調べます

[追記]手作業でやったら、だいぶ間違いがありました。使い捨てのプログラムを作ってやり直しました。以下はその結果に基づき書き換えました。[/追記]

次のように置きます。

  • u = (1, 2, 2)
  • uu = (2, 2, 2) = U
  • d = (0, 0, 1)
  • dd = (0, 0, 0) = D
  • ud = (0, 1, 1) = X
  • du = (1, 1, 2) = Y
  • uud = (1, 1, 1) = V

カウンタ操作のモノイド演算を露骨に書き下してみます。次の表のようになります。このモノイドの掛け算九九の表です。モノイド演算の第一因子uと各要素との掛け算の結果が第1行(横の並び)です。

u d U D X Y V
u U X U D V u V
d Y D U D d V V
U U V U D V U V
D V D U D D V V
X u D U D X V V
Y U d U D V Y V
V U D U D V V V

この表を使って、表示をできるだけ短くするようにすれば、どんな命令文字列でも長さ3までの文字列に簡約(縮約、還元)できます。実際に現れる長さ3までの文字列を列挙すれば:

  1. ""
  2. "u"
  3. "d"
  4. "uu"
  5. "dd"
  6. "ud"
  7. "du"
  8. "uud"

長い命令文字列を、意味を変えずに“圧縮”できます。例えば:


uuddduuddd
= UDdUDd
// UD = D
= DdDd
// Dd = D
= DD
// DD = D
= D
= dd

表を見ていると、次の法則に気が付きます。

  • xが何でも、xU = U
  • xが何でも、xD = D
  • xが何でも、xV = V

この法則を使ってみると:


uuddduuddd
= UDdUDd
= (UDdU)Dd
// xD = D
= Dd
// Dd = D
= D
= dd

--

まーいずれにしても、カウンタ値の状態とは離れて、カウンタ操作のモノイドMだけを話題にできます。カウンタ操作であるup, downがカウンタに対してどう作用するか、つまり、どういう状態遷移を引き起こすかを知らなくても、カウンタ操作のモノイド演算はちゃんとできます。

カウンタの実体である大域変数に、最終的に破壊的代入を行う部分をカプセル化して、更新リクエスAPIだけを公開すると、API利用者からは、カウンタ値の状態遷移は見えなくなります。どうでもよくなります。切り離せます。

そこで、カウンタ操作のモノイド演算だけをベースにして、カウンタ操作をAPI経由で行う計算単位(副作用有り関数とか)達を整合的に順次実行する方式が定義可能となります。それが、クライスリ射のクライスリ結合であり、計算単位達が総体としてクライスリ圏を構成するわけです。クライスリ圏における結合法則と単位法則は、モノイドMの結合法則と単位法則から導かれます。そのことを視覚的に納得するには絵図を描いてみてくださいな -- っと、ここいらのことがモニャドセミナー4の内容でした。


純計算部分が X→Y であるような計算単位f。副作用は、モノイドMの値(オペレーション列)を、戻り値とは別チェンネルに出力することで実現。よって、f:X→Y×M。もうひとつ g:Y→Z×M があると、fとgをどうやって結合(compose)する? 普通の結合じゃうまくいかないよ。

モノイド演算を利用してクライスリ結合を作れば具合がいいぞ。

fとgのクライスリ結合を f#g とすると、結合法則は (f#g)#h = f#(g#h) だけど … 成立するのかな?

クライスリ射 f:X→Y×M に対して、W(f) = (f×idM);(idY×m) と定義しよう。mはMのモノイド演算。f' = W(f): X×M→Y×M となる。このW(ラッピング)は、クライスリ圏から普通の結合を持つ圏への関手になっている。関手の条件のひとつは:

  • W(f#g) = W(f);W(g) -- 結合(合成)を保つ

さあどうでしょうか?

このラッピングのいいところは、アンラップで包んだ中身を取り出して再現できることだね。ラップ/アンラップをすることで、fやgのようなコンポネント実装に手を加えずに、異なる実行環境の上で動かせる。そのとき、構造やセマンティクスは保存(あるいは整合的に変換)することが保証される。

*1:JavaScriptでは、引数は左から右に評価しているようです。