seratch's weblog in Japanese

About Scala, Java and Ruby programming in Japaense. If you need English information, go to http://blog.seratch.net/

Type Dynamic を type safe に扱う方法

Type Dynamic とは?

Scala 2.10 から Type Dynamic という動的なメソッド/フィールド呼び出し機能が追加されました*1

http://docs.scala-lang.org/sips/pending/type-dynamic.html

これは動的なのでコンパイルチェックの恩恵を受けることは出来ません。不正な呼び出しなら例外を throw するという使い方が一般的かと思います。

import scala.language.dynamics
object Example extends Dynamic {
  def selectDynamic(name: String): String = {
    if (name.matches("^[A-Z0-9].+")) throw new IllegalArgumentException(s"${name} should be uncapitalized!")
    name.toUpperCase
  }
}
val result = Example.getName // "GETNAME"

Example.GetName
/*
java.lang.IllegalArgumentException: GetName should be uncapitalized!
   at Sample$.selectDynamic(<console>:10)
   at .<init>(<console>:10)
   at .<clinit>(<console>)
 */

便利な反面、実行時エラーまで不正に気づけないのはちょっと残念ですね。

しかし、大丈夫です。

これまた Scala 2.10 から追加された macro (experimental)をもってすれば、この Type Dynamic でさえもコンパイルチェックをかけることが可能です。

シンプルな実装例

以下はその実装サンプルです。メソッド名が大文字・数字で始まっていたらコンパイルエラーになります。

ExampleMacros.scala が macro の実装です。

文字列をチェックして問題があれば c.error を呼び出していますが、ここでコンパイルエラーになります。

もし問題なければ、全てを大文字にして返します。

2014 年の追記

context.eval() は効率が悪く、コンパイル時間に多大な影響があるので、以下のようにする方がよいです。

https://github.com/scalikejdbc/scalikejdbc/pull/241

ScalikeJDBC での事例

今回、主に私が開発している ScalikeJDBC という DB アクセスライブラリで、これを活用しました。

https://github.com/seratch/scalikejdbc

まず、以下の例を見てください。

case class Member(id: Long, name: String)
object Member extends SQLSyntaxSupport[Member] { 
  def apply(m: ResultName[Member])(rs: WrappedResultSet) = new Member(
    id = rs.long(m.id), name = rs.string(m.name)
  )
}

val id = 123
val m = Member.syntax("m")
val member: Option[Member] = sql"select ${m.result.*} from ${Member as m} where ${m.id} = ${id}"
  .map(Member(m.resultName)).single.apply()

いろんなところに出てきている m.id や m.name といった呼び出しはすべて Type Dynamic による動的なメソッド呼び出しです。

1.4.8 までは、うっかり「m.namae」のように typo してしまった場合、そのミスには実行時までそれに気づけませんでした。もちろんテストは書いていれば実行されるので気づけるわけですが、できればコンパイラに見つけてもらえる方が嬉しいですね。

今回、macro によるコンパイルチェックを導入した 1.5.0 からは、これをコンパイルエラーとして検知できるようになりました。

そのためのマクロの実装は以下です。

https://github.com/seratch/scalikejdbc/blob/17c88af120f590894678868eb239bf38240be96c/scalikejdbc-interpolation-macro/src/main/scala/scalikejdbc/SQLInterpolationMacro.scala

実装にあたり、@pab_tech さんが公開していたサンプルが参考になりました。また、困って stackoverflow で質問したらこれまた @pab_tech さんが教えてくれました!

http://stackoverflow.com/questions/15653550/calling-instance-method-in-macro-implementation-for-selectdynamic

これからは足を向けて寝られませんね。

少しそれて runtime reflection の話・・

ちなみに今回の主題からは少しそれますが、こちらも Scala 2.10 から追加された runtime reflection API (experimental)が 2.10.1 現在で thread-safe でないという(致命的な)問題を抱えています。

https://issues.scala-lang.org/browse/SI-6240

それが fix された暁*2には上記の apply メソッドは自動生成できるようになる予定です*3

そうなったら、以下のようなコードが動作することになり非常にwktkなのですが・・

case class Member(id: Long, name: String)
object Member extends SQLSyntaxSupport[Member]

val m = Member.syntax("m")
val members: List[Member] = sql"select ${m.result.*} from ${Member as m}"
  .map(Member(m.resultName)).list.apply()

それはまたリリースできるようになったらお知らせします。

Enjoy type-safe type dynamic!

以上、Type Dynamic をコンパイルチェックにかける方法のご紹介でした。

macro の威力の片鱗を味わいましたね・・・!

2014 年の追記(再)

context.eval() は効率が悪く、コンパイル時間に多大な影響があるので、以下のようにする方がよいです。

https://github.com/scalikejdbc/scalikejdbc/pull/241

*1:Ruby でいう method_missing のような

*2:2.10.2 という噂

*3:今は実装は出来ているがリリースできない状態・・