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

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

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

参照用 記事

厳密分離指向テンプレートエンジン:条件分岐

業務連絡を公開ブログでやるのもどうかな…… やっていることがダダ漏れになるのは何にも気にしないのだけど、ブログとしては雑音かな、という点でちょっと躊躇するのよね。けど、まーいいや。できるだけ解説付きで書いている(つもりだ)し。

Kuwataさんのエントリーの議論は、テンプレート処理用VMのインストラクションセットをどうすべぇ? といった観点ですけど、ここでは、テンプレートの利用者/利用場面の観点から条件分岐について考えてみます。

厳密分離原理のほころび

テレンス・パーの厳密分離の原理(http://www.cs.usfca.edu/~parrt/papers/mvc.templates.pdf)はホントに「厳密」で、テンプレート内での演算はすべて禁止しています。算術演算、論理演算はもとより比較演算(等号や不等号)もダメです。

StringTemplate (http://www.stringtemplate.org/) における {if $cond} の意味は {if defined($cond)} のことであり、論理的な真偽ではなくて変数(プレイスホルダ)値の存在/非存在のチェックです。テレンス・パーはまた、「テンプレート作成者は変数の型を知るべきでない」とも主張していて、変数がbooleanだとかintegerだとかの情報はテンプレート作成者には伝わりません。テンプレート上では、真偽値という概念はないのです。

実は、テレンス・パーのこの主張は、一部破綻したりゆらいじゃったりしています。


{if $loggedIn}
いらっしゃい、{$userName}さん。
{else}
いらっしゃい、ゲストさん。
{/if}

この例で、変数$loggedInがboolean型で、プログラム側でfalseとセットすると、{if defined($loggedIn)} の意味なので、条件は真と判断されて「いらっしゃい、{$userName}さん。」のほうが表示されます。

いくらなんでもこれは混乱の原因なので、変数がbooleanのときに限って、「falseと未定義を同一視する」としています。しかし、この方向を進めると、「0, "", [], {} などと未定義を同一視する」となるでしょう -- それが悪いとは言いませんが、型に関する解釈に一貫性が欠ける印象はあります。

僕自身は、型に関する情報をテンプレート作成者とプログラム作成者が共有するのがマズイとは思っていません。むしろ積極的に、型に関する共通認識を持つべきと思っています。つまり、変数セットに関して、名前とインフォーマルな意味だけではなく、フォーマルな型に関しても、テンプレート作成者とプログラム作成者が合意すべきだ、ということです。

論理演算に関して、StringTemplateでも not だけは入れています。原理的には、if条件にnotがなくても困りません。{if not $cond}...A...{else}...B...{/if} は {if $cond}...B...{else}...A...{/if} と書けばいいからです。しかし、使い勝手がだいぶ悪くなるので入れたのでしょう。

比較演算とswitch/case

以上から分かることは、「テンプレート側では型情報を一切使わない」とか「使い勝手は考慮しない」という方針を維持するのは困難だということです。もちろん、無節操に機能や構文を追加すると厳密分離のメリットがなくなっちゃうので注意が必要ですが、プレゼンテーション構築に必要な能力はテンプレートに持たせなきゃならんのです。

さて、ifの条件判断としては次のものが必要そうです。

  1. 真偽値
  2. is_defined(式)
  3. is_null(式)
  4. is_empty(文字列または配列)
  5. is_zero(整数)
  6. equals(式, 定数)
  7. これらの否定条件

このなかで一番問題なのは equals (等号)です。定数との比較に限定してもなお強力な演算なので、できれば禁止したいところです。

現状では、if条件内で比較を許さなくても、switch/caseの形で(潜在的な)比較を利用できれば十分かと考えています。僕が設計に関わって、既に5,6年運用しているテンプレートエンジンがあるのですが、これでは switch/case が非常に多用されています。(ちなみに、{switch $cond}{case true}...{/case}{default}...{/default}{/switch} で代用できるのでifはありません。)経験的に、switch/case がないのは辛すぎるかと。

{switch 式} と書くときの式は、任意の型を持つ必要はなくて、列挙型に限定してもそれほど不便はなく、この制限が安全なテンプレートを書く助けになると思います。

まとめると:

  1. if条件内での明白な比較(等号)の必要性はハッキリしない。なくてもいいかもしれない。
  2. switch/case、または同等な構文は、実用上は必須と思える。
  3. switchで評価する式の値は列挙型に限ってもよい。

もう一度、なにをしたいのか?

SmartyもGenshiも、テンプレート内にプログラミング言語(それぞれPHPPython)のコードを書けてしまいます。これの何が問題かといえば; まず作業の分離ができなくなってしまいます。それと、コード混じりテンプレートでは、安全性の保証がほぼ不可能です。

ここで言う安全性とは、「正しいコンテキストデータが与えられれば、正しい最終出力を得られる」ことを、あるレベルで根拠を持って主張できることです。人間は勘違いをするし、ミスをやらかします。ですから、勘違いやミスをソフトウェアにより検出できることは意義があるでしょう。

コンテキストデータがある関数により出力されるとして、その出力の型が宣言されているとします。その型宣言(スキーマ)を参照すれば、テンプレート内に出現する式の型安全性を保証するのは比較的容易です(「JSONデータにアクセスするパス式の正しさについて」参照)。

さらに、テンプレートの制御構造を制限すると、正規表現と対応付けることができます。

制御構造 正規表現メタ文字
elseなしのif 省略可能 ?
switch/case 選択肢 |
foreach 繰り返し *

正規表現で書けるということは、言語クラスとして正規言語の範囲内ですから、取り扱いや解析は容易です。つまり:

  1. 式言語を制限すれば、型安全性を保証できる。
  2. 制御構造を制限すれば、正規言語のクラス内に入ることを保証できる。

以上のことが、机上の空論や単なる屁理屈に終わらないためには、テンプレートとしての実用性やパフォーマンスが問題になってきます。条件分岐や等号の扱いは、理屈と実用性の境界に位置する問題なのです。