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);
     }