💵

String Interpolation によるパターンマッチの紹介

2022/12/18に公開

はじめに

この記事は Scala Advent Calendar 2022 の 18日目の記事です。

昨日は @110416 さんによる「Rustacean のための Scala 3 入門」でした。
明日は @tarao さんによる「部分型の変性と極性について」です。

この記事のコードは Scala 3.2.0 で書いていますが、記事中の内容は Scala 2.13.0 以降の全てのバージョンで利用が可能です。

String Interpolation

Scala には String interpolation(文字列補間) と呼ばれる機構があります。

何やら難しそうな名前ですが、最近のモダンな言語であれば大抵持っている文字列リテラルに式を埋め込める機構の事ですね。

val name = "gakuzzzz"
s"Hello $name" // Hello gakuzzzz と評価される

Kotlin, Swift, C#, ECMAScript, Ruby, Python, PHP, F#, etc... 色んな言語で見られるやつです。

Scala では通常の文字列リテラル("..." or """...""") に prefix s をつける(s"..." or s"""...""")ことで式を埋め込めるようになります。

パターンマッチでの利用

一方、他の言語ではあまり見掛けない特徴として、Scala ではパターンマッチで String Interpolation を利用することができます。

val s"Hello $name" = "Hello gakuzzzz"
println(name) // gakuzzzz が出力される

これを利用すると、単純な文字列のパースなどで正規表現や String#split などを使うこと無く必要な値を取り出す事ができたりします。

例えば HTTP の Authorization Bearer ヘッダを使う認証処理なども以下のように書くこともできます。

def authenticate(request: Request): AuthenticationResult = {
  request.getHeader("Authorization") match {
    case Some(s"Bearer $token") if verify(token) => Success(decode(token))
    case _                                       => Failure
  }
}

便利ですね。

カスタム Interpolation

また、Scala の String Interpolation は Swift や C#, ECMAScript などなどと同様に拡張したカスタムの Interpolation を定義する事ができます。

StringContext に対し拡張メソッドを定義すると、そのメソッド名を prefix として Interpolation を使うことができます。

case class Foo(params: Any*)

implicit class FooInterpolation(context: StringContext) {
  def foo(params: Any*): Foo = Foo(params:_*)
}

foo"piyo ${3 * 100} poyo" // Foo(ArraySeq(300)) と評価される

上記例の様に結果のオブジェクトは String である必要はありません。

具体的な活用事例の一つとして、ScalikeJDBC では SQL Interpolation を提供しています。

SQLに直接値を埋め込むように記述できながら、実際には placeholder 構文に置き換わるSQLオブジェクトを組み立てています。これにより SQL Injection を防ぎ安全なクエリ実行ができるようになっています。

val id = "38c2444d-f311-30fe-7718-f18b16a37487"
val query = sql"select id, name from members where id = ${id}"
// SQL(
//   sql = "select id, name from members where id = ?", 
//   parameters = Seq(38c2444d-f311-30fe-7718-f18b16a37487)
// ) と評価される
execute(query) // SQL Injection を気にせず安全に実行できる

パターンマッチで使えるカスタム Interpolation

そしてカスタムの Interpolation もパターンマッチで使えるようにすることが可能です。

具体的には、prefix として利用する拡張メソッドを Extractor にします。
Extractor とは unapply もしくは unapplySeq メソッドを持ったオブジェクトの事ですね。

「拡張メソッドをオブジェクトにする」というと意味の分からない文章に聞こえてしまいますが、実は拡張メソッドの定義は必ずしもメソッドである(def で定義されている)必要はなく、広い意味での関数になっていれば問題ありません。

どう言うことかというと、

implicit class IntOps(value: Int) {
  def printDouble(): Unit = println(value * 2)
}
100.printDouble() // 200 が出力される

この拡張メソッドは以下のように val で書き換えたり、

implicit class IntOps(value: Int) {
  // def の代わりに val で Function1 オブジェクトとして定義している
  val printDouble = () => println(value * 2)
}
100.printDouble() // 200 が出力される

以下のように object で書き換えたりすることができます。

implicit class IntOps(value: Int) {
  // apply メソッドを持つオブジェクトとして定義している
  object printDouble {
    def apply(): Unit = println(value * 2)
  }
}
100.printDouble() // 200 が出力される

これを利用して カスタム Interpolation の定義も object に書き換える事ができます。

// Before
case class Foo(params: Any*)
implicit class FooInterpolation(context: StringContext) {
  def foo(params: Any*): Foo = Foo(params:_*)
}
foo"piyo ${3 * 100} poyo" // Foo(ArraySeq(300)) と評価される
// After
case class Foo(params: Any*)
implicit class FooInterpolation(context: StringContext) {
  object foo {
    def apply(params: Any*): Foo = Foo(params:_*)
  }
}
foo"piyo ${3 * 100} poyo" // Foo(ArraySeq(300)) と評価される

このオブジェクトに unapply もしくは unapplySeq を定義すれば、無事 Extractor として扱えるようになり、パターンマッチでカスタム Interpolation を使えるようになります。

case class Foo(params: Any*)
implicit class FooInterpolation(context: StringContext) {
  object foo {
    def apply(params: Any*): Foo = Foo(params:_*)
    def unapplySeq(value: Foo): Option[Seq[Any]] = Some(value.params)
  }
}
Foo(100, 200) match {
  case foo"$a, $b" => (a, b)
  case _           => (0, 0)  
} // (100, 200) と評価される

という訳で、Scala では String Interpolation をパターンマッチで使えるよという紹介でした。参考になれば幸いです。

Discussion