関数型のテンプレートエンジンを作ってみる(0.2.3) - 実行系を書く
前回の続きで、テンプレートエンジンのエンジン部分を書く。
今回実装したところまでをVers_0.2というブランチにしてみた。
方針
では順番に実装していく。
インタフェースの変更
0.1で作ったコアのインタフェース群を少し修正する。
Templateインタフェースのapply()は1.0ではTemplateを返していた。今回は「define」タグがコンテキストに影響を与える仕組みを実現するため、(Template, Context)を戻すようにする。(Javaはタプル無いのでTemplatePairというクラスを作った。)
意味としては、「自分自身にcontextを適用した結果(「旧」の戻り値と同じ)」と「contextに対する変化分」を戻す。contextに対する追加が無ければ空のContextを戻す。
旧:
public interface Template<V, T extends Template<V, T>> { T apply(Context<V, T> context); boolean isReducible(); }
↓↓↓
新:
public interface Template<V, T extends Template<V, T>> { TemplatePair<V, T> apply(Context<V, T> context); boolean isReducible(); }
public class TemplatePair<V, T extends Template<V, T>> { private final T template; private final Context<V, T> context; private TemplatePair(T template, Context<V, T> context) { this.template = template; this.context = context; } public T template() { return template; } public Context<V, T> context() { return context; } // template, contextからなるTemplatePairを戻す。 public static <V, T extends Template<V, T>> TemplatePair<V, T> pairOf(T template, Context<V, T> context) { return new TemplatePair<V, T>(template, context); } // templateと空のContextからなるTemplatePairを戻す。 public static <V, T extends Template<V, T>> TemplatePair<V, T> pairOf(T template) { return pairOf(template, ContextUtils.<V, T>emptyContext()); } }
それ以外の基本インタフェースは変更ないけど、ContextはUtilクラスを作っておく。
Contextの合成や空のContextを作りやすくしておく。
public final class ContextUtils { private ContextUtils() { } // symbolに対してvalueを戻すContextを生成する。 public static <V, T extends Template<V, T>> Context<V, T> newContext(final Symbol symbol, final T value) { return new Context<V, T>() { public T get(Symbol s) { return symbol.equals(s) ? value : null; } }; } // symbolに対してvalueを戻し、それ以外にはcontextの戻り値を戻すContextを生成する。 public static <V, T extends Template<V, T>> Context<V, T> put(final Context<V, T> context, final Symbol symbol, final T value) { return merge(newContext(symbol, value), context); } // 二つのContextを合成する。firstを探し、値が無ければnextを探す。 public static <V, T extends Template<V, T>> Context<V, T> merge(final Context<V, T> first, final Context<V, T> next) { return new Context<V, T>() { public T get(Symbol s) { T value = first.get(s); if (value != null) { return value; } return next.get(s); } }; } @SuppressWarnings("rawtypes") private static Context EMPTY_CONTEXT = new Context() { public Template get(Symbol name) { return null; } }; // 空のContextを戻す。 @SuppressWarnings("unchecked") public static <V, T extends Template<V, T>> Context<V, T> emptyContext() { return EMPTY_CONTEXT; } }
テンプレートクラス(既存)の変更
apply()の戻り値の型が変わっているので、SimpleContainerとSimpleLiteralの実装も変更する必要がある。
いずれもapply()の中身だけの変更。
SimpleLiteralは変数を持たないためapply()しても変化しないし、Contextにも影響を与えないので自分自身と空のContextのペアを戻す。
public class SimpleLiteral implements SimpleTemplate { ... // 自分自身と空のContextを戻す。 public TemplatePair<Object, SimpleTemplate> apply(Context<Object, SimpleTemplate> context) { return TemplatePair.<Object, SimpleTemplate>pairOf(this); } ... }
SimpleContainerは各要素をapply()した結果を蓄積して戻す。
public class SimpleContainer implements SimpleTemplate { private final List<SimpleTemplate> elements; ... @Override public TemplatePair<Object, SimpleTemplate> apply(Context<Object, SimpleTemplate> context) { if (!isReducible()) { return TemplatePair.<Object, SimpleTemplate>pairOf(this); } // 評価後の子要素のTemplateを保持する List<SimpleTemplate> newElements = new ArrayList<SimpleTemplate>(); // contextへの変更を保持する Context<Object, SimpleTemplate> newContext = ContextUtils.emptyContext(); for (SimpleTemplate element : elements) { // contextの差分と引数のcontextをマージする。 Context<Object, SimpleTemplate> totalContext = ContextUtils.merge(newContext, context); // 各要素にapply TemplatePair<Object, SimpleTemplate> applied = element.apply(totalContext); // 評価結果のTemplateを追加 newElements.add(applied.template()); // 評価結果のContextの差分を追加 newContext = ContextUtils.merge(applied.context(), newContext); } // 評価済みの子要素を保持するSimpleContainerと、Contextの差分を足し込んだ結果を戻す。 return TemplatePair.pairOf(new SimpleContainer(newElements), newContext); } ... }
こうすることで、前でdefineした変数をその後ろで参照できるようになるはず。
前回はプレースホルダ(シンボル名を「{」「}」で囲ったもの)をSimpleReferenceという名前のテンプレートとして実現していたが、今回はinsert/defineというタグを実装するので、SimpleReferenceクラスは廃止して、各タグを「○○プロセッサ(XXProcessor)」という名前で実装する。
defineタグの実装
defineには3種類の書式がある。name常にシンボルだが、valueは即値とシンボルと、valueを書かずにコンテナに内容を記述する方式がある。
// 即値 {define name=yourName value="terazzo" /} // シンボル(参照) {define name=yourName value=parameterName /} // コンテナに記述 {define name=helloWold} Hello, {insert value=yourName /} ! {/define}
3種類の値を持ってif文で処理を切り分けるのは嫌なのでインタフェースを切って多態で対処する。(実装クラスは後ほど。)
public interface SimpleSource { SimpleTemplate getTemplate(Context<Object, SimpleTemplate> context); }
defineタグは、評価すると、コンテキストに対して「name属性の値(シンボル)」という名前で「value属性の値」という影響を与えるようにしたい。
また、defineタグ自体は表示しないので、一旦評価すると何か無害なTemplateに変化させたい。
public class SimpleDefineProcessor implements SimpleTemplate { // name属性で指定したSymbol private final Symbol symbol; // value属性またはコンテナの中身で定義した値 private final SimpleSource source; ... @Override public TemplatePair<Object, SimpleTemplate> apply(final Context<Object, SimpleTemplate> context) { // 値を取り出す SimpleTemplate definition = source.getTemplate(context); if (definition == null) { // 定義が見つからない場合は自分自身を戻す=未評価 return TemplatePair.<Object, SimpleTemplate>pairOf(this); } // 「(symbolの値)に対して(definitionの値)を戻す」コンテキストを生成 Context<Object, SimpleTemplate> newContext = ContextUtils.newContext(symbol, definition); // 「無害なTemplate」と差分のコンテキストを戻す return TemplatePair.pairOf(new SimpleNop(), newContext); } ... }
「無害なTemplate」はapplyしてもテンプレートにもコンテキストにも影響を与えないTemplateであるSimpleNopとして実装する。また、SimpleNopは最後にgetString()で文字列化する時に空文字列を戻すようにする。*1
public class SimpleNop implements SimpleTemplate { @Override public TemplatePair<Object, SimpleTemplate> apply(Context<Object, SimpleTemplate> context) { return TemplatePair.<Object, SimpleTemplate>pairOf(this); } @Override public String getString() { return ""; } @Override public boolean isReducible() { return false; } }
SimpleSourceは、それぞれのパターンに合わせて3種類の実装を持たせる。
まず即値の場合(value="hoge")。getTemplate()するとその文字列を表すSimpleLiteralになる。
public class SimpleExpression implements SimpleSource { private final String expression; public SimpleExpression(String expression) { this.text = expression; } @Override public SimpleTemplate getTemplate(Context<Object, SimpleTemplate> context) { return new SimpleLiteral(expression); } }
シンボルの場合(value=hoge)。getTemplate()するとContextからそのシンボルで値を取り戻して戻す。
public class SimpleReference implements SimpleSource { private final Symbol symbol; public SimpleReference(Symbol symbol) { this.symbol = symbol; } @Override public SimpleTemplate getTemplate(Context<Object, SimpleTemplate> context) { return context.get(symbol); } }
コンテナの内容として指定した場合、getTemplate()するとそのコンテナを表すSimpleContainerになる。
SimpleTemplatePrototypeはパーサの時に出て来た、変数をバインドする前の式にあたるもの。
SimpleContainerPrototype自身もSimpleTemplatePrototypeとして実装する。
public class SimpleContainerPrototype implements SimpleTemplatePrototype, SimpleSource { private final List<SimpleTemplatePrototype> elements; public SimpleContainerPrototype(List<SimpleTemplatePrototype> elements) { super(); this.elements = elements; } @Override public SimpleTemplate instantiate(Context<Object, SimpleTemplate> lexicalContext) { List<SimpleTemplate> tempalteElements = new ArrayList<SimpleTemplate>(); for (SimpleTemplatePrototype source : elements) { tempalteElements.add(source.instantiate(lexicalContext)); } return new SimpleContainer(tempalteElements); } @Override public SimpleTemplate getTemplate(Context<Object, SimpleTemplate> context) { return instantiate(context); } }
さて、パース時はSimpleDefineProcessorではなく変数をバインドするまえの形であるSimpleDefineProcessorPrototypeするようにしていた。
そこで、SimpleDefineProcessorを生成できるようにSimpleDefineProcessorPrototypeを実装する。
public class SimpleDefineProcessorPrototype implements SimpleTemplatePrototype { private Symbol symbol; private SimpleSource source; public SimpleDefineProcessorPrototype(Symbol symbol, SimpleSource source) { super(); this.symbol = symbol; this.source = source; } public SimpleTemplate instantiate(Context<Object, SimpleTemplate> lexicalContext) { return new SimpleDefineProcessor(lexicalContext, symbol, source); } }
先ほどのSimpleDefineProcessorはコンストラクタ等が実装されていなかったので書き直す。
また、instantiate時に受け取ったContext(=構文的に静的に決定されるContext)を保持して評価時に利用するようにする。クロージャ的なイメージ。
public class SimpleDefineProcessor implements SimpleTemplate { /** 初期化時のContext */ private final Context<Object, SimpleTemplate> lexicalContext; /** name属性で指定したSymbol */ private final Symbol symbol; /** value属性またはコンテナの中身で定義した値 */ private final SimpleSource source; public SimpleDefineProcessor( Context<Object, SimpleTemplate> lexicalContext, Symbol symbol, SimpleSource source) { super(); this.lexicalContext = lexicalContext; this.symbol = symbol; this.source = source; } @Override public TemplatePair<Object, SimpleTemplate> apply(final Context<Object, SimpleTemplate> context) { // 初期化時のlexicalContext + 引数のcontextを使用する(引数の方を優先) Context<Object, SimpleTemplate> totalContext = ContextUtils.merge(context, lexicalContext); // 値を取り出す SimpleTemplate definition = source.getTemplate(totalContext); if (definition == null) { // 定義が見つからない場合は自分自身を戻す=未評価 return TemplatePair.<Object, SimpleTemplate>pairOf(this); } // 「(symbolの値)に対して(definitionの値)を戻す」コンテキストを生成 Context<Object, SimpleTemplate> newContext = ContextUtils.newContext(symbol, definition); // 「無害なTemplate」と差分のコンテキストを戻す return TemplatePair.pairOf(new SimpleNop(), newContext); } @Override public boolean isReducible() { return true; // 評価されると変化するのでtrue } @Override public String getString() { throw new IllegalStateException("Unevaluated define:" + source); // 残っているとNG } }
insertタグの実装
insertには2種類の書式がある。valueは即値とシンボルがある。即値の方はナンセンスな気もするけど。
こんにちは、{insert value="terazzo"/}様。 こんにちは、{insert value=yourName/}様。
また、シンボルの場合、参照先のテンプレートを評価する時に渡すContextをコンテナ内のdefineタグで指定できる。
{insert value=helloWold} {define name=yourName value="terazzo" /} {/insert}
では実装。
insertはコンテナを評価して(つまり中に含まれるdefineを評価して)得たContextをvalueで参照されるテンプレートに渡して評価し、評価結果を自分自身のapplyの結果として戻す。
但し、評価結果が既約でない場合はinsertタグを維持する。
public class SimpleInsertProcessor implements SimpleTemplate { /** 初期化時のContext */ private final Context<Object, SimpleTemplate> lexicalContext; /** value属性で指定した値 */ private final SimpleSource source; /** コンテナの中身を保持するSimpleTemplate。コンテナは引数を作るのに使用する。 */ private final SimpleTemplate content; public SimpleInsertProcessor( Context<Object, SimpleTemplate>lexicalContext, SimpleSource source, SimpleTemplate content) { this.lexicalContext = lexicalContext; this.source = source; this.content = content; } @Override public TemplatePair<Object, SimpleTemplate> apply(final Context<Object, SimpleTemplate> context) { Context<Object, SimpleTemplate> totalContext = ContextUtils.merge(context, lexicalContext); // value値を取り出す SimpleTemplate template = this.source.getTemplate(totalContext); // 空なら、未評価。但しcontextは覚えておく if (template == null) { return TemplatePair.<Object, SimpleTemplate>pairOf( new SimpleInsertProcessor(totalContext, source, content)); } // contentを評価すると、中に含まれるdefineがcontextを戻す。 TemplatePair<Object, SimpleTemplate> appliedContent = content.apply(totalContext); Context<Object, SimpleTemplate> argumentContext = appliedContent.context(); // insert先のテンプレートを引数のコンテキストで評価 TemplatePair<Object, SimpleTemplate> result = template.apply(argumentContext); if (!result.template().isReducible()) { return TemplatePair.pairOf(result.template()); } // insert先のテンプレートが既約でない場合、insertタグを維持。 return TemplatePair.<Object, SimpleTemplate>pairOf( new SimpleInsertProcessor( totalContext, new SimpleWrapper(result.template()), appliedContent.template())); } @Override public boolean isReducible() { return true; // 評価されると変化するのでtrue } @Override public String getString() { throw new IllegalStateException("Unevaluated define:" + source); // 残っているとNG } }
defineタグで参照されるテンプレートの評価時には、テンプレートがinstantiateされたときのContextと、insertが渡すContextのみが使われる。insertタグ自体の評価時のコンテキストが渡されるわけではない。つまりある種のスコープゲートとして機能する。
取り出してしまったvalueの内容を再利用する為に、SimpleWrapperというSimpleSource実装クラスを用意している。
public class SimpleWrapper implements SimpleSource { private final SimpleTemplate content; public SimpleWrapper(SimpleTemplate content) { this.content = content; } @Override public SimpleTemplate getTemplate(Context<Object, SimpleTemplate> context) { return content; } }
評価結果が既約でない場合、つまり変数を含むテンプレートの場合、それをそのままinsertの評価結果として戻すわけにはいかない。なぜならinsertの評価結果は、次回評価されるときはinsertがあった場合と同じContextで評価されるため、insert先のテンプレートをそれで評価すると静的スコープにならない。そこで、再度insertタグを戻す事でスコープゲートを維持しないといけない。
最後に、パース時に生成したSimpleInsertProcessorPrototypeからSimpleInsertProcessorを生成できるようにSimpleInsertProcessorPrototypeを実装する。
public class SimpleInsertProcessorPrototype implements SimpleTemplatePrototype { private final SimpleSource source; private final SimpleTemplatePrototype contentPrototype; public SimpleInsertProcessorPrototype(SimpleSource source, SimpleTemplatePrototype contentPrototype) { super(); this.source = source; this.contentPrototype = contentPrototype; } public SimpleTemplate instantiate( Context<Object, SimpleTemplate> lexicalContext) { return new SimpleInsertProcessor( lexicalContext, source, contentPrototype.instantiate(lexicalContext)); } }
自身のinstantiate()時にコンテナを同じ静的コンテキストを持つテンプレートしてinstantiate()している。
apply時にやるべきなのかどうかちょっと分からないけど、基本的に動作に変わりはない。
使用例
トン吉サンプルを新しいフォーマットで書くとこうなる。
@Test public void testTonkichi() { SimpleTranslator translator = new SimpleTranslator(); SimpleTemplate template = translator.toTemplate("" + "{insert value=aisatsu}" + "{define name=okyakusamamei value=customerName /}" + "{/insert}" + "{insert value=raitenonrei}" + "{define name=raitenbi value=visitDay /}" + "{/insert}" ); Context<Object, SimpleTemplate> context1 = new ContextBuilder<Object, SimpleTemplate>() .put(Symbol.of("aisatsu"), translator.toTemplate("こんにちは、{insert value=okyakusamamei/}様。")) .put(Symbol.of("raitenonrei"), translator.toTemplate("{insert value=raitenbi/}にはご来店いただき、\n" + "まことにありがとうございます。")) .context(); Context<Object, SimpleTemplate> context2 = new ContextBuilder<Object, SimpleTemplate>() .put(Symbol.of("customerName"), translator.toTemplate("板東トン吉")) .put(Symbol.of("visitDay"), translator.toTemplate("1月21日")) .context(); SimpleTemplate applied = template.apply(context1).template(); SimpleTemplate applied2 = applied.apply(context2).template(); String result = translator.fromTemplate(applied2); assertEquals("こんにちは、板東トン吉様。1月21日にはご来店いただき、\n" + "まことにありがとうございます。", result); }
一つ目のapply()でinsertの先のaisatsu、raitenonreiが評価されるが、customerNameとvisitDayが未定義であり、つまりaisatsu評価時のokyakusamameiやraitenonrei評価時のraitenbiが未定義であるため、insertは保持されたままになる。
二つ目のapply()でcustomerName:"板東トン吉"とvisitDay:"1月21日"を渡すと、それぞれaisatsuにokyakusamamei、raitenonreiにraitenbiが渡され、評価の結果既約のテンプレート(SimpleLiteralなど)となり、文字列として出力できるようになる。
insertがスコープゲートして機能しているかを見てみる。
テンプレート1をhelloWoldという名前で定義
Hello, {insert value=yourName /}!
テンプレート2(main)で、helloWoldをinsertする。
{insert value=helloWold} {define name=yourName value=inputName /} {/insert}
テンプレート2評価時にinputNameを与えてやると、表示される。
SimpleTranslator translator = new SimpleTranslator(); SimpleTemplate helloWorld = translator.toTemplate( "Hello, {insert value=yourName /}!" ); SimpleTemplate main = translator.toTemplate( "{insert value=helloWold}" + "{define name=yourName value=inputName /}" + "{/insert}" ); Context<Object, SimpleTemplate> context = new ContextBuilder<Object, SimpleTemplate>() .put(Symbol.of("helloWold"), helloWorld) .put(Symbol.of("inputName"), translator.toTemplate("板東トン吉")) .context(); String output = translator.fromTemplate(main.apply(context).template()); System.out.println("output = " + output);
↓↓↓
output = Hello, 板東トン吉!
テンプレート2評価時にinputNameを与えずにyourNameを与えてやると、エラーになる。
SimpleTranslator translator = new SimpleTranslator(); SimpleTemplate helloWorld = translator.toTemplate( "Hello, {insert value=yourName /}!" ); SimpleTemplate main = translator.toTemplate( "{insert value=helloWold}" + "{define name=yourName value=inputName /}" + "{/insert}" ); Context<Object, SimpleTemplate> context = new ContextBuilder<Object, SimpleTemplate>() .put(Symbol.of("helloWold"), helloWorld) .put(Symbol.of("yourname"), translator.toTemplate("板東トン吉")) // insert先の変数名で追加 .context(); String output = translator.fromTemplate(main.apply(context).template()); System.out.println("output = " + output);
↓↓↓
Unevaluated insert:sample.hotplate.sample.processor.SimpleInsertProcessor@65b60280
テンプレートを引数として渡すサンプル。
テンプレート内で定義したテンプレートを、インサート先に渡してみる。
インサートされるテンプレート: 名前表示部分を可変とする。
Hello, {insert value=namePart /}!
インサートする側: namePartをdefineして渡す。
{insert value=helloWold} {define name=namePart} {insert value=familyName/} {insert value=firstName/} {/define} {/insert}"
familyName, firstNameはインサートする側で束縛される。
SimpleTranslator translator = new SimpleTranslator(); SimpleTemplate helloWorld = translator.toTemplate( "{define name=firstName value=\"花子\" /}" + "Hello, {insert value=namePart /}!" ); SimpleTemplate main = translator.toTemplate( "{define name=japaneseStyleNamePart}" + "{insert value=familyName/} {insert value=firstName/}" + "{/define}" + "{insert value=helloWold}" + "{define name=namePart value=japaneseStyleNamePart /}" + "{/insert}" ); Context<Object, SimpleTemplate> context = new ContextBuilder<Object, SimpleTemplate>() .put(Symbol.of("familyName"), translator.toTemplate("板東")) .put(Symbol.of("firstName"), translator.toTemplate("トン吉")) .put(Symbol.of("helloWold"), helloWorld) .context(); SimpleTemplate applied = main.apply(context).template(); String output = translator.fromTemplate(applied); System.out.println("output = " + output);
↓↓↓
output = Hello, 板東 トン吉!
インサートする側は以下のように間接的に書く事も出来る。
{define name=japaneseStyleNamePart} {insert value=familyName/} {insert value=firstName/} {/define} {insert value=helloWold} {define name=namePart value=japaneseStyleNamePart /} {/insert}"
渡したテンプレートに変数を残しておいて、includeされた側で評価時に決定しても良い。
インクルードされる側:
{insert value=message} {define name=companyName value="山田商会" /} {/insert} --- Copyright(c) 山田商会 All Rights Reserved.
インクルードする側:
{insert value=layout} {define name=message} こんにちは、{insert value=companyName /}のホームページにようこそ! {/define} {/insert}
実行結果
SimpleTranslator translator = new SimpleTranslator(); SimpleTemplate yamadaStyle = translator.toTemplate( "{insert value=message}\n" + "{define name=companyName value=\"山田商会\" /}\n" + "{/insert}\n" + "---\n" + "Copyright(c) 山田商会 All Rights Reserved.\n" ); SimpleTemplate main = translator.toTemplate( "{insert value=layout}\n" + "{define name=message}\n" + "こんにちは、{insert value=companyName /}のホームページにようこそ!" + "{/define}\n" + "{/insert}\n" ); Context<Object, SimpleTemplate> context = new ContextBuilder<Object, SimpleTemplate>() .put(Symbol.of("layout"), yamadaStyle) .context(); String output = translator.fromTemplate(main.apply(context).template()); System.out.println("output = " + output);
↓↓↓
output = こんにちは、山田商会のホームページにようこそ! --- Copyright(c) 山田商会 All Rights Reserved.
ちょっと例が不自然だけど……。
感想とか課題
defineとinsertがわりと対称な関係にあるように思える。(define:contextに影響を及ぼす/insert:templateに影響を及ぼす。)
もう少し実装を詰めればコード的にもその関係が現れそうな気がするので少し考えてみたい。
ASTと評価器に分離するのはそのうちやりたい。
最後に文字列化するところも同様にtreeをtraverseすることで書けるようになるはず。
言語実装経験とか無いのでそういう普通の事が難しい。
評価順序によって結果が変わるのをなんとかしたい。引数を明示化して、自由変数が一つでも残っている場合はdefineを動かさない等の対処が必要かも。
*1:要素が空のSimpleContainerや中身が空文字列のSimpleLiteralでも良かったけど・・・