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 からは、これをコンパイルエラーとして検知できるようになりました。
そのためのマクロの実装は以下です。
実装にあたり、@pab_tech さんが公開していたサンプルが参考になりました。また、困って stackoverflow で質問したらこれまた @pab_tech さんが教えてくれました!
これからは足を向けて寝られませんね。
少しそれて 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() は効率が悪く、コンパイル時間に多大な影響があるので、以下のようにする方がよいです。