構造で意味が成立するテンプレートを考える
はじめに
現代のフロントエンド開発では、「テンプレート」というものが非常に多機能かつ構文的に複雑になっています。if文、for文、ディレクティブ、バインディング、スコープ束縛──テンプレートはまるでもう一つのプログラミング言語のようです。
でも本来、テンプレートとは「構造を記述するもの」ではなかったでしょうか?
この発想から出発して、テンプレートに必要な最低限の構文だけを残し、構造だけで意味が成立するテンプレートの在り方を再定義してみたいと思います。
構造が意味を持つとは?
以下のような構文を見たとき:
{{ for:list }}
{{ list.*.value }}
{{ endfor: }}
あなたはこのテンプレートが「list の各要素の value を繰り返し表示する」と直感的に理解できるはずです。
ポイントは list.*.value
という構造的なパスです。
-
list
は配列 -
*
は「全要素」 -
value
は各要素のプロパティ
この一行で、「構造」「意味」「処理」がすでに語られているのです。
パス記法とワイルドカード
このテンプレートでは、構造をパスとして表現します。
list.*.value
というパスは「list 配列の各要素の value プロパティ」を示します。
この *
(アスタリスク)は単なる記号ではなく、「構造の中の繰り返し要素」を意味するセレクタなのです。これにより、構文的な for 文やスコープ束縛が不要になります。
暗黙のスコープ束縛($1, $2)
構造的セレクタ *
の出現位置に応じて、自動的に $1
, $2
, $3
… のようなスコープ変数が束縛されます。
たとえば:
list.*.names.*.value
というパスであれば:
-
$1
= list の要素のインデックス -
$2
= names の要素のインデックス
このように構造に応じて意味のある変数が自動的に与えられることで、手続き的な束縛を行う必要がありません。
getterによる構造的意味の定義
たとえば:
get "list.*.doubleValue"() {
return this["list.*.value"] * 2;
}
この1行で、list の各要素に doubleValue プロパティを付加し、その値は value の2倍になることを意味します。
getter の構造的セレクタが「このロジックを list の各要素に適用する」と宣言しているのです。
演算対象としての構造
構造パスは、セレクタであるだけでなく、そのまま演算対象になるという特徴があります。
return this["list.*.value"] * 2;
このように、テンプレート構造で指定した値がそのまま getter 内で演算に使える。構造と意味がシームレスに接続されている状態です。
条件式や選択状態も構造で書ける
get "list.*.selected"() {
return this.$1 === this.selectedIndex;
}
「selectedIndex で示されている要素が選択されているか?」という状態も、構造だけで表現できます。
APIではなく構造を書くという転換
getter を書いていくという行為は、構造を整える作業に近くなります。
たとえば:
get "regions.*.population"() {
return this.$getAll("regions.*.prefectures.*.population")
.reduce((a, b) => a + b, 0);
}
これは構造的に「各都道府県の人口を合計して地域人口とする」ことを定義しているだけです。構文的には極めて簡単ですが、意味のある構造として機能します。
抽象的に定義し、具体的に評価される
構造駆動テンプレートのgetterは、
- 抽象的な構造パス(list.*.value)で定義され
- 呼び出されるときに具体的なスコープ($1=2 など)で評価される
この「定義と評価の分離」が、宣言的で柔軟なテンプレートを可能にしています。
最後に:構造で語るという発想
構造駆動テンプレートは、構文ではなく構造でテンプレートを記述するというパラダイムです。
- list.*.value → 構造的ループ
- list.*.selected → 条件の意味づけ
- regions.*.population → 集計の定義
構造だけでここまでの意味を表現できるのなら、テンプレートに構文はもういらないかもしれません。
この構文・思想にピンときた方は、ぜひフィードバックをお願いします。実装の公開前に、思想の手応えを確かめるための先行公開です。
Discussion