文字列を段階的に組み立てる

プログラムで文字列を組み立てていると、 `"(" ^ なにか ^ ")"` のように、文字列結合で段階的に結果を組み立てたいことがある。

文字列が小さいことがわかっているうちはこれでもよいのだけれど、文字列が大きくなってくると段々と計算量が気になってくる（特に最適化をしていなければ、文字列の長さを n とすると結合のたびに O(n) のコストがかかる）。

左から右に組み立てることがわかっていれば、OCamlなら `Buffer` モジュールが使えるが、括弧のようなものがからむとそうも言っていられない。

stringを使うのをやめて、[rope](https://en.wikipedia.org/wiki/Rope_(data_structure))のようなデータ構造を使う手もあるけど、依存ライブラリを増やしたくない場合もある（言語処理系によっては文字列の内部表現がropeになっていて以下のような小細工をする必要がない場合もある）。

ということで、自分でデータ構造をつくってみる。

```ocaml
type t =
  | Leaf of int * string
  | Concat of int * t * t
```

型 `t` は `^` による文字列結合の構文木を表現したもので、 `Concat` が `^`、 `Leaf` が文字列値に対応する。

ついでに、文字列化のときの便利のために結合後の文字列長を持たせておく。

```ocaml
let length = function
  | Leaf (i, _) -> i
  | Concat (i, _, _) -> i
```

長さを手で与えると間違えるので、smart constructor的な関数を用意する。

```ocaml
let leaf s =
  Leaf (String.length s, s)

let empty = leaf ""

let (^) t1 t2 =
  Concat (length t1 + length t2, t1, t2)
```

文字列を結合するたびに `leaf` と書くのも面倒なので、文字列を追加するための演算子も定義する。

```ocaml
let (@+) s t =
  leaf s ^ t

let (+@) t s =
  t ^ leaf s
```

`"a" @+ "b" @+ x` は `"a" @+ ("b" @+ x)` のように解釈されてほしいので右結合になるように演算子名を `@` で始めておく（OCamlの演算子の結合順位は、演算子の初めの文字で決まる）。逆に `x +@ "a" +@ "b"` は `(x +@ "a") +@ "b"` になってほしいので、 `@+` の反対の `+@` にしておく。こうしておくと、 `+` で始まる演算子は右結合で `@` で始まる演算子よりも結合が強いので、 `"a" @+ x +@ "b" +@ "c"` が `"a" @+ ((x +@ "b") +@ "c")` のようになり、うまい具合いに型がつくようになるし、文字列の側に `@` をつけるという規則になって見た目にも覚えやすい。

これで、結合は O(1) でできるようになった。

あとはこれを文字列化できるようにする。

```ocaml
let to_string t =
  let buf = Buffer.create @@ length t in
  let rec loop = function
    | Leaf (_, s) -> Buffer.add_string buf s
    | Concat (_, t1, t2) ->
      loop t1;
      loop t2
  in
  loop t;
  Buffer.contents buf
```

ここは素直に副作用をつかって `Buffer` で組み立てる。固定長のバッファを端から埋めているので O(n) だろう。

`to_string` を見ていると、 `loop` が末尾再帰になっていないのがちょっと気になる。

`^` は結合的な演算なので、 `(a ^ b) ^ c = a ^ (b ^ c)` だ。さらに、 `a ^ (b ^ (c ^ empty))` のように変形していけば `list` と同型にできるので、 `List.iter` と同じように末尾再帰でループをまわせそうな気がする。

```ocaml
let to_string' t =
  let buf = Buffer.create @@ length t in
  let rec loop rest = function
    | Leaf (_, s) ->
      Buffer.add_string buf s;
      begin match rest with
      | r::rs ->
        loop rs r
      | [] ->
        Buffer.contents buf
      end
    | Concat (_, t1, t2) ->
      loop (t2::rest) t1
  in loop [] t
```

構造を一度に変換する必要はないので、 `Concat` に出逢うたびに右部分木をリストに押し込んでおいて、少しずつ構造を変換していく。

使い方はこんな風になる。

```ocaml
let () =
  ("1" @+ empty) +@ "2" |> to_string' |> print_endline;
  "1" @+ (empty +@ "2") |> to_string' |> print_endline;
  "1" @+ (empty +@ "2" +@ "3") |> to_string' |> print_endline;
  ("1" @+ empty +@ "2") +@ "3" |> to_string' |> print_endline;
  empty +@ "1" +@ "2" +@ "3" +@ "4" |> to_string' |> print_endline;
  let x = leaf "xy" in
  let x = "{" @+ "(" @+ x +@ ")" +@ "}" in
  x |> to_string' |> print_endline;
  ()
```

ropeや他のデータ構造にしておけば、結合以外に文字列の分割などもサポートできるが、結合と文字列化だけに機能を搾れば比較的簡単に効率のよい実装ができる。

実際に使ってみると、 OCaml の関数適用演算子と優先順位がかぶって使いにくいという問題があったがそれは御愛嬌ということで（関数適用は `(@@)` でなくて `(&)` の方がいいと言う人の気持ちがよくわかった）。

## おまけ: `String.concat` 版

Standard MLにはOCamlの `Buffer` のようなものがないらしいので、Standard MLに移植しやすそうな `String.concat` を使った `to_string` も書いてみた。

```ocaml
let to_string'' t =
  let rec loop acc rest = function
    | Leaf (_, s) ->
      begin match rest with
      | [] -> s::acc
      | r::rs -> loop (s::acc) rs r
      end
    | Concat (_, t1, t2) ->
      loop acc (t1::rest) t2
  in
  loop [] [] t |> String.concat ""
```

末尾再帰版と同じ方針で右部分木から処理していけばリストを後ろから組み立てていける。

`String.concat` は最初に結果文字列の長さ分の領域を確保し、順番に埋めていくので O(n) だ（OCaml, SML#, MLton調べ）。

追記: Standard MLでもBasisライブラリへのBufferモジュールの追加は議論されているようだ。[2015 004 Addition of Buffer module · SMLFamily/BasisLibrary Wiki](https://github.com/SMLFamily/BasisLibrary/wiki/2015-004-Addition-of-Buffer-module)

## 参考

* Gaucheではこのような処理が[text.tree](http://practical-scheme.net/gauche/man/?l=ja&p=text.tree)モジュールとして提供されている。