LoginSignup
4
0

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-03-26

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

Scalaの stripMargin メソッド

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

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 を利用すると、以下のようにデフォルトでは | 文字までの空白を落とした文字列を得ることができる。
オーバーロードされている stripMargin(marginChar: Char) で区切り文字を別の文字に切り替えることもできる(ここでは例として ;)。

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/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で実装してみると、

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

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版と同等の機能を果たしていることが分かる。

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 (ある種の制限付きユーザー定義リーダーマクロ)を利用してみる。

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 できる。

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

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0