関数型のテンプレートエンジンで計算

関数型のテンプレートエンジンで何が出来るか - terazzoの日記」の続編ですよ。
但し中途半端の上に、かなりお遊びで。


関数型のテンプレートエンジンの定義はこの辺りで。

  • 各テンプレートが独立した静的スコープを持つ
  • テンプレート(の一部)がファーストクラスのオブジェクトとして扱える。
    • 変数に代入したり、引数で渡せる。

今回はレギュレーションとして、さらに以下の条件を加える。

  • defineタグ(変数・引数の宣言)およびinsertタグ(適用および表示)のみ使用
  • ELは使用しない

このような機能が制限されたテンプレートエンジンでも、うっかり数値の計算が出来てしまうというデモ。
作りかけの自作のテンプレートエンジンHotplate Vers_0.3で実際に動くサンプルで説明。

数の定義

テンプレートに、中身をあらわすプレースホルダがあり、その周りを何か(<div>タグなど)で囲うようなテンプレートを考える。
例:

<div class="box">{insert value=x/}</div>

これにx=「わたしです」を適用すると、側がdivに囲われた「わたしです」が表示される。

わたしです


さらに、関数型のテンプレートエンジンでは、「側を入れ子にして表示する」とか「側のテンプレートと中身のテンプレートを引数で渡し、側をn回入れ子にして中身を表示するテンプレート」などを作ることが出来る。
例えば、側を三重にして中身を表示するテンプレートは、側を変数「f]、中身を変数「x」として以下のようになる。*1

{insert value=f}
  {define name=x}
    {insert value=f}
      {define name=x}
        {insert value=f}
          {define name=x value=x/}
        {/insert}
      {/define}
    {/insert}
  {/define}
{/insert}

これにf=「<div class="box">{insert value=x/}</div>」、x=「わたしです」を適用。

    SimpleTemplate three = translator.toTemplate("" +
            "{insert value=f}" +
                "{define name=x}" +
                    "{insert value=f}" +
                        "{define name=x}" +
                            "{insert value=f}" +
                                "{define name=x value=x/}" +
                            "{/insert}" +
                        "{/define}" +
                    "{/insert}" +
                "{/define}" +
            "{/insert}"
    );

    SimpleTemplate f = translator.toTemplate("<div class=\"box\">{insert value=x/}</div>");
    SimpleTemplate x = translator.toTemplate("わたしです");
    printNumber(three, f, x);
    public void printNumber(SimpleTemplate n, SimpleTemplate f, SimpleTemplate x) {
        SimpleTranslator translator = new SimpleTranslator();
        
        SimpleTemplate applied =
            n.apply(
                new ContextBuilder<Object, SimpleTemplate>()
                .put(Symbol.of("f"), f)
                .put(Symbol.of("x"), x)
                .context()
            ).template();

        System.out.println(translator.fromTemplate(applied));
    }

出力

<div class="box"><div class="box"><div class="box">わたしです</div></div></div>

わたしです

fを「1+{insert value=x/}」、xを「0」に変更すれば、以下のような出力になる。
出力

1+1+1+0

このようなテンプレートを数値として扱い、計算をおこなう。

ゼロおよび1を足すテンプレート

ゼロは「側」を無視して中身だけを表示する。

SimpleTemplate zero = translator.toTemplate("" +
   "{insert value=x/}"
);

任意の数値に対して、「1を足す」テンプレートは以下のようになる。

SimpleTemplate succ = translator.toTemplate("" +
    "{insert value=f}" +
        "{define name=x}" +
            "{insert value=n}" +
                "{define name=f value=f/}" +
                "{define name=x value=x/}" +
            "{/insert}" +
         "{/define}" +
    "{/insert}"
);

zeroにsuccを適用した後に、上のfとxを適用してみる。

    SimpleTemplate succ_zero =
        succ.apply(ContextUtils.newContext(
                Symbol.of("n"), zero))
        .template();
    SimpleTemplate succ_succ_zero =
        succ.apply(ContextUtils.newContext(
                Symbol.of("n"), succ_zero))
        .template();
    SimpleTemplate succ_succ_succ_zero =
        succ.apply(ContextUtils.newContext(
                Symbol.of("n"), succ_succ_zero))
        .template();
    // fとxを与えて表示
    SimpleTemplate f = translator.toTemplate("1+{insert value=x/}");
    SimpleTemplate x = translator.toTemplate("0");
    printNumber(succ_zero, f, x);
    printNumber(succ_succ_zero, f, x);
    printNumber(succ_succ_succ_zero, f, x);

出力

1+0
1+1+0
1+1+1+0

succ()するごとに1ずつ増えている。

足し算するテンプレート

m, nを数値を表すテンプレートとすると、mとnの足し算を表すテンプレートは以下のようになる。

{insert value=n}
  {define name=f value=f/}
  {define name=x}
    {insert value=m}
      {define name=f value=f/}
      {define name=x value=x/}
    {/insert}
  {/define}
{/insert}

n回fを適用した側に、中身としてm回fを適用したxを渡す。
実際に2と3を足してみる。

    SimpleTemplate plus = translator.toTemplate("" +
            "{insert value=n}" +
            "{define name=f value=f/}" +
            "{define name=x}" +
                "{insert value=m}" +
                    "{define name=f value=f/}" +
                    "{define name=x value=x/}" +
                "{/insert}" +
             "{/define}" +
            "{/insert}"
    );

    SimpleTemplate f = translator.toTemplate("1+{insert value=x/}");
    SimpleTemplate x = translator.toTemplate("0");
    SimpleTemplate applied = plus.apply(
            new ContextBuilder<Object, SimpleTemplate>()
            .put(Symbol.of("m"), two)
            .put(Symbol.of("n"), three)
            .put(Symbol.of("f"), f)
            .put(Symbol.of("x"), x)
            .context()
    ).template();
    System.out.println("two+three = " + translator.fromTemplate(applied));

出力

two+three = 1+1+1+1+1+0

5になっている。


「mとnを足した数値」自体をテンプレートとして取り出せないのは、テンプレートエンジンの実装で自由変数の扱いとか簡約とかその辺がちゃんと作れてないからで、ちゃんと実装すればできるはず。

掛け算するテンプレート

    SimpleTemplate multiply = translator.toTemplate("" +
        "{insert value=n}" +
            "{define name=f}" +
                "{insert value=m}" +
                    "{define name=f value=f/}" +
                    "{define name=x value=x/}" +
                "{/insert}" +
            "{/define}" +
            "{define name=x value=x/}" +
        "{/insert}"
    );

「m回fを適用する」ことをn回適用する。
実際に2と3を掛けてみる。

    SimpleTemplate f = translator.toTemplate("1+{insert value=x/}");
    SimpleTemplate x = translator.toTemplate("0");
    SimpleTemplate applied = multiply.apply(
            new ContextBuilder<Object, SimpleTemplate>()
            .put(Symbol.of("m"), two)
            .put(Symbol.of("n"), three)
            .put(Symbol.of("f"), f)
            .put(Symbol.of("x"), x)
            .context()
    ).template();
    System.out.println("two x three = " + translator.fromTemplate(applied));

出力

two x three = 1+1+1+1+1+1+0

6になっている。

1を引くテンプレート

    SimpleTemplate pred = translator.toTemplate("" +
        "{define name=_f value=f/}" +
        "{define name=_x value=x/}" +
        "{define name=pred_in}" +
            "{insert value=n}" +
                "{define name=f}" +
                    "{define name=g value=x/}" +
                    "{insert value=h}" +
                        "{define name=f}" +
                            "{insert value=g}" +
                                "{define name=f value=_f/}" +
                            "{/insert}" +
                        "{/define}" +
                        "{define name=x}" +
                            "{insert name=x value=x/}" +
                        "{/define}" +
                    "{/insert}" +
                "{/define}" +
                "{define name=x}" +
                    "{insert value=_x/}" +
                "{/define}" +
            "{/insert}" + 
        "{/define}" +

        "{insert value=pred_in}" +
            "{define name=h}" +
                "{insert value=f}" +
                    "{define name=x value=x/}" +
                "{/insert}" +
            "{/define}" +
        "{/insert}"
    );

うまく説明できません。このテンプレートエンジンは引数名が固定でα変換とか出来ないので中の方で参照できるように_fと_xという名前で再定義している。
5から1引いて表示。

    SimpleTemplate f = translator.toTemplate("1+{insert value=x/}");
    SimpleTemplate x = translator.toTemplate("0");
    SimpleTemplate pred_five = pred.apply(
           new ContextBuilder<Object, SimpleTemplate>()
           .put(Symbol.of("n"), five)
           .put(Symbol.of("f"), f)
           .put(Symbol.of("x"), x)
           .context()
   ).template();
   System.out.println("pred(five)= " + translator.fromTemplate(pred_five));

出力

pred(five)= 1+1+1+1+0

一応動いているようだ……


predを使って引き算を定義できるはずなんだけど、テンプレートエンジンの実装がいい加減なせいかうまく動かない。

結論?

静的スコープを持ちテンプレートを変数や引数に使えるテンプレートエンジンでは、ELやホスト言語に頼らなくても、またかなり中途半端な実装でも、テンプレート上だけで足し算や掛け算などの計算をおこなうことができる。「テンプレートエンジンから計算機能を排除したい」という要望があるなら、厳密にはこの辺りは注意が必要かも。(実質的には害はないと思うけど。)


あ、ちなみに元ネタはwikipedia:ラムダ計算の「自然数と算術」のところにあります。あんまりちゃんと理解してないけど。

*1:見やすいように改行とインデントを付けているが実際のデータにはない。