CLOVER🍀

That was when it all began.

ScalaのString Interpolationを自分で書いて試してみる

Scala 2.10.0で追加されたString Interpolationですが、これまでs補間子やf補間子のような既存のものは使ってきましたが、そういえば自分で書いたことがなかったので試してみました。

参考にしたのは、以下の2つのサイトです。

文字列の補間
http://docs.scala-lang.org/ja/overviews/core/string-interpolation.html

文字列の補間
http://www.ne.jp/asahi/hishidama/home/tech/scala/string.html

String Interpolationを自分で定義する際には、Implicit ClassかつValue Classにすべきだと。なので、まずはこんなクラスを用意。

src/main/scala/MyStringInterpolation.scala

object MyStringInterpolation {
  implicit class SimpleStringInterpolation(val sc: StringContext) extends AnyVal {
    // ここに定義を書く
  }
}

StringContextに対する、Implicit Conversionを定義するわけですね。

では最初は、とりあえず定数値を返すようなString Interpolationを書いてみます。

  implicit class SimpleStringInterpolation(val sc: StringContext) extends AnyVal {
    def helloworld(args: Any*): String = "Hello World"
  }

先のImplicit Classのメソッドとして定義すればよいみたいです。

このメソッド名が、そのままString Interpolationとして使えます。
*確認コードは、ScalaTestを使っています

    describe("helloworld") {
      it("foobar") {
        helloworld"foobar" should be ("Hello World")
      }

      it("foobar${i}foobar") {
        val i = 10
        helloworld"foobar${i}foobar" should be ("Hello World")
      }
    }

何を入力されても、「Hello World」が返ります。もちろん、何の役にも立ちませんが。

ちなみに、この時にString Interpolationの定義をimportするのをお忘れなく。

import MyStringInterpolation._

続いて、今度は引数の内容を解析してみます。Stringを返すのではなく、リテラルの部分と変数の部分をそれぞれListにして、かつタプルにして返却してみます。String Interpolationの評価結果として、必ずしもStringを返却する必要はありません。

    def tuples(args: Any*): (List[String], List[Any]) =
      (sc.parts.toList, args.toList)

StringContext#partsでリテラル部が、変数部分は定義したString Interpolationの引数として渡されます。

よって、このtuples補間子を使うと、こういうことになります。

    describe("tuples") {
      it("Hello${v1}World${v2}!!") {
        val (v1, v2) = ("str1", "str2")
        tuples"Hello${v1}World${v2}!!" should be (List("Hello", "World", "!!"),
                                                  List("str1", "str2"))
      }

      it("Hello${1 + 2}World") {
        tuples"Hello${1 + 2}World" should be (List("Hello", "World"),
                                              List(3))
      }
    }

StringContext#partsと、メソッド引数の対比がわかりましたね。

ところで、通常のs補間子と同じような動作をさせようとするString Interpolationを作成してみようとすると

    def noesc(args: Any*): String = {
      val ai = args.iterator
      sc.parts.reduceLeft[String] { case (acc, p) =>
        acc + ai.next + p
      }
    }

なんと、エスケープシーケンスがかかった文字の解釈が妙なことになります。

    describe("noesc") {
      it("\\t\\n$s\\t\\n") {
        val s = "str"
        noesc"\\t\\n$s\\t\\n" should be ("\\\\t\\\\nstr\\\\t\\\\n")
      }
    }

「\」が倍になりましたね。

そこで、今度はStringContext#treatEscapesを、StringContext#partsの各Stringにかけてみます。

    def esc(args: Any*): String = {
      val ai = args.iterator
      sc.parts.tail.foldLeft(StringContext.treatEscapes(sc.parts.head)) {
        case (acc, p) =>
          acc + ai.next + StringContext.treatEscapes(p)
      }
    }

今度は、エスケープシーケンスが入ったものも、正しく扱われます。

    describe("esc") {
      it("\\t\\n$s\\t\\n") {
        val s = "str"
        esc"\\t\\n$s\\t\\n" should be ("\\t\\nstr\\t\\n")
      }
    }

こうすると、s補間子と同じ動作になるわけですね。

    describe("esc") {
      it("esc == s") {
        val str = "foo"
        val i = 10
        esc"Hello $str \\t \\n World $i" should be (s"Hello $str \\t \\n World $i")
      }
    }

ちなみに、自分で定義したString Interpolationでも、存在しない変数とかを埋め込んだりすると

      it("\\t\\n$s\\t\\n") {
        // コメントアウト!
        // val s = "str"
        esc"\\t\\n$s\\t\\n" should be ("\\t\\nstr\\t\\n")
      }

コンパイルエラーになります。素晴らしい。

src/test/scala/MyStringInterpolationTest.scala:42: not found: value s
[error]         esc"\\t\\n$s\\t\\n" should be ("\\t\\nstr\\t\\n")
[error]                    ^
[error] one error found

というわけで、String Interpolationを定義する時は、やっぱり定義済みのString Interpolation、つまりStringContextのソースを見てみる感じでしょうね。

試したコードは、こちらにあります。
https://github.com/kazuhira-r/string-interpolation-example