何か FizzBuzz を書くのが流行ってるらしいので、 Python で書いてみた。
Y = lambda f: ((lambda p: f(lambda x: p(p)(x)))
(lambda p: f(lambda x: p(p)(x))))
fizzbuzz = lambda l: ((lambda f:
(lambda n: '' if n > l else (
'Fizz Buzz' if n % 3 == 0 and n % 5 == 0 else
'Fizz' if n % 3 == 0 else
'Buzz' if n % 5 == 0 else
str(n)) + '\n' + f(n + 1))))
print Y(fizzbuzz(100))(1)
わざわざ再帰を使って、さらに Y コンビネータを使って、任意の数から始めて任意の数まで FizzBuzz させるというコード。改めて思うが、 Python はこういうジョークコードを書くのにまったくもって向いてない。まあ、悪い冗談の一つだなこりゃ。
Paradise Lost の新作。メタル路線に回帰した前々作 "Paradise Lost" と前作 "In Requiem" がどちらも素晴らしい作品だったので当然今作も大いに期待しており、そしてこれは期待に違わぬ力作となった。
アルバムのオープニングナンバーの "As Horizons End" は彼らの Myspace でいち早く公開された曲の一つ。まず3/4拍子のリズムに意表を突かれる曲だが、さらに Nick が初期のドゥーム・デス時代を思い起こさせる歌唱をしていることに驚かされた(といってもあそこまでワイルドではなく、十分に抑制された歌い方)。他の曲でもヘヴィな歌唱が聴かれ、バックの演奏もそれに輪をかけてヘヴィ(7弦ギターを導入したらしい)、それでいてメロディの質は過去の名作と比べてもまったく劣らない。
先に挙げた "As Horizons End" の他にも、彼らには珍しいアップテンポでアグレッシブな "Frailty" やどこまでも陰鬱な "Last Regret" など、印象深い曲を多数収録。中でもタイトルナンバーの "Faith Divides Us - Death Unites Us" の壮大さは素晴らしすぎ。
幻霧ノ塔ト剣ノ掟を遊んでるんだが、難しいとか簡単とか以前の問題で UI が酷すぎないかこれ。
その他諸々、この UI はいくらなんでも弁護しきれない。ダンジョンのギミックは中々面白いだけに、非常に残念。戦闘バランスはどうもかなり大雑把な模様だが、まだ序盤なのでなんとも。
Caty の次のリリースではいろいろ変更点が加わる予定なのだが、その中の一つがログイン状態のチェック処理。今のログインチェック処理はログインしてるかどうかしか見てないので、ログイン ID とかのチェックも入れる予定なのだが、その他にチェック失敗時の動作を変える予定。
今の仕様だと、ログインしてない状態で以下のスクリプトへのアクセスがあると、ログイン画面へのリダイレクトとなる。
loggedin | some_command | print foo.html
流石にこれは適用できる範囲狭いだろということで、ちょっと仕様を変更。タグディスパッチで動作を変えることにした。
loggedin | when {
OK => some_command | print foo.html,
NG => redirect /login.html
}
ただまあ、ログインチェック以外にもセキュリティ関連の前処理ってのはいろいろ必要なわけだ。例えば二重送信防止トークンチェック機能を check_dup_token というコマンドで実装したとすると、多分パイプラインはこうなる。
loggedin | when {
OK => check_dup_token | when {
OK => some_command | print foo.html,
DUP => redirect ...
}
NG => redirect /login.html
}
それぞれのコマンドの正常系のパス(この場合はどちらも OK)を辿れば最終的な出力がわかるので、まあいいかなという気もする。
ええまあまったくその通りで、ユーザ定義のメタコマンドを書かせるとなると、本気大マジでモナドを理解してもらう必要があり、さらにはメタコマンド同士の結合がモナド則を満たしていないと実行時に悲劇が起こる(あるいは実行すらできない)ので、こりゃまずいなと思ったわけです。
まず今の仕様の構文がこれ。
logged_in | has_token | is_valid | process_form | print /form/ok.html
エラー処理は全部自動(パイプラインを中断させる例外を発生させる)なので、これは使い物にならない。というわけでタグディスパッチでエラー処理を書く(previouse.html が元の画面とする)。
logged_in | when {
OK => has_token | when {
OK => is_valid | when {
OK => process_form | print /form/ok.html,
NG => print /previouse.html
},
NG => print /previouse.html
},
NG => print /login.html
}
when 式の } の後にカンマを忘れそうというのを除けばまあ悪くはないかなあと思いつつ、俺が提案したのはこれ。
logged_in | has_token | is_valid {
process_form
} | when {
OK => print /form/ok.html,
INVALID => print /previouse.html,
DUP_TOKEN => print /previouse.html,
NO_TOKEN => print /previouse.html,
NOT_LOGGED_IN => print /login.html
}
最後にエラー処理をまとめて書いた方が、色々精神衛生にいいじゃん。何よりこっちの方が読み易いし。
ただし、これを実現するにはコマンドを受け取ってコマンドを返すメタコマンドが必要で、そのメタコマンドはいわゆる関手であり、さらにはメタコマンドによって生成されるコマンドの型がモナド則を満たさないと使い物にならないことが判明。実際に使うのは Maybe モナドのバリアントみたいなものなのだが、仮にそれを Exceptional 型として、 Caty の JSON スキーマの構文風に書き下すと
type Exceptional = @OK _T | @INVALID | @NO_TOKEN | @DUP_TOKEN | @NOT_LOGGED_IN;
任意の型に OK タグを付けたものとそれ以外のタグが付いているもののユニオンとなる。実際にユーザ定義の Exceptional 型を使う関手を認めるなら、さらにこの型自体が拡張できないと不便なのだが、それは今のところ置いておく。
一旦先のパイプラインに戻る。 "logged_in | has_token | is_valid {process_form}" というメタコマンドの合成をどう捉えるかだが、これは次のパイプラインと意味的には同じ。ここでの pass は入力をそのまま出力にコピーするだけのコマンド。
logged_in {pass} | has_token {pass} | is_valid {process_form}
この時の各コマンドの型は process_form の入出力の型を X, Y とおけば
logged_in {pass} :: X -> @OK X | @NOT_LOGGED_IN
has_token {pass} :: X -> @OK X | @NO_TOKEN | @DUP_TOKEN
is_valid {process_form} :: X -> @OK Y | @INVALID
となり、普通に考えると型が違うのでコマンドを繋げられない。そこでモナドを(無理やり)導入して、 Haskell 風に書くなら
return a = @OK a
(@OK x) >>= f = f x
@NOT_LOGGED_IN >>= f = @NOT_LOGGED_IN
:
:
というモナド則を導入し、パターンマッチでコマンドの結合の行われているポイントで気合でディスパッチという事をすれば、先のようなコマンドラインを書く事ができる。
……が、いくらなんでもこれはハードコア過ぎる。もしもこれをやる場合、ただでさえコマンドとスキーマの宣言が必要なところへ
が入ってくるので、これはもう天地神明に誓って破綻しており、これだったら普通にタグディスパッチの入れ子を書いた方が一兆倍マシだし俺の労力も少なくなるというのが昨日のミーティングから戻る途中にいろいろ考えた挙句の結論。
もっとも開発の途中段階ではまず正常系だけ書いておき、エラー処理は後で書きたいみたいな要求も出てくるだろうし、そこで常にタグディスパッチしないと型エラーというのも(今は型エラーにはならないけど、静的チェック+型推論を導入するとエラーになる)どうかなと思わなくもない。いや、今は動的チェックしかしてないので正常系は普通に動くんだけどさ。
というわけでいろいろあって、ちと先のユーザ定義関手のアイディアは却下というかなんというか。ちなみに妥協案として考えていたものには次のようなものがあった。
try {
logged_in | has_token | is_valid | process_form
} | when {
OK => print /form/ok.html,
INVALID => print /previouse.html,
DUP_TOKEN => print /previouse.html,
NO_TOKEN => print /previouse.html,
NOT_LOGGED_IN => print /login.html
}
このやり方だと、先に存在した型エラーの問題は多分起きない。というのも try は関手ということなので、このパイプラインは
try {logged_in} | try {has_token} | try { is_valid } | try { process_form} | when ...
と等価で、 try 関手で移されたコマンドの結合さえどうにかすればいい。早い話が先に出したモナド則みたいなのを使ってコマンドを繋いでいけばいいわけで、これならタグディスパッチ方式と統合できるはず。一応補足的に書いておくと、 try 関手は
という機能をコマンドに与える関手である。実はこれはこれで予めエラー用のタグを登録する方法が悩ましかったり、エラー用のタグではなく例外が投げられたらどうすんのかとかいろいろあるのだが、まあ logged_in とかを全部関手にしてユーザ定義関手を認めるよりは混乱は少ないかなと。
追記:いやまてよ、これだと関手を適用する前の時点で結合不能だ。ここは A|B 型を A -> X 型のコマンドに渡せるようにして、実行時エラーが出ることを認めるべきだな(実用上はそっちの方が良さげ)。つまりここでの try 関手は、実行時エラーの出る結合を実行時エラーの出ない結合に移す関手となる。
さて Caty 本体に入れる予定の機能のひとつに JSON データを RDB や Datastore に永続化するためのライブラリがあるのだが、どうしたもんかな。まずは Python に SQLite が付属してることからも RDB への永続化を考えており、そこでの問題は二つある。
の二つだ。 Caty の利用者層及び利用される場面を考えれば、最初にスキーマをガッツリ決めてくださいってのは無理というか無謀。最終的にスキーマが決まるにしても、その途中ではスキーマの変化が起こりうる。そこで「スキーマ書き直す度に DB 作り直せ」ってのは、なんていうか有り得ないだろ。最低でもカラムの増減程度には対応しないと。
それに何より JSON スキーマではオプショナルな項目の存在を認めている。例えば以下のスキーマを考える。
type foo = object {
"x": string,
"y": integer
};
この場合、 x でも y でもない項目に対しては any 型が対応付けられ、任意のデータを格納可能となる。流石にそれはヤバいというのであれば、こういうアプローチも可能である。
type foo = object {
"x": string,
"y": integer,
*: string
};
任意の項目名で string 型を格納可能となる。もしも他の項目を認めないなら、その場合は never 型を指定すればよい。デフォルトでは any 型なのはまあやり過ぎなんだが(せめて string か)、何にせよ割と緩めのスキーマではある。
つまりこれは実際にはスキーマレスな RDB の設計に近いのだが、そのスキーマレスな RDB の設計手法は既にいくつか知られているものがあり、データ数のオーダーが大したことなければ下記の論文のようなモデル写像アプローチが都合がいい。
おそらくこの論文のアプローチを元にした手法でいけるはず。配列・リスト・タプルをどう扱うか、検索する時のクエリ言語をどうするかが問題だが、まあアイディアはある。
あとは元のスキーマがアグレッシブに変化しても過去のデータが取り出せるような細工が必要かどうかだ。流石に元々 string だったのが object になりましたとかいうのはダメだろうから、多分そういう部分はデフォルト値を入れて、あとは普通に取り出せるというのが妥協点だな。
Caty のスクリプトのパーサにすげえバグ発見。今気がついて良かったというか、テストコードを書き直して初めて発覚した。いや、実際には Caty のバグと言い切っていいかは微妙なんだが。
Caty スクリプトのオプション引数の解析には Python の OptionParser クラスを利用しているのだがこいつがなかなかの曲者で、
と二つの罠に引っかかった。最初の奴はだいぶ前に直したんだが、二個目の奴はいままで気がつかなかった。どっちにしろ適切なメソッドをオーバーライドしたクラスを用意すればいいのだが、どうにも気持ちが悪いというかなんというか。
今回の問題の本質というのは、おそらく null 値が認められているのが原因だろう。問題になった OptionParser のメソッドは次のような定義になっている。
def _get_args(self, args):
if args is None:
return sys.argv[1:]
else:
return args[:]
def parse_args(self, args=None, values=None):
rargs = self._get_args(args)
:
:
引数の値が None かどうかで処理を振り分けているが、ここでの問題は parse_args に None 値が入り込むことを防ぐ事が難しいということだ。もしも適当な文字列処理をした結果を parse_args に渡すとして、その値が None でないことをどうやって保証するのか。テストを書くのは確かにいいことだが、テストは大体において漏れるものだ。今回このテスト漏れが発生した理由は
という条件が重なった結果だ。そして新しいテスト環境ではコマンドライン引数を使っての起動となったので、結果としてバグが発覚したわけだ。
いっちゃあなんだが、今主流となっているプログラミング言語の多くはいつの間にか紛れ込んだ null 値に対して無防備過ぎる。契約による設計をサポートした言語なら多少はマシかもしれないが、そちらもテスト漏れと同じ問題を抱えている。なので、殆どの言語では null 値との永遠に続く敗戦処理に苛まれ続けることになる。
この問題に対する解法は Haskell の Maybe モナドや OCaml の option あたりで、確か Scala にも Option 型があったと思うが、 null 値が使える時点で片手落ちというかダメだ。俺はもういい加減 null との戦いには終止符を打ちたいのだが、どうも C# の nullable や Java の Null dereference expressions を見ると、世界はどうも間違った方向に進んでいるようにしか思えない。
BPStudy#26 で Caty について喋ることになりました。基本的に檜山さんにお任せしたいところですが、多分俺も何か喋らざるを得ないでしょう(ちゃんと打ち合わせしないと)。内容については流石に圏論がアレでモナドがソレでというのはヘヴィ過ぎるので、主に Caty の設計思想やデザイナとプログラマの責務を分割して作業するための要件など、そういう話が中心になると思います。
圏論デスレース拡張版〜米田のレンマ、その真の絶望〜(正式名称:第一回層圏トポス米田フェスティバル)に行ってきた。さっそくhirataraさんが感想をあげてますな。
今回は圏論の初心者に対して一日で米田のレンマまでぶっ通しで説明するという何かの法律に引っかかりそうな強行軍だったのだが、午前中に圏の基本的な公理群と関手・自然変換を説明して、それらの例をいくつか出す所までは終了。午後の 14:00 ぐらいから米田埋め込み〜米田のレンマに突入して、途中休憩を何回か交えつつも何とか大体終了。本当にどこまで無茶をすれば気が済むんだこの面子は。
もっとも集まったメンバーが何かもうハルマゲドンでも起こしそうな方々(C++ 標準化委員会の人たちとかガチ Haskeller とか)だったので、多分きっと大丈夫だったと思いたいところです。いや、本当にみなさんお疲れさまでした。
今日は新メニューとして中華風オムライス(別名:中身がチャーハンの天津丼)を作った。作り方は……いるのかこれ? まあ、一応書いておくか。まずは餡の材料から
作り方は以下の通り。
亀の歩みのようなすっとろい進行で進めてる「幻霧ノ塔ト剣ノ掟」だが、やっぱこれインターフェースと戦闘のテンポが致命的に悪いし、バランスもちとどうかと思う。戦闘のバランスは3Fの時点で戦士が単体では単なる壁に過ぎず、魔術師の呪文で敵を寝かせ(睡眠の効かない敵は呪文で倒す)、無防備になったところを叩かないとそもそも攻撃が当たらない。というか、寝てる相手にも攻撃を外すことがあるんだが。
ついでにエンカウント率もかなりワイルドで、全然エンカウントしねーとか思ってると、二歩ぐらい歩くごとに敵が襲いかかってきたりする。なんとなくこのゲームの乱数の取扱いとパラメータの計算の方向性はわかったが、ゲーム黎明期ならともかく近代でこれやっちゃダメだろそれっていうのが正直な感想。
そもそも敵が5体×2グループとかで出てくるんだから、普通に殴ってたらタルくて仕方がない。ある程度壁になれる戦士を育てたら、他の面子はひたすら魔法連打の方が良さそうな気がしてきた。このゲームは戦士や魔術師一本でキャラを育てるのでなく、戦士・盗賊・魔術師・司祭の基本技能をそれぞれのキャラに割り振るというもので、例えば戦士兼盗賊とかができるわけだ。となると、戦士と盗賊は兼任で残り三人は魔術師と司祭兼任という編成が考えられるので、主に戦闘のタルさをどうにかするためにこっちの編成でやってみよう。
しかしこうしてみると、エルミナージュって本当に親切なゲームだったんだなあ。
仕事で必要になったんで適当なノート PC を買ったんだが、最近のノート PC ってあれだねー、リカバリデータを HDD に入れてるんだねー。……いや、光学ドライブ付いてないマシンとかはいいとして、光学ドライブ付いてるマシンだったら普通にリカバリ CD 付けてくれよ。
おかげで Linux とのデュアルブートにするのに一苦労というか、そもそも今までデュアルブートなんぞやったことなかったんで悪戦苦闘中なんだが。俺の買ったマシンはリカバリ領域が HDD の先頭付近にあってブートローダがそこにあるくせえから、下手な事が出来ない。もしかしたら CD に grub をインストールして起動フロッピーならぬ起動 CD を作ってしまうかも知れん。
Jack はネットワーク上で MIDI イベントは流石に流せなかったか。まあ、期待半分でやっていたんで別にいいんだが。
わざわざ些細なつぶやきに反応しなくてもとか、興味を持つ人とかいたりしねーかなー程度の観測気球をご本尊に撃墜されたら元も子もないっすよとか、なんだかんだいって檜山さんあなたこの事が書きたくて書きたくて仕方がなかったんじゃないんですかとか、まあでもそのうち書かざるを得ないでしょうとか思うので、ちょっと Caty スクリプトの現在と次のリリース、近いうちに実装される機能について実装担当よりいくつか書いてみる。
まず現在の Caty スクリプトはチューリング完全ではない。というか、チューリング完全だとパワフル過ぎて検証可能性とかにえらいこと影響が出るので、大体次のようなスクリプトが書ける程度の表現力に止めることにしている。
loggedin --as admin {
"OK" => read-table | each {some-command|another-command|...} | print /result.html,
"NG" => redirect /login.html
}
ただ現状の Caty スクリプトは匿名のコード辺をその都度書いていくスタイルで、再利用性に乏しい。流石にみんなコピペコードの嵐は(たとえ2,3行のスクリプトでも)嫌でしょうというわけで、次のリリースでは Caty スクリプトをファイルに保存して呼び出せる機能を追加する予定というかもう既に一応でき上がっていて、あとは細かい調整のみだったり。
/* /scripts/foo.caty */
each {some-command|another-command|...}
/* 呼び出し元 */
loggedin --as admin {
"OK" => foo.caty,
"NG" => redirect /login.html
}
それでこの機能を最初のリリースに入れなかった理由は、普通に実装するとチューリング完全になりかねないので、そこら辺の調整が間に合わなかったというのが大きな理由。とにかく流石にそれはマズいなーと思ったので、今のところこの機能はマクロ展開みたいな形で実装している。これなら再帰的なスクリプト呼び出しを書いたら無限ループしてエラーになるので安心だ(?)。そういえば前に却下されたユーザ定義関手は、あれも下手を打つとラムダ計算されてチューリング完全になっちまうのが却下の理由の一つだった。
それで檜山さんのエントリーにある Caty スクリプトをチューリング完全にしてしまう機能については、まあ常に使えるわけではなくて特定の環境下でのみ利用可能になるという方向性になるでしょう。具体的な事はまた次の機会にということで。
とはいえ、「マゾ・テスト」でないテスト機能は次のリリースに入れてもいいかな。例えば次のようなデータを与えると、入出力のチェックの突合せを行ってレポートを出すとか。
{
"command": "cmd1|cmd2", // 任意のパイプライン
"data": [
["input data", "expected data"],
[{"x": 0, "y": 1}, null]
]
}
ちょっと試しにそういうレポートを出す機能を Caty スクリプトのみで書いてみたらビルトインにいくつか付け足すだけで書けたので、間に合う公算はあるかな。
Web 上で日々繰り替えされるプログラミング系のどーでもいい論争を見るに付け、どうも今のソフトウェア開発の実務者には理学の知識とセンスが致命的に欠落しているケースが多いのではないかと思いたくなる。計算機科学って理学の組長であるところの数学の舎弟みたいなもんだし、ソフトウェア工学がおそらくまだ未成熟であることを考えると、まともな工学的手法の探求のためにはもっと基礎の部分について学ばないとダメなんじゃないのか(当然俺もまだまだ未熟なので、あんま偉そうな事を書くのはアレだが)。
どうでもいいが、風邪ひいたと思って医者に行ったら急性胃腸炎と診断された。先月辺りにも急性胃腸炎にかかった覚えがあるんだが、一体どうなってんだ。火の通ったもんしか食ってないし、食器はちゃんと洗ってるし、本当に心当たりがないぜ。
仕事のし過ぎで体がぶっ壊れ気味っていう可能性が無きにしも非ずだが、別にそんなオーバーワークしてるわけでもないしな(というかオーバーワークがバレると怒られるんで意識してセーブしてる)。
Caty は今のところプロトタイプなので後方互換性とか完全に投げっぱなしジャーマンで開発しているのだけど、正式版になったらそうもいかない。既に檜山さんが書いていたと思うけど、後方互換性に関してはマイナーバージョンアップでは互換性を保ち、メジャーバージョンアップでは互換性を保証しないというポリシーになると思う。
ここでの問題は、そのバージョンアップ後の Caty モジュールの名前空間。今は全てのパッケージが caty をルートとした名前空間の中にあって、これらのモジュールは from caty.xxx import yyy みたいな形でインポートするわけだが、メジャーバージョンアップ後のモジュールとその前のモジュールが同じようにインポートできてしまうのはマズいよなあとも思う。互換性を保証しないっつってんだから、そもそも前のバージョンと同じようにインポートさせるのはどうよ。前のバージョン用に作ったコマンドを後のバージョンの環境にまるまるコピペして、それが単に動かないだけならいいんだけど、おかしな動作をしつつもエラーにならないとか最悪だからな。なので警告の意味でもモジュールインポート時に即エラーにするのは大事。
で、Python の標準モジュールを見ると popen や urllib あたりはモジュール名+番号で名前空間を分けているので、 caty もメジャーバージョンアップの度に名前空間を分けた方が良さそうだ。確か ANTLR も同じようにしてたはず。
ここ数日ほどウィルス性胃腸炎でくたばっていたのだが(今回は38度近くまで発熱したので激ヤバだった)、だいぶ持ち直してきたので BPStudy#26 で俺が喋る内容について予告めいたことをいくつか。
西原先生、何やってるんですかああああああああああぁぁぁぁぁぁぁぁぁ!?
「毎日かあさん」などの善人漫画(こっちも好きだが)により文化人的なポジションを手に入れたと思ったら、それを全部投げ捨てるような狂気に走るんだから西原先生からは目が離せない。まず FX 漫画をヒルズに行き損なったベンチャー起業の社長(サイバラ教信徒)とタッグを組んで描くという時点で頭のネジが何本か飛んでるが、西原先生が FX を始めたすぐ後にサブプライムのビッグウェーブ到来というのはある意味で神憑った引きの強さ(というか弱さ)。
そして単行本に収録された対談ページの特別ゲストが森永卓郎、よりによってモリタクだ。元よりサイバラ教信徒は西原先生が対談の席で一発ギャグや大放言をかまして引っ掻き回すカオスを期待しているものだが、よりによって相手側にもカオスな奴をあてがいやがった。案の定、 FX の話をしてるってのに「私の憧れはマーサ・スチュワート。彼女みたいにインサイダー取引してみたい」と西原先生が言い出せばモリタクがメイド喫茶の話を振る、いい意味で酷い対談になっている。前述のベンチャー起業の社長が途中でフェードアウト気味なのも仕方あるまい。
FX 漫画は半分ほどで終わり、残りは全く別の鳥頭日記という漫画が載っているのだが、「毎日かあさん」ぐらいしか西原先生を知らない新規ファンは覚悟すること。明らかに昔の西原先生のスタイルの漫画であり(FX の方もそうだが)、全てのページから凄絶な瘴気が放たれている。
昨日の BPStudy で使ったスライドを公開。といっても昨日やったことで価値のある部分って俺と檜山さんがアドリブで喋ったり質疑応答で答えたりした部分だと思うので、スライドだけ見ても何が何やらかもしんないけど。
というわけで、昨日の質疑応答では話がこんがらがるのであえて喋らなかったことをいくつか。
Caty テンプレートは常にプレースホルダーへの入力はエスケープされるので、もしも Wiki の本文の出力などを行うときは {$wiki|noescape} などとエスケープさせないためのフィルターの指定を行わなければならない。なので、もしもこれを忘れたら表示が崩れてしまうのでは? そういったプレゼンテーションロジックは分離できないか? といった質問が出た。あの場では説明しなかったが、実はこの部分はある手段を用いて分離可能である。
仮に {$wiki} というプレースホルダーだけを持つテンプレートに値を入れるなら、そおテンプレートを出力するコマンドは object{"wiki": string} -> string という型を持っていることになる(実際、次の次あたりのリリースで print コマンドは多相型になるのでこの通りの動作となる)。そして string 自身にはたいした情報がないのでそれが HTML エスケープされるべきか否かはテンプレートの書き手が判断するしかない。なので、ここに更なる情報を付け加えられるならエスケープ処理はテンプレート本体から分離できることになる。
昨日のデモで何度かやったが、 Caty スクリプトにはアノテーションタグの機能がある。
type person = @person object {
"name": string,
"age": integer
};
上記の型は object 型ではなく @person object 型となり、厳密に object 型とは区別される。同じようにスカラー型にもタグは付加できるので、例えば以下のような型を作ることができる。
type htmlString = @html string;
そしてテンプレートエンジン側に htmlString 型はエスケープしない、という事前の了解があるのなら、 HTML エスケープのプレゼンテーションロジックはテンプレート内部から分離できることになる。これは Terrence Parr の主張における Renderer を切り替えることで
という事が簡単に行える。
このアイディアは一見悪くなく思えるが(いや実際悪くないんだが)、そもそもタグ機能はユーザーが(組み込み型の名前とバッティングしなければ)自由に定義できる事を前提にしているので、そこにシステムから「このタグ使っちゃダメ」を入れ込むというのは理念的に問題がある。何より一つでもこのような措置を認めてしまうと、「○○のためにシステムレベルで使うタグ××を定義しましょう」という話がいくらでも出てくる恐れが十分にある。
ただし、もしも十分に議論を練った上で本当に普遍的なシステムレベルのタグセットを定義して、それに関する規約が明文化できるなら、別にこのやり方を否定するつもりはない。例えばタグ名を _ で始めるのは特殊な用途のタグ名だという事にして、
type htmlString = @_html string;
という形で記述する事にし、特殊タグは事前に定義したシステムレベルのものだけをみとめ、また特殊タグは直接書かせないで htmlString などの名前付き型で使わせる、といった運用ができるのであれば、この部分のプレゼンテーションロジックは隠蔽できる。万が一コマンドの書き手が htmlString ではなく string を出力しても、デフォルトがエスケープモードで安全側に倒されているので問題はない。また、このやり方の場合 string と htmlString は別の型なので、コマンドレベルで見ても HTML 文字列をよくわかんないまま文字列操作してぶっ壊れるなんてことはなくなる(型エラーになる)。
というわけで、このやり方は検討する価値がある。最初に思いついた時はテンプレートエンジンが Caty の拡張 JSON に依存するのが嫌だったが(単体テスト困難になる)、テスト用にはそのあたりと切り離された Renderer を DI すればいいので実はどうにかなる(当初は Renderer が分離していなかった)。ただし優先順位的に微妙な問題なので、いつ手を付けるかは不明。
実は水曜日にやったリハーサルでも「Caty を RPC サーバに」みたいな話は出てきて、確かに Caty には単なる Web アプリケーションフレームワーク以上のポテンシャルはある。が、それはあくまでも副次的な結果に過ぎないと思う。
昨日喋った通り Caty の目的はスキルの有無に関わらず真っ当な開発スタイルによる安全・安心なサイト構築の手段を提供するというもので、だから学習容易性だの単純性だのにこだわっているわけ。そしてその目的をきちんと達成するためにはそれ相応の理論基盤が必要だし、そもそも真っ当なソフトウェアの作りをすると大抵がフロントエンド+言語処理系+ドライバ・システムコールみたいな設計に近づく。多くの場合は言語処理系が抽象 API 層か DSL かなにかで代用されてるけど、ちょっと Caty の場合は事情が違うから本物の言語処理系が乗ってるってだけ。そうやって設計・構築されたソフトウェアが普遍性を持つのは当たり前で、だから Web とコンソールが対等だし GUI を被せるのも全然問題ないわけ。当然、 RPC サーバにしたって問題はない。
実は Caty ってトリッキーでも斬新でも何でもなくて、スクリプトエンジンとその他の部分の関係はシェルスクリプトとターミナルとバックにある OS のシステムコールとディスプレイドライバなどのドライバだったりするし、サイトの構造も ~/public_html, ~/cgi-bin, ~/bin みたいな構造だし、本当に昔からある技術の延長線上であって突然出てきたキメラなんかじゃない。
多分一番衝撃を与えたであろう Caty スクリプトと JSON スキーマも、ちゃんとした理論的な背景があってああなっているのであって、別に適当ぶっこいてやってるわけじゃない。その理論を理解するにはいろいろ手間入り物入りだが、まともなソフトウェアを作ろうとするのにろくすっぽ勉強しないでコードだけ書いてる方がよっぽどおかしいので、極めて真っ当な状況だともいえる。
とかいう事を考えてる暇があったら、俺は Caty スクリプトのコンパイラと VM の設計をとっととやるべきなんだが。あと型推論アルゴリズムの実装。
追記:まずユーザに勘付かれはしないだろうけど、 Caty スクリプトにはモナドあるよ(定式化の道具に使ってるって意味で)。それについては檜山さんがそのうち何か説明するでしょう。で、こういう事を書くと「Caty スクリプトってすごい言語なんじゃないの?」と思われるかもしれないけど、マジで Caty スクリプトはしょぼい機能しかないんだって。覚えることなんてろくにないし、そもそもベター JSON として使うところから始められるんだって。
追記:発表の最中に「Caty コマンド宣言で Python による実装だけでなくて Caty スクリプトによる実装もコマンドの実装手段として使おう」という檜山さんの提案を鬼反応で却下したわけだが、その理由を今から説明する。
まず Caty のコマンド宣言は以下のようになっている。
command length :: array [_T] -> integer
refers python:util.Length;
ここでは util モジュールの Length クラスを length コマンドの実装で使っているという宣言になる。これと同様に、 Caty スクリプトもコマンド宣言で使いたいというのが檜山さんの要望。
command foo :: string -> string
refers caty:foo.caty
これはダメだ。なぜなら、 foo.caty の中身がこうなっていたら無限再帰に陥るからだ。
foo
ちなみに foo.caty の中身がこうなっているのは合法。
foo.caty
何故かというと、 Caty スクリプト内部で別のスクリプトファイルを呼び出すのはコード展開の形式で実装しているので(C の #include に近い)、スクリプト呼び出しが再帰的な場合はそもそもコンパイルができなくなっている。
普通のユーザが使う場合は、スクリプトのコンパイル結果がコマンドの実装のバグを除けば常に有限の実行ステップで終了するのが Caty スクリプトの要件なので、それを叩き壊すような機能は入れるわけにはいかない。
まあ、俺もユーザ定義関手と高階関数文法を却下されてるんだがな。