このブログは、旧・はてなダイアリー「檜山正幸のキマイラ飼育記 メモ編」(http://d.hatena.ne.jp/m-hiyama-memo/)のデータを移行・保存したものであり、今後(2019年1月以降)更新の予定はありません。

今後の更新は、新しいブログ http://m-hiyama-memo.hatenablog.com/ で行います。

エルゴット・イテレーション付きのCatyScript

CatyScript 2.0 で何ができるか? まずは例から:


/** 整数値の階乗を求める
* 入力として負の整数を許す、負の入力に対する出力はその数
*/
command fact :: integer -> integer {
[dec, pass] | fact2
};

command fact2 :: [integer, integer] -> integer {
doset {
nth 1 >= n,
nth 2 >= m
};
%n | zero-p || negative-p |
when {
OK => %m,
NG => [(%n | dec), ([%n, %m] | mul)] | repeat // 繰り返し
}
};

機能も視認性も普通のプログラミング言語遜色ない -- 慣れの問題はあるだろうが。比較にJavaScript版。


function fact(k) {
return fact2(k -1, k);
}

function fact2(n, m) {
if (n <= 0) {
return m;
} else {
return fact2(n - 1, n * m);
}
}

以下に補足:

コマンド

事前定義されたコマンドは:

  1. dec :: integer -> integer //* 入力から1引く
  2. mul :: [number, number] -> number //* 掛け算をする
  3. zero-p :: number -> @(OK|NG) number //* ゼロかどうか判断する
  4. negative-p :: number -> @(OK|NG) number //* 負の数かどうか判断する

変数参照

変数参照はパーセントを使う。ほとんどどこにでも変数参照を書ける。

set, doset構文

set構文は、set { varname = expression, ...}。JSONオブジェクトを使うのはやめた、変数名は裸で書いて、代入記号はイコール使う。doset構文は、set構文の diagrammatice order (do)版。'>=' は不等号じゃないのよね!(クレーム来そうだが) 変数が1個のときに、set varname = expression を許すか? 許したほうが使いやすそうだが、expressionの終端が問題(大丈夫かな)。

制御構造

  1. '|' は単なるパイプ
  2. ';' は左側からの入力を捨てるパイプ、f ; g は f | void | g
  3. '||' は短絡ORを意味するパイプ f || g は f | (when {OK==>pass, NG=>g})
  4. '|&' は短絡ANDを意味するパイプ f |& g は f | (when {OK=>g, NG==>pass})
  5. repeatはスコープの最初に戻る。スコープはトップレベルブロック、beginブロック、eachブロックで作られる。repeatの入力は、そのまま次の繰り返しの入力となる。繰り返しに入る直前でブロック内の局所環境は完全にクリアされる。ループカウンタなどは入出力で渡すかファシリティを使うしか無い。異なる繰り返し実行において局所状態は共有できない。

注目!

注目すべきはrepeatで、beginと共に(beginはなくてもいいが)エルゴット・イテレーションを実現する。エルゴット・イテレーションは、コンウェイ不動点の双対であり、直和をモノイド積とするモノイド圏のトレースと余対角で表現できる。

実行モデルは、一方向に進むだけ。ルーピングする繰り返しはあっても、「呼ばれて戻る」という概念はない。したがって、戻るために何かを覚えておくこともない。スコープごとの束縛環境はあるが、コールスタックはない。

エルゴット・イテレーション(goto-with-data; 行きっぱなし制御)を使った言語ってあんまりないと思うが、call/return方式と比べて、goto-with-data方式のほうが有利な点がある。

  1. 意味論が単純。
  2. 完全に図式的な表現ができる。(レイアウトが難しいが。)
  3. スタックフレームを使わないので、スタックオーバーフローはない。(無限走行はある。積極的に無限走行してもよい。)
  4. 現在のコンピュータハードウェアによる処理に向いているので、低水準での最適化がしやすい。可能性としては、少ないメモリで高速に実行できる。

call/returnではないのにも関わらず、考え方は再帰である。one-way recursion(一方向再帰)とでも呼ぼうか。累積変数を使う(last-call最適化された)末尾再帰だと思えばよい。リストの逆転を書いてみる。図式的に、つまり厳密に型付けされたフローチャートで考えるのがコツ

command reverse :: [T*] --> [T*] {
[ [ ], pass ] | reverse2
};

command reverse2 :: [ [T*] , [T*] ] --> [T*] {
doset {
nth 1 >= acc, // 累積変数
nth 2 >= rest // 残っている処理対象
};

%rest | list:empty-p |
when {
OK => %acc,
NG => doset {list:car >= first, list:cdr >= rest2};
[ ([%first, %acc] | list:cons), %rest2 ] | repeat // 繰り返し
}
};

JavaScript版、補助関数付き。


function list_empty_p(lis) {
return lis.length == 0;
}

function list_car(lis) {
return lis[0];
}

function list_cdr(lis) {
lis.shift();
return lis;
}

function list_cons(x, lis) {
lis.unshift(x);
return lis;
}

// ここからが本チャン

function reverse(lis) {
return reverse2([], lis);
}

function reverse2(acc, rest) {
if (list_empty_p(rest)) {
return acc;
} else {
var first = list_car(rest);
var rest2 = list_cdr(rest);
return reverse2(list_cons(first, acc), rest2);
}
}

beginブロックを使って1つにまとめれば:

command reverse :: [T*] --> [T*] {
[ [ ], pass ] |
begin {
doset {
nth 1 >= acc, // 累積変数
nth 2 >= rest // 残っている処理対象
};

%rest | list:empty-p |
when {
OK => %acc,
NG => doset {list:car >= first, list:cdr >= rest2};
[ ([%first, %acc] | list:cons), %rest2 ] | repeat // 繰り返し
// beginブロックの最初から繰り返す
}
}
};

簡単なカウントダウンも挙げれば:


/* e.g. caty> 10 | countdown */

command countdown :: integer -> void {
zero-p || negative-p |
when {
OK => void,
NG => dump | dec | repeat
}
};

あと、無限ループ:

command forever :: void -> void {
do-something; repeat
};

その他

ハイフン変数とパス記法は入れてもいい気がする。%- は input、%-.1 は input[1]。%-1 は %_ARGV[-1]。%foo[1].a.bとかもみたまんま。