Clojureで複数行文字列リテラルをstripMarginする

Scalaでは、複数行文字列リテラルに対して、区切り文字までの空白を除去する `stripMargin` というメソッドが提供されている。
Clojureでも同じようなことをしたいなぁ、ということで関数を書いてみた。
サンプルコードはこちら: https://github.com/lagenorhynque/strip-margin-test

# Scalaの `stripMargin` メソッド

Scalaでtriple quoteによる複数行リテラルを書くとき、単に改行を入れるだけでは行頭にインデント分の空白が含まれてしまう。

```scala:REPL
scala> println("""def factorial(n: Int): Int = {
     |              if (n == 0) 1
     |              else n * factorial(n - 1)
     |            }""")
def factorial(n: Int): Int = {
             if (n == 0) 1
             else n * factorial(n - 1)
           }

```

Stringに対して呼び出せるメソッド [`stripMargin`](http://www.scala-lang.org/api/current/scala/collection/immutable/StringLike.html#stripMargin:String) を利用すると、以下のようにデフォルトでは `|` 文字までの空白を落とした文字列を得ることができる。
オーバーロードされている [`stripMargin(marginChar: Char)`](http://www.scala-lang.org/api/current/scala/collection/immutable/StringLike.html#stripMargin(marginChar:Char):String) で区切り文字を別の文字に切り替えることもできる(ここでは例として `;`)。

```scala:REPL
scala> println("""def factorial(n: Int): Int = {
     |              if (n == 0) 1
     |              else n * factorial(n - 1)
     |            }""".stripMargin)
def factorial(n: Int): Int = {
             if (n == 0) 1
             else n * factorial(n - 1)
           }

scala> println("""def factorial(n: Int): Int = {
     |           |  if (n == 0) 1
     |           |  else n * factorial(n - 1)
     |           |}""".stripMargin)
def factorial(n: Int): Int = {
  if (n == 0) 1
  else n * factorial(n - 1)
}

scala> println("""def factorial(n: Int): Int = {
     |           ;  if (n == 0) 1
     |           ;  else n * factorial(n - 1)
     |           ;}""".stripMargin(';'))
def factorial(n: Int): Int = {
  if (n == 0) 1
  else n * factorial(n - 1)
}

```

この `stripMargin` は、以下のようなメソッドとして実装されている。

cf. https://github.com/scala/scala/blob/v2.12.1/src/library/scala/collection/immutable/StringLike.scala#L183-L205

```scala:scala/collection/immutable/StringLike.scala
def stripMargin(marginChar: Char): String = {
  val buf = new StringBuilder
  for (line <- linesWithSeparators) {
    val len = line.length
    var index = 0
    while (index < len && line.charAt(index) <= ' ') index += 1
    buf append
      (if (index < len && line.charAt(index) == marginChar) line.substring(index + 1) else line)
  }
  buf.toString
}

def stripMargin: String = stripMargin('|')
```

# Clojureの関数として実装

上記のScalaの `StringLike#stripMargin` メソッドの実装を参考にClojureで実装してみると、

```clj:strip_margin_test/core.clj
(ns strip-margin-test.core
  (:require [clojure.string :as str]))

(defn strip-margin
  ([s]
   (strip-margin s \|))
  ([s delimiter]
   (->> (for [line (str/split-lines s)]
          (let [len (count line)
                index (atom 0)]
            (loop []
              (when (and (< @index len)
                         (<= (int (.charAt line @index)) (int \space)))
                (swap! index inc)
                (recur)))
            (if (and (< @index len)
                     (= (int (.charAt line @index)) (int delimiter)))
              (.substring line (inc @index))
              line)))
        (str/join "\n"))))
```

Scalaでの実装を大部分そのまま直訳する形で実装したが、(おそらく性能面での考慮によるとはいえ)明らかに低レベルで複雑なコードになっているので、高レベルな関数の組み合わせとして書き換えてみる。
ちょうどStackOverflowで過去にquestionになっていたため、こちらも参考に再実装してみると、
cf. http://stackoverflow.com/questions/3872151/clojure-stripmargin

```clj:strip_margin_test/core.clj
(ns strip-margin-test.core
  (:require [clojure.string :as str]))

(defn strip-margin
  ([s]
   (strip-margin s "\\|"))
  ([s delimiter]
   (let [p (re-pattern (str "^\\s*" delimiter))]
     (->> s
          (str/split-lines)
          (map #(str/replace-first % p ""))
          (str/join "\n")))))
```

正規表現を利用する都合でデフォルト区切り文字 `|` をcharではなくエスケープしたStringとして指定するように変更を加えたが、clojure.stringの関数の組み合わせで十分シンプルに実装できた。
この関数をREPLで試してみると、以下の通りScala版と同等の機能を果たしていることが分かる。

```clj:REPL
strip-margin-test.core=> (println (strip-margin "(defn factorial [n]
                    #_=>                           (if (zero? n)
                    #_=>                             1
                    #_=>                             (* n (factorial (dec n)))))"))
(defn factorial [n]
                          (if (zero? n)
                            1
                            (* n (factorial (dec n)))))
nil
strip-margin-test.core=> (println (strip-margin "(defn factorial [n]
                    #_=>                        |  (if (zero? n)
                    #_=>                        |    1
                    #_=>                        |    (* n (factorial (dec n)))))"))
(defn factorial [n]
  (if (zero? n)
    1
    (* n (factorial (dec n)))))
nil
strip-margin-test.core=> (println (strip-margin "(defn factorial [n]
                    #_=>                        ;  (if (zero? n)
                    #_=>                        ;    1
                    #_=>                        ;    (* n (factorial (dec n)))))"
                    #_=>                        ";"))
(defn factorial [n]
  (if (zero? n)
    1
    (* n (factorial (dec n)))))
nil
```

# tagged literalで独自のリテラルにしてみる

ここまでで定義した関数 `strip-margin` はすでにScalaの `stripMargin` メソッドと同様に機能するが、せっかくなので(?) [tagged literal](https://clojure.org/reference/reader#tagged_literals) (ある種の制限付きユーザー定義リーダーマクロ)を利用してみる。

```clj:data_readers.clj
{smt/s strip-margin-test.core/strip-margin}
```

`data_readers.clj` でリテラルのシンボルと関数を対応付けることで、ここでは `(strip-margin-test.core/strip-margin "...")` の代わりに独自のリテラルとして `#smt/s "..."` と書けるようになる。
第2引数で区切り文字を切り替えることはできなくなったが、リテラル記法でより簡潔に `strip-margin` できる。

```clj:REPL
strip-margin-test.core=> (println #smt/s "(defn factorial [n]
                    #_=>                 |  (if (zero? n)
                    #_=>                 |    1
                    #_=>                 |    (* n (factorial (dec n)))))")
(defn factorial [n]
  (if (zero? n)
    1
    (* n (factorial (dec n)))))
nil
```

# まとめ

- Scalaの `stripMargin` メソッドのような機能は比較的簡単にClojureでも実現できる
- Clojureのtagged literalを利用すると、独自のリテラルを提供することができる

# Further Reading

- [Clojure - The Reader](https://clojure.org/reference/reader)
    - [Literals](https://clojure.org/reference/reader#_literals)
    - [Tagged Literals](https://clojure.org/reference/reader#tagged_literals)
    - cf. [日本語版: Clojure - Reader(リーダー)](https://japan-clojurians.github.io/clojure-site-ja/reference/reader)