malでかんたんなテンプレートエンジンを書いてみた

言語非依存なテンプレートエンジンがあったらいいなと昔からボンヤリ考えていた（切実にほしいという程ではない）のですが、[mal（Make a Lisp）](https://github.com/kanaka/mal)で作ったらどうだろうと思って試しにやってみました。「なんかやってみたくなってやってみた」系の記事です。

Lisp を書くのは年に数回という感じの人が書いていますので、拙いところは大目に見ていただければと思います。

ちなみに去年のアドベントカレンダーではこんなのを書きました。

<!--
LibreOffice BasicでLispインタプリタ（mal）を書いた - Qiita
-->
https://qiita.com/sonota88/items/501f0efa437bc4d53cc7


# できたもの

https://github.com/sonota88/malten

`sonota88/mal` をサブモジュールとして使っているので、 `git clone --recursive ...` でクローンする必要があります。


# Ruby版

Ruby 版だとこんな感じ。

```ruby
template = <<TEMPLATE
<h1>品目一覧</h1>

<p>購入日: <%= date %></p>

<table>
  <tr>
    <th>ID</th>
    <th>品名</th>
    <th>価格</th>
  </tr>
  <% (map (fn* [item] (do %>
  <tr>
    <td><%= (get item "id") %></td>
    <td><%= (get item "name") %></td>
    <td><%= (- (get item "price")
               (if (get item "discount")
                 (get item "discount")
                 0))
        %> 円</td>
  </tr>
  <% )) items) %>
</table>
TEMPLATE

context = {
  date: "2021-12-25",
  items: [
    { id: 1, name: "foo", price: 100, discount: nil },
    { id: 2, name: "bar", price: 200, discount: nil },
    { id: 3, name: "baz", price: 300, discount: 50 }
  ]
}

rendered = Malten.render(context, template)
print rendered
```

`<% ... %>` または `<%= ... %>` の中にコードを書きます。ERB や JSP に倣ってベタなスタイルにしましたが、コードの部分が mal なので、見たことあるような、ないような、怪しげな雰囲気ですね。

<!--
`<%= ... %>` は `<% (print ...) %>` と同じなのでミニマム狙いの観点では必要ないのですが、数行足すだけでよいのでついでにやってしまいました。
`<%= ... %>` も数行足すだけでよいのでついでにやってしまいました。

例として HTML を出力していますが、エスケープ処理は何もしてないです。

hashmap のキーをシンボルにしていないのは Java版に合わせるため（後述）です。
-->

----

出力も一応貼っておきます。

```html
              
<h1>品目一覧</h1>

<p>購入日: 2021-12-25</p>

<table>
  <tr>
    <th>ID</th>
    <th>品名</th>
    <th>価格</th>
  </tr>
  
  <tr>
    <td>1</td>
    <td>foo</td>
    <td>100 円</td>
  </tr>
  
  <tr>
    <td>2</td>
    <td>bar</td>
    <td>200 円</td>
  </tr>
  
  <tr>
    <td>3</td>
    <td>baz</td>
    <td>250 円</td>
  </tr>
  
</table>
```


# Java版

Java 版も作ってみました（テンプレートは同じなので省略）。

```java
        String template = getTemplate();

        Context context = new Context();
    
        context.map.put("date", "2021-12-25");
        
        {
            List<Map<String, Object>> items = new ArrayList<>();
            items.add(new Item(1, "foo", 100, null).toPlain());
            items.add(new Item(2, "bar", 200, null).toPlain());
            items.add(new Item(3, "baz", 300,   50).toPlain());
            context.map.put("items", items);
        }

        String rendered = Malten.render(context, template);
        System.out.println(rendered);
```

大体似たような感じですね。

<!--
Integer, String, Map, List を含むデータで軽く確認しました。
-->





# 概観

<!--
テンプレートエンジンの大雑把な理解としてはこんな感じです。
-->
大雑把な処理の流れ。

```
テンプレートテキスト
    ↓
    ↓レンダリング用コード生成
    ↓
レンダリング用コード（mal のコード）
    ↓
    ↓eval（ここでコンテキストを参照）
    ↓
出力テキスト
```

<!--
レンダリング用コードができてしまえば、あとは eval に渡すだけです。
-->




# レンダリング用コード生成

<!--
eval の部分は eval するだけなので、
レンダリング用コード生成について少しメモ。
-->

たとえば、次のようなテンプレートテキストがあったとして、

```erb
# 品目一覧

<% (map (fn* [item] (do %>

- <% (print (get item "name")) %> <% (print (get item "price")) %> 円

<% )) items) %>
```

これを変換すると次のような mal のコードになる。元のテンプレートと比べて眺めると何やってるかなんとなく分かりますよね。

```clojure
(print "# 品目一覧\n\n")

(map (fn* [item] (do

    (print "\n\n- ")
    (print (get item "name"))
    (print " ")
    (print (get item "price"))
    (print " 円\n\n")

)) items)
```

<!--
mal のコードができたら、後はそれを eval するだけ。eval するだけなのでここはらくちん。
このときにコンテキストを与えます。

 ```javascript
[
  { id: 1, name: "foo", price: 100 },
  { id: 2, name: "bar", price: 200 }
]
 ```

名前を付けて env に入れればOK。
-->

下請けの関数などは除いて骨組部分だけ貼ります。

```clojure
;; malten.mal

;; <% ... %> の中身の長さを求める
(def! mal-code-length
  (fn* [rest]
       (let* [iter (fn* [rest2 pos]
                       (if (s#start-with? rest2 "%>")
                           pos
                         (iter (s#rest rest2) (+ pos 1))))
                   ]
         (iter rest 0))))

;; <% ... %> の部分の処理
(def! gen-renderer-code
  (fn* [rest buf acc]
       (let* [len (mal-code-length rest)]
         (gen-renderer-text
          (s#drop rest (+ len 2))
          "" ; clear buf
          (cons
           (if (s#start-with? rest "=")
               (list 'code-print (s#substring rest 1 len))
             (list 'code (s#substring rest 0 len)))
           (cons (list 'text buf)
                 acc))))))

(def! gen-renderer-text
  (fn* [rest buf acc]
       (if (nil? rest)
           (cons (list 'text buf) acc) ; end of iteration
         (if (s#start-with? rest "<%")
             (gen-renderer-code
              (s#drop rest 2) ; "<%" を除去
              buf
              acc)
           (gen-renderer-text (s#drop rest 1)
                              (str buf (s#first rest))
                              acc)))))

(def! to-code
  (fn* [part]
       (let* [
              type    (nth part 0)
              content (nth part 1)
              ]
         (cond
          (= type 'code)       content
          (= type 'code-print) (str "(print " content ")\n")
          (= type 'text)       (str "(print " (pr-str content) ")\n")))))

(def! gen-renderer
  (fn* [template]
       (let* [reversed-parts (gen-renderer-text
                              template
                              ""  ; buf
                              '() ; acc
                              )
                             ]
         (l.foldr (fn* [part acc]
                       (str acc (to-code part)))
                  ""
                  reversed-parts))))
```

軽い用途向けでも、たとえば入力が 1000 文字を越えると動きません、では困るので TCO（末尾呼び出しの最適化）が効くように書く必要があります。理解があやふやだったのですが、今回 mal の TCO のしくみについておさらいして、ある程度書いて慣れることができて良かったです。

<!--
 ----

mal のコードを生成する処理は、大雑把には

- `<% ... %>` の部分とそれ以外を分ける
- 分けたデータを使ってコード生成

に別れます。
で、後半の処理では

- `<% ... %>` の部分だったら中身（`<%` `%>` は除く）をそのまま出力
- `<%= ... %>` の部分だったら `(print 中身)` を出力
- それ以外の部分だったら `(print (pr-str その部分))` を出力

とやればいいだけなので、かんたんですね。実際のコードでもそのまんま。

```clojure
(cond (= type 'code)       content
      (= type 'code-print) (str "(print " content ")\n")
      (= type 'text)       (str "(print " (pr-str content) ")\n"))
```
-->


# レンダリング用コードの eval

基本的には eval するだけですが、コンテキストは mal の型に合わせて変換してあげる必要があります。Ruby 版だとこんな感じ。

```ruby
# ruby/mal.rb

  def self.to_mal_val(v)
    case v
    when Array
      # List は mal 側で用意されているクラス
      List.new(v.map { |el| to_mal_val(el) })
    when Hash
      v
        .to_a
        .map { |k, _v|
          [k.to_s, to_mal_val(_v)] # Java版に合わせてキーを String にしている
        }
        .to_h
    else
      v
    end
  end
```


# core への追加

文字列処理をするための最低限の関数として `s#first`, `s#rest`, `s.cons` と、改行なしで print する `print` を追加しました。

Ruby 版であればこんなの。

```ruby
# ruby/mal.rb

  ADDITIONAL_CORE_FUNCS = {
    :print => lambda { |x| print x },
    :"s#first" => lambda { |_self| _self[0] },

    :"s#rest" => lambda { |_self|
      if _self.size <= 1
        nil
      else
        _self[1..-1]
      end
    },

    :"s.cons" => lambda { |first, rest|
      if rest.nil?
        first
      else
        first + rest
      end
    }
  }
```

あとはこれを `$core_ns` に追加してあげればOK。

```ruby
# {mal}/impls/ruby/stepA_mal.rb のこの部分

$core_ns
  .merge(ADDITIONAL_CORE_FUNCS)
```


他にも便利な関数を追加してリッチにしていくと、mal のレイヤーで書く部分が楽になったりパフォーマンスが改善されたりすると思うのですが、移植コストとのトレードオフになるでしょう。



----

というわけで、 mal のコードを一度書くだけ（※1）で 86 の言語（※2）で同じように動く（※3）テンプレートエンジンが手に入りました。

※1: 実際は各言語ごとに多少手を入れる必要あり
※2: 2021-12-25 現在
※3: たぶん。試してないです。



# メモ

- 動いたので満足（という程度の試みです）
- 2日くらいで書いたプロトタイピングです。細かいとこは雑。
- 自分用のツールなどでは様子を見つつ使っていこうかなという気持ちになった
- テンプレートエンジン、ミニマムな機能だけでよければ簡単。「試しに何か書いてみたい」というときのお題としてもお手頃で、実用できそうな雰囲気があるのも良いです。
- `pr-str` は言語依存の機能で実装されている
    - たとえば Ruby 版は inspect、 Java 版では commons-lang3 の StringEscapeUtils を使っている
    - コーナーケースが気になる場合は mal で書き直すとよさそう
- 遅い。文字列の処理で、一度文字のリストにばらして加工してまた文字列に戻す（`s#to-chars` と `s.from-chars`）ということをやっていて、遅いです。
  文字列版の car, cdr, cons だけあればあとはリスト用の関数に丸投げできるよね、というのを（やったことなかったので）試してみたかったのでした。今回やってみて気が済みました。
  直交性はあるけど普通に計算量的に厳しい、という体験ができました。後で書き直すかも。
- たとえば Java でテンプレートエンジン自作するとなると、コード生成まではいいとして実行どうするんだろ、コンパイルしないといけないよね？ [JavaCompiler](https://www.ne.jp/asahi/hishidama/home/tech/java/JavaCompiler.html) 
を使えばできそう？ とかのあたりがめんどくさそうだなーと思って実際に作るとこまで至っていなかったのですが（やったことないので億劫）、Java で書かれたインタプリタで動かせばコンパイルのことを考えなくてよくなると気付いて、そっか、そういう手があるのか、なるほどとなりました。で、そういうのを思いついたときにサッと使える mal は便利。

<!--
そしてそういう場合に mal は手軽で便利です。
    - 遅い。ベース言語で書く部分を増やせば速くなるが、移植性・保守性とトレードオフ
- Java 版の mal では map のキーは String 固定（シンボルは使えない）
    - `Env#data` の型が `HashMap<String,MalVal>` になってる
-->

----

<!--
- mal で実用を目指す場合どんな障壁があるか知りたい、どれくらい実用できるものが作れるか探りたい
- 文字列の処理で、一度文字のリストにばらして加工してまた文字列に戻す（`s#to-chars` と `s.from-chars`）ということをやっていて、
  たぶん入力が大きくなると遅くなる＜計算量的に辛くなる＞はず。今回はなんとなく直交性を気にして＜直交性を優先する方向に倒して＞やってみましたが、
  実用するなら文字列処理はリスト処理に丸投げせず専用の関数を書く、それでも遅ければベース言語で書くといった手当が必要だと思います。
  - 文字列も最低限 car, cdr, cons があればなんとかなるよね、というのを（やったことなかったので）試してみたかった
- 実用するために必要なこと
    - 環境とコードを渡して read-eval を呼び出す部分
    - mal の型との相互変換
        - テンプレートエンジンの場合は戻りが文字列なので、片方だけやればOK
    - 最低限の文字列処理
- もちろんパフォーマンスを重視するならわざわざ mal でやる必要はないわけで、
  遅くてもいいからポータビリティの方が重要という場合は個人的には選択肢になるかなと思える感じ。
-->



## Java版だとhashmapのキーとしてシンボルが使えない

詳しく調べてませんがメモ。

Java版は hashmap のキーとして文字列を期待しているらしく、エラーになります。

```
Mal [java]

user> { 'a 123 }
Uncaught java.lang.ClassCastException: mal.types$MalList cannot be cast to mal.types$MalString: mal.types$MalList cannot be cast to mal.types$MalString

# 文字列ならOK
user> { "a" 123 }
{"a" 123}
```

Ruby 版だとシンボルでもOK。

```
Mal [ruby]

user> { 'a 123 }
{a 123}
```