JavaでReaderモナドでテンプレートエンジン
前に作ったReaderモナドを使ってテンプレートエンジンを実装するよ。
といっても「The Reader monad」にあるサンプルを写経しただけ。
テンプレートの実装
サンプルを見るとこうなっている
- これはテンプレートの抽象構文表現です -- Text Variable Quote Include Compound data Template = T String | V Template | Q Template | I Template [Definition] | C [Template] ... -- テンプレートを解決し文字列にする resolve :: Template -> Reader Environment (String)
Javaには代数データ型は無いのでインタフェースとクラスで実装する。
まず、Templateをインタフェースで定義。resolveはTemplateのメソッドとして定義する。
public interface Template { Reader<Environment, String> resolve(); }
Text、Variable、Quote、Include、CompoundはこのTemplateの実装クラスとして定義する。(後述)
Environmentというのは変数の定義のまとまり。これもクラスで定義する。
また、lookupVar、lookupTemplate、addDefsはEnvironmentのインスタンスメソッドとして定義する。
import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; // -- 環境は名前付きテンプレートの連想リストと名前付きの変数値の // -- 連想リストで構成されています // data Environment = Env {templates::[(String,Template)], // variables::[(String,String)]} public final class Environment { private final Map<String, Template> templates; private final Map<String, String> variables; public Environment(Map<String, Template> templates, Map<String, String> variable) { this.templates = Collections.unmodifiableMap(templates); this.variables = Collections.unmodifiableMap(variable); } // -- 環境から変数を探索 // lookupVar :: String -> Environment -> Maybe String // lookupVar name env = lookup name (variables env) public String lookupVar(String name) { return variables.get(name); } // -- 環境からテンプレートを探索 // lookupTemplate :: String -> Environment -> Maybe Template // lookupTemplate name env = lookup name (templates env) public Template lookupTemplate(String name) { return templates.get(name); } // -- 環境に解決された定義のリストを追加 // addDefs :: [(String,String)] -> Environment -> Environment // addDefs defs env = env {variables = defs ++ (variables env)} public Environment addDefs(List<NameValuePair> defs) { Map<String, String> variables = new HashMap<String, String>(this.variables); for (NameValuePair def : defs) { variables.put(def.name, def.value); } return new Environment(this.templates, variables); } }
NameValuePairは只のタプルです。
public final class NameValuePair { public final String name; public final String value; public NameValuePair(String name, String value) { this.name = name; this.value = value; } }
Definitionは、クラスで定義。また、resolveDefはDefinitionのインスタンスメソッドとして定義する。
import com.google.common.base.Function; // data Definition = D Template Template public final class Definition { private final Template t; private final Template d; public Definition(Template t, Template d) { this.t = t; this.d = d; } // -- Definition を解決し、(name,value) の組を生成 // resolveDef :: Definition -> Reader Environment (String,String) // resolveDef (D t d) = do name <- resolve t // value <- resolve d // return (name,value) public Reader<Environment, NameValuePair> resolveDef() { return t.resolve().bind(new Function<String, Reader<Environment, NameValuePair>>() { @Override public Reader<Environment, NameValuePair> apply(final String name) { return d.resolve().bind(new Function<String, Reader<Environment,NameValuePair>>() { @Override public Reader<Environment, NameValuePair> apply(String value) { return Reader.unit(new NameValuePair(name, value)); } }); } }); } }
それでは個々のテンプレートの実装クラスを書いていく。
まずはText。内容となる文字列を持つ。resolveは端的に内容をunitして戻す。
public final class Text implements Template { private final String s; public Text(String s) { this.s = s; } // resolve (T s) = return s @Override public Reader<Environment, String> resolve() { return Reader.unit(s); } }
Textはリテラルとして使用するほかに、変数名などにも使う。
次にVariable。内容としてTemplateを持つ。
resolveは、内部のTemplateを解決して得られた文字列varNameを元に環境から値を取得して戻すReaderを生成する。
import com.google.common.base.Function; public final class Variable implements Template { private final Template t; public Variable(Template t) { this.t = t; } // resolve (V t) = do varName <- resolve t // varValue <- asks (lookupVar varName) // return $ maybe "" id varValue @Override public Reader<Environment, String> resolve() { return t.resolve().bind(new Function<String, Reader<Environment, String>>() { @Override public Reader<Environment, String> apply(final String varName) { return Reader.asks(new Function<Environment, String>() { @Override public String apply(Environment e) { return e.lookupVar(varName); } }).bind(new Function<String, Reader<Environment,String>>() { @Override public Reader<Environment, String> apply(String varValue) { return Reader.unit(varValue != null ? varValue : ""); } }); } }); } }
次にQuote。内容としてTemplateを持つ。
resolveは、内部のTemplateを解決して得られた文字列tmplNameを元に環境からテンプレートを取得して文字列として戻すReaderを生成する。
import com.google.common.base.Function; public final class Quote implements Template { private final Template t; public Quote(Template t) { this.t = t; } // resolve (Q t) = do tmplName <- resolve t // body <- asks (lookupTemplate tmplName) // return $ maybe "" show body @Override public Reader<Environment, String> resolve() { return t.resolve().bind(new Function<String, Reader<Environment,String>>() { @Override public Reader<Environment, String> apply(final String tmplName) { return Reader.asks(new Function<Environment, Template>() { @Override public Template apply(Environment e) { return e.lookupTemplate(tmplName); } }).bind(new Function<Template, Reader<Environment,String>>() { @Override public Reader<Environment, String> apply(Template body) { return Reader.unit(body != null ? body.toString() : ""); } }); } }); } }
body.toString()のところが気に食わないのだが。lookupTemplateして得たTemplateを再評価するべきではないかなあ。
@Override public Reader<Environment, String> resolve() { return t.resolve().bind(new Function<String, Reader<Environment,String>>() { @Override public Reader<Environment, String> apply(final String tmplName) { return Reader.asks(new Function<Environment, Template>() { @Override public Template apply(Environment e) { return e.lookupTemplate(tmplName); } }).bind(new Function<Template, Reader<Environment,String>>() { @Override public Reader<Environment, String> apply(final Template body) { if (body != null) { return Reader.asks(new Function<Environment, String>() { @Override public String apply(Environment e) { return body.resolve().runReader(e); } }); } else { return Reader.unit(""); } } }); } }); }
次にInclude。内容として名前を返すTemplateと、得られたTemplateを評価するローカルの変数定義を持つ。
resolveは、内部のTemplateを解決して得られた文字列tmplNameを元に環境からテンプレートを取得してそれをローカルの変数定義のもとで評価した結果を戻すReaderを生成する。
import java.util.ArrayList; import java.util.List; import com.google.common.base.Function; public final class Include implements Template { private final Template t; private final List<Definition> ds; public Include(Template t, List<Definition> ds) { this.t = t; this.ds = ds; } // resolve (I t ds) = do tmplName <- resolve t // body <- asks (lookupTemplate tmplName) // case body of // Just t' -> do defs <- mapM resolveDef ds // local (addDefs defs) (resolve t') // Nothing -> return "" @Override public Reader<Environment, String> resolve() { return t.resolve().bind(new Function<String, Reader<Environment,String>>() { @Override public Reader<Environment, String> apply(final String tmplName) { return Reader.asks(new Function<Environment, Template>() { @Override public Template apply(Environment e) { return e.lookupTemplate(tmplName); } }).bind(new Function<Template, Reader<Environment,String>>() { @Override public Reader<Environment, String> apply(final Template body) { if (body != null) { return Reader.asks(new Function<Environment, List<NameValuePair>>() { @Override public List<NameValuePair> apply(Environment e) { List<NameValuePair> defs = new ArrayList<NameValuePair>(); for (final Definition d : ds) { defs.add(d.resolveDef().runReader(e)); } return defs; } }).bind(new Function<List<NameValuePair>, Reader<Environment,String>>() { @Override public Reader<Environment, String> apply(final List<NameValuePair> defs) { return body.resolve().local(new Function<Environment, Environment>() { @Override public Environment apply(Environment e) { return e.addDefs(defs); } }); } }); } else { return Reader.unit(""); } } }); } }); } }
写像関数mapMは用意してないので「Monad support in Haskell」を読みながら意訳した。
最後にCompound。これは単純に複数のTemplateを繋げたもの。
import com.google.common.base.Function; public final class Compound implements Template { private final Template[] ts; public Compound(Template... ts) { this.ts = ts; } // resolve (C ts) = (liftM concat) (mapM resolve ts) @Override public Reader<Environment, String> resolve() { return Reader.asks(new Function<Environment, String>() { @Override public String apply(Environment e) { StringBuilder sb = new StringBuilder(); for (Template t : ts) { sb.append(t.resolve().runReader(e)); } return sb.toString(); } }); } }
これもliftMやmapM無いので意訳。だいたいあってると思う。
使用例
変数定義(環境)を用意してやって、それでプレースホルダ内の変数を置換する。
パーサ用意するのは面倒なので、プログラム中でそれらしく組み立てている。
@Test public void testVariable() { // "こんにちは、{お客様名}様。" Template t = new Compound( new Text("こんにちは、"), new Variable(new Text("お客様名")), new Text("様。")); Map<String, String> variables = new HashMap<String, String>(); variables.put("お客様名", "板東トン吉"); Environment e = new Environment(Collections.<String, Template>emptyMap(), variables); String result = t.resolve().runReader(e); assertEquals("こんにちは、板東トン吉様。", result); }
変数「お客様名」が「板東トン吉」で置き換えられる。
Includeは局所的な変数定義と一緒に生成して埋め込む。
@Test public void testInclude() { List<Definition> defs1 = Arrays.asList( new Definition(new Text("お客様名"), new Text("板東トン吉"))); List<Definition> defs2 = Arrays.asList( new Definition(new Text("来店日"), new Text("1月21日"))); // "{挨拶}{来店御礼}" Template template = new Compound( new Include(new Text("挨拶"), defs1), new Include(new Text("来店御礼"), defs2)); Map<String, Template> templates = new HashMap<String, Template>(); // "挨拶" : "こんにちは、{お客様名}様。" templates.put("挨拶", new Compound( new Text("こんにちは、"), new Variable(new Text("お客様名")), new Text("様。"))); // "来店御礼" : "{来店日}にはご来店いただき、\nまことにありがとうございます。" templates.put("来店御礼", new Compound( new Variable(new Text("来店日")), new Text("にはご来店いただき、\nまことにありがとうございます。"))); Environment context = new Environment(templates, Collections.<String, String>emptyMap()); // 評価 String result = template.resolve().runReader(context); // すべて展開されているかを確認 assertEquals( "こんにちは、板東トン吉様。1月21日にはご来店いただき、\nまことにありがとうございます。", result); }
元の環境の変数をリネームして使うこともできる。
下の例では、元の環境で「ログインユーザ名」という変数名で定義された値を、テンプレート「挨拶」内では、「お客様名」というローカルな変数名で使えるようにしている。
@Test public void testIncludeWithLocalName() { List<Definition> defs1 = Arrays.asList( new Definition(new Text("お客様名"), new Variable(new Text("ログインユーザ名")))); List<Definition> defs2 = Arrays.asList( new Definition(new Text("来店日"), new Text("1月21日"))); Template template = new Compound( new Include(new Text("挨拶"), defs1), new Include(new Text("来店御礼"), defs2)); Map<String, Template> templates = new HashMap<String, Template>(); templates.put("挨拶", new Compound( new Text("こんにちは、"), new Variable(new Text("お客様名")), new Text("様。"))); templates.put("来店御礼", new Compound( new Variable(new Text("来店日")), new Text("にはご来店いただき、\nまことにありがとうございます。"))); Map<String, String> variables = new HashMap<String, String>(); variables.put("ログインユーザ名", "板東トン吉"); Environment context = new Environment(templates, variables); // 評価 String result = template.resolve().runReader(context); // すべて展開されているかを確認 assertEquals( "こんにちは、板東トン吉様。1月21日にはご来店いただき、\nまことにありがとうございます。", result); }