Scalaでは、複数行文字列リテラルに対して、区切り文字までの空白を除去する stripMargin
というメソッドが提供されている。
Clojureでも同じようなことをしたいなぁ、ということで関数を書いてみた。
サンプルコードはこちら: https://github.com/lagenorhynque/strip-margin-test
Scalaの stripMargin
メソッド
Scalaでtriple quoteによる複数行リテラルを書くとき、単に改行を入れるだけでは行頭にインデント分の空白が含まれてしまう。
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
を利用すると、以下のようにデフォルトでは |
文字までの空白を落とした文字列を得ることができる。
オーバーロードされている stripMargin(marginChar: Char)
で区切り文字を別の文字に切り替えることもできる(ここでは例として ;
)。
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
は、以下のようなメソッドとして実装されている。
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で実装してみると、
(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
(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版と同等の機能を果たしていることが分かる。
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 (ある種の制限付きユーザー定義リーダーマクロ)を利用してみる。
{smt/s strip-margin-test.core/strip-margin}
data_readers.clj
でリテラルのシンボルと関数を対応付けることで、ここでは (strip-margin-test.core/strip-margin "...")
の代わりに独自のリテラルとして #smt/s "..."
と書けるようになる。
第2引数で区切り文字を切り替えることはできなくなったが、リテラル記法でより簡潔に strip-margin
できる。
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を利用すると、独自のリテラルを提供することができる