テンプレートの修飾子(モディファイア)とかフィルターとか呼ばれる機構の詳細仕様を詰めないままになってんですが、Catyスクリプトと同じモデルでいいや、と思いました。以下の記述は大ざっぱ、絵算を補ってください -- 色付き絵算のとても良い練習問題。
モデルとなる圏
Catyスクリプトと同様に、直積と直和を持ち、モナド類(モナド、コモナド、両モナド、それらの変形)をいくらでも作れる圏を背景に考えます。いくつか(任意個数だけど)の変数の値を保持する状態空間Vと、2つの例外クラスE, Fを固定します。
状態空間Vは、単一代入変数の集まりとします。精密に定式化するなら、問い合わせ余加群(readするAPI)と単一代入モノイドが作用する更新加群(updateするAPI)とかを使うけど、面倒なので、f:V×A→V×B という形の写像(背景圏の射)を使うことにします。fは、メインストリーム入出力 A→B 以外にVを読み、それを変更(要するに代入の副作用)した結果も返すとします。
Eは捕捉不可能例外のクラス(型)、Fは捕捉可能例外のクラス。例外クラスが2つある点は、通常Catyスクリプトより複雑です。
メインストリーム入出力が A→B である射は、f:V×A → E + F + V×B で表現され、適当な一般化クライスリ結合により一般化クライスリ圏を作ります。
例外ハンドリング
例外 E + F のうち、Fは捕捉可能です。例外ハンドラは、V×F → E + F + V×B という形の写像です。通常の射とまったく同じ形ですが、メインストリーム入力の型が例外型Fになっています。
例外ハンドラ h:V×F → E + F + V×B があるとき、H(f):E + F + V×B → E + F + V×B を次のように定義します。
- H(h) := (E + h + V×B);α;[∇ + F + ∇']
めんどくさそうですが、絵算で描けば、Hは簡単なラッピング変換です。記号をだいぶ略記しているので説明すると:
- E + h + V×B の、EとV×Bは idE, idV×B のことで、+ は写像の直和です。
- したがって、E + h + V×B : E + F + V×B → E + (E + F + V×B) + V×B
- αは、E + (E + F + V×B) + V×B と (E + E) + F + (V×B + V×B) の同型を与える写像です。
- ∇はEの余対角、∇'はV×Bの余対角の略記。
- したがって、∇:E + E → E, ∇' : V×B + V×B → V×B
fがクライスリ圏の射、gがもとの圏の射のとき、fを元の圏に戻して考えた射とgが結合可能なとき、その結合を {f}g と書きましょう。f:V×A → E + F + V×B であり、h:V×F → E + F + V×B のとき、fとH(h)に関して {f}H(h) が作れます。([追記]← 少しウソが入っている、最後の追記を参照[/追記])
- {f}H(h) : V×A → E + F + V×B
上記のごときプロファイルを持つ f, h に関して、{f}H(h) を例外処理コードと考えます。fがtryスコープで実行され、捕捉可能例外はすべてhに流されて、hの結果は再びメインスリトームに戻されます。端的に言えば、fが失敗したときhが代行してパイプラインを修復します。
事例
図式順結合の構文はパイプ記号「|」を使います。
- 変数集合Vの利用例: {$article|noescape|nl2br}
noescapeは、データ処理は何もしないので、メインストリームでは恒等射です。しかし、変数の集合V内のエスケープ処理を「する/しない」のフラグを「しない」にセットします。パイプラインの最後で暗黙に起動されるレンダラーがこのフラグを参照します。
- 例外処理の利用例: {$greeting||defautl:"hello"|toupper}
例外ハンドリングのところは便宜上「||」で示しました。実際の構文では同じ「|」で代用すると思います。パラメータはコロン「:」で区切ります。
変数参照 $greeting が Undefined例外(捕捉可能)を投げた*1ときは、例外ハンドラdefaultで処理され、パイプラインは継続します。$greetingが正常値を出力すれば、defaultはスキップされます。例外処理のtryスコープが明示されてませんが、「||」より左側が自動的にtryスコープになると思ってください。
恩恵
Smartyの修飾子とDjangoテンプレートのフィルターは、これで説明が付くでしょう。変数の読み書きと例外のthrow/catchができるので、ある程度のことはできます。Catyスクリプトの場合と同様に、型宣言が事前にあるなら、パイプラインの型安全性のチェックがだいたい静的にできます。フィルタープラグインの仕様も、このモデルに基づけばハッキリと記述できます。
[追記]
実際に絵算してみたら、結果は同じなんですが、途中は違ってました。fの実行の前に、Vをコピーしておかないと、fがVを壊してしまうかも知れません。で、Vを事前にコピーして、fが成功したときはコピーを捨てることにします。直積を右にまとめるため、V×A じゃなくて A×V を使います。また、# は足し算ですが、# の左は例外データ型、右は正常データ型とします。
上から下に処理や解釈が流れるとして、だいたい次のよう。
A×V (入り口) ---------------------- A×ΔV (Vをコピーする) ---------------------- f×V (fの実行;バックアップしたVはそのまま) ---------------------- (E + F # B×V)×V (fの実行結果) ---------------------- (E + 0 # F + B×V)×V (例外を捕捉した) ---------------------- E + 0 # (F + B×V)×V (分合律で掛け算を展開した) ---------------------- E + 0 # F×V + B×V×V (分配律で掛け算を展開した) ---------------------- E + 0 # F×V + B×V (右端のVを捨てた;fの成功ケース) ---------------------- E + 0 # h + B×V (例外ハンドラーhの実行;fの失敗ケース) ---------------------- E + 0 + (E + F # B×V) + B×V (hの実行結果) ------------------------------- (E + E) + F # (B×V) + (B×V) (まとめ直すとこうなる) ------------------------------- ∇ + F # ∇' (余対角の実行;場合分けの合流) ---------------------- E + F # B×V (出口)
実際には、事前にVをコピー(バックアップ)しなくても、「失敗のときでもVを壊さないでね」とかの規約とモラルでなんとかなるような気がします。
[/追記]
*1:実装言語の例外が重いときは、ほんとの例外の代わりにアプリケーションレベルの規約を用いたほうがいいでしょう。