言語仕様を決めてから20ヶ月以上…。予定していた構文・機能は全部動くようになりました。バグはまー残っているでしょうし、パフォーマンスもこれから、後から追加した機能で中途半なモノもあります。が、擬似コード/机上コード(処理系なし)として書いたものが今は全て実行できます。
以下、スクリプトコードを羅列するだけ。
まず、2010年11月に机上で書いた無限FizzBuzz。2011年の時点で(「CatyScriptで無限に動き続けるFizzBuzzが書けた」)は代替手段を使って動いていたのですが、今は当初の予定通りの繰り返し制御構造begin/repeatで動きます。
module sample; /** 入力が、引数で指定された数で割り切れるかどうかを判定する */ command dividable [integer] :: integer -> (@True integer | @False integer) { [ [pass > n, %1] | num:mod, 0 ] | eq | when { True => @True %n, False => @False %n, } }; /** 入力値を開始の数として、FizzBuzzを永遠に続ける。 * 出力を生成することはないので、出力の型はneverである。 */ command fizzbuzz-forever :: integer -> never { begin { dividable 15 | when { True => [pass, ("Fizz Buzz" | cout)] | nth 1, // 15の倍数 False => dividable 3 | when { True => [pass, ("Fizz" | cout)] | nth 1, // 3の倍数 False => dividable 5 | when { True => [pass, ("Buzz" | cout)] | nth 1, // 5の倍数 False => [pass, (to-string | cout)] | nth 1 // その他 } } } | sleep 1000 | num:inc | repeat } };
次は「CatyScriptで記述するCatyシェル」で書いたシェル。これもbegin/repeatで繰り返し、例外捕捉try/catchも使っています。
/** quitコマンドであるかどうか判定する */ command quit-p :: string -> @True string | @False string { pass > in | text:regmatch "^\\s*quit\\s*$" | when { OK => @True %in, NG => @False %in, } }; /** エラーメッセージをコンソールに印字する */ command print-error :: void -> void { "ERROR" | cout }; /** 評価して結果をコンソールに印字する */ command eval-print :: string -> void { try {eval} | catch { except => print-error, normal => xjson:pretty | cout } }; /** シェルを実行する */ command shell :: void -> void { "Caty interactive shell" | cout; begin { "> " | cout --no-nl; cin | quit-p | when { True => void, // ブロックを抜けて終了 False => eval-print; repeat } } };
CatyScriptに高階関数(関数を引数にする関数)はないのですが、文字列のevalを使うとモナドはけっこう簡単に書けます。典型的なモナドであるListモナドとMybeモナド。
/** コマンドライン文字列 * CatyScriptは高階の対象(指数対象)を直接は扱えない。 * しょうがないから、文字列として実行すべきコードを渡す。 */ type commandStr = string(remark="コマンドライン文字列"); /** Listモナド台関手の対象部分 */ type List<T> = [T*]; /** Listモナドのその他の構成要素 */ class List { /** Listモナド台関手の射部分 * List型のmap関数 */ command map<S, T> [commandStr /* S -> T */] :: List<S> -> List<T> { each { [pass, %1] | eval } }; /** モナド単位 */ command unit<T> :: T -> List<T> { [pass] }; /** モナド乗法 */ command flatten<T> :: List<List<T>> -> List<T> { list:concat }; }; /** Maybeモナド台関手の対象部分 */ type Maybe<T> = { "defined": boolean, "value": T?, }; /** Maybetモナドのその他の構成要素 */ class Maybe { /** Maybeモナド台関手の射部分 * Maybe型のmap関数 */ command map<S, T> [commandStr /* S -> T */] :: Maybe<S> -> Maybe<T> { pass > in | $.defined | cond { false => {"defined": false}, true => [%in | $.value, %1] | eval > value; {"defined": true, "value": %value}, } }; /** モナド単位 */ command unit<T> :: T -> Maybe<T> { {"defined": true, "value": pass} }; /** モナド乗法 */ command flatten<T> :: Maybe<Maybe<T>> -> Maybe<T> { pass > in | $.defined | cond { false => {"defined": false}, true => %in | $.value.defined | cond { false => {"defined": false}, true => {"defined": true, "value": (%in | $.value.value)}, }, } }; };
次のTreeRec(ツリーレコード)モナドはベキ等モナドですが、型の定義には再帰と型パラメータが使われています。
/** TreeRecモナド台関手の対象部分 * TreeRecは、再帰的な総称型である。 */ type TreeRec<T> = { *: (T | TreeRec<T>)? }; /** TreeRecモナドのその他の構成要素 */ class TreeRec { /** モナド台関手の射部分 * TreeRec型のmap関数 * TreeRec型が再帰的なので、再帰的コマンドになる。 */ command map<S, T> [commandStr /* S -> T */] :: TreeRec<S> -> TreeRec<T> { each --obj { when { object => TreeRec.map<S, T> %1 , * => [pass, %1] | eval , } } }; /** モナド単位 */ command unit<T> :: T -> TreeRec<T> { {"": pass} }; /** モナド乗法 */ command flatten<T> :: TreeRec<TreeRec<T>> -> TreeRec<T> { /* TreeRec はベキ等モナドである */ pass }; };
最後に、僕が一番欲しかった/入れたかった機能はgoto制御(forward構文)なので、繰り返しではなくて、制御をピンポンして無限に Hello, world. を印字し続けるスクリプト。
/** Hello, と印字してworldにフォーワードする */ command hello :: void -> never { "Hello, " | cout --no-nl; forward sample:world }; /** world. と印字してhelloにフォーワードする */ command world :: void -> never { "world." | cout; forward sample:hello };