CLOVER🍀

That was when it all began.

Scala 2.10.0 Type Dynamic

Scalaで動的にメソッドやフィールドを定義するための、SIP-17 Type Dynamicについて扱おうと思います。
http://docs.scala-lang.org/sips/pending/type-dynamic.html

Scala自身は静的型付け言語なので、こういう仕組みが入るのはどうなんでしょうと思うのですが、開発の動機はDSLを意識したもののようですね。

それにしても、Groovyだと静的型付けに関する機能を追加してたり…まあ、長所短所の話ですね。

使い方

scala.DynamicというトレイトをMix-inしたクラスを定義すると、そのクラスに対して動的にメソッドやフィールドを定義できるようになります。

class DynaClass extends Dynamic

DynamicトレイトをMix-inしたクラスを作成するためには、以下のimport文

import scala.language.dynamics

またはコンパイルオプションとして、以下の指定が必要です。

-language:dynamics

実際の動的なメソッドやフィールドの定義の方法ですが、DynamicトレイトをMin-inしたクラスに対して、以下のような呼び出し規則でメソッドを定義すればよいみたいです。

foo.method("blah")      ~~> foo.applyDynamic("method")("blah")
foo.method(x = "blah")  ~~> foo.applyDynamicNamed("method")(("x", "blah"))
foo.method(x = 1, 2)    ~~> foo.applyDynamicNamed("method")(("x", 1), ("", 2))
foo.field           ~~> foo.selectDynamic("field")
foo.varia = 10      ~~> foo.updateDynamic("varia")(10)
foo.arr(10) = 13    ~~> foo.selectDynamic("arr").update(10, 13)
foo.arr(10)         ~~> foo.applyDynamic("arr")(10)

では、順に。

あと、受け取っているパラメータなどを表示するため、以下のメソッドを使用することにします。

class DynaClass extends Dynamic {
  private def dump(prefix: String, name: String, values: Any*): Unit =
    values.toList match {
      case Nil => println(s"$prefix: name = $name")
      case _ => println(s"$prefix: name = $name, values = $values")
    }
}

applyDynamic

メソッド呼び出しに対して、動的処理を行います。定義例は、以下になります。

class DynaClass extends Dynamic {
  def applyDynamic(name: String)(values: Any*): Unit =
    dump("applyDynamic", name, values: _*)

他のどのメソッドもそうですが、メソッドの第1引数は動的ディスパッチした時のメソッド名やフィールド名が、引数を受け取る場合はカリー化した上で引数を定義します。

使用例。

    val dc = new DynaClass
    dc.method("blah")

実行結果。

applyDynamic: name = method, values = WrappedArray(blah)

また、配列へのインデックス適用もapplyDynamicですね。

    val dc = new DynaClass
    dc.array(10)

実行結果。

applyDynamic: name = array, values = WrappedArray(10)

applyDynamicNamed

名前付きで引数を指定したメソッド呼び出しに対して、フックを行います。定義例は、以下になります。

class DynaClass extends Dynamic {
  def applyDynamicNamed(name: String)(pairs: (String, Any)*): Unit =
    dump("applyDynamicNamed", name, pairs: _*)

この場合、引数にはパラメータ名と値のタプルが渡ってきます。

使用例。

    val dc = new DynaClass
    dc.method(x = "blah")
    dc.method(x = 1, 2)

実行結果。

applyDynamicNamed: name = method, values = WrappedArray((x,blah))
applyDynamicNamed: name = method, values = WrappedArray((x,1), (,2))

ちょっとわかりにくいですが、パラメータ名を指定しなかった方は、パラメータ名の引数の部分が空文字となります。

selectDynamic

フィールドの参照に対して、フックを行います。以下、定義例です。

class DynaClass extends Dynamic {
  def selectDynamic(name: String): String = {
    dump("selectDynamic", name)
    "value"
  }

使用例。

    val dc = new DynaClass
    println(dc.field)

実行結果。

selectDynamic: name = field
value

2つ目の出力は、フィールド参照の結果、返ってきた値ですね。

ただ、このサンプルだと

dc.array(10) = 13    ~~> dc.selectDynamic("array").update(10, 13)

という記載はコンパイルエラーとなります。

error: value update is not a member of String
    dc.array(10) = 13

selectDynamicの結果が、updateメソッドを持つクラスのインスタンスを返さないといけないようで。

うーん、なんか破綻しているような…。

とりあえず、簡単に済ませたいので先ほどのサンプルのselectDynamicメソッドを自分自身を返すように変更し、さらに自身にupdateメソッドを定義します。

class DynaClass extends Dynamic {
  def update(index: Int, value: Int): Unit =
    dump("update", index.toString, value)

  def selectDynamic(name: String): this.type = {
    dump("selectDynamic", name)
    this
  }

使用例。

    val dc = new DynaClass
    dc.array(10) = 13

実行結果。

selectDynamic: name = array
update: name = 10, values = WrappedArray(13)

とりあえず、動きました。

たぶんこれ、あんまりよくない例な気がしますが…。

updateDynamic

フィールドへの代入をフックします。

…が、これ動かせませんでした。

そもそも、DynamicトレイトをMix-inしたクラスに、何もメソッド定義しない状態だと

class DynaClass extends Dynamic

こんな感じに書くと

    val dc = new DynaClass
    dc.variable = 10

以下のように怒られます。

error: value selectDynamic is not a member of DynaClass
error after rewriting to dc.<selectDynamic: error>("variable")
possible cause: maybe a wrong Dynamic method signature?
    dc.variable = 10
    ^

selectDynamicの定義がないって言ってるよね?updateDynamicの定義じゃなくて?

では、先のselectDynamicの定義を持ってきてみます。

class DynaClass extends Dynamic {
  def selectDynamic(name: String): this.type = {
    dump("selectDynamic", name)
    this
  }

すると今度は

error: reassignment to val
    dc.variable = 10
                ^

「valに再代入はできないぞ」と怒られます…。どうなってるんだ??

では、selectDynamicを消してupdateDynamicを定義してみると

class DynaClass extends Dynamic {
  def updateDynamic(name: String)(values: Any*): Unit =
    dump("updateDynamic", name, values: _*)

やっぱり、selectDynamicがないと怒られます。

error: value selectDynamic is not a member of DynaClass
error after rewriting to dc.<selectDynamic: error>("variable")
possible cause: maybe a wrong Dynamic method signature?
    dc.variable = 10
    ^

何がいかんのだろう??

その他、気になったこと

実体を定義したメソッドの呼び出しには動的ディスパッチはかからない

すでに、dumpメソッドでもやっていますが。

val dc = new DynaClass
dc.method("Kazuhira")

class DynaClass extends Dynamic {
  def applyDynamic(name: String)(values: Any*): Unit = {
    println("applyDynamicCalled")
    hello(name)
  }

  def hello(s: String): Unit =
    println(s"Hello ${s}")
}

これを実行すると、

applyDynamicCalled
Hello method

となり、実体があるメソッドを呼び出す分には動的ディスパッチはかからないようです。ま、かかったら無限ループに陥りそうですからね。

ちなみに、これはDynamicトレイトをMin-inしたクラスを使用する側からしても、同じことです。

val dc = new DynaClass
dc.hello("Scala")

動的ディスパッチはかかりませんね。

Hello Scala

フィールド参照とかも同じかな?

xxxDynamicメソッドの定義と整合性が取れているかどうか、コンパイラがチェックする

すでに先のupdateDynamicメソッドの例でも見せていますが、動的ディスパッチの機能を使うためには、DynamicトレイトをMix-inしたクラスに対応したメソッド定義が必要です。

さもなければ、コンパイルエラーとなります。

val dc = new DynaClass
dc.method("Java")

class DynaClass extends Dynamic

これをコンパイルしようとすると

error: value applyDynamic is not a member of DynaClass
error after rewriting to dc.<applyDynamic: error>("method")
possible cause: maybe a wrong Dynamic method signature?
    dc.method("Java")
    ^

と「applyDynamicがないぞ!」と怒られます。

では、今度はapplyDynamicメソッドを定義しますが、引数は取らないように書いてみます。

val dc = new DynaClass
dc.method("Java")

class DynaClass extends Dynamic {
  def applyDynamic(name: String): Unit =
    println("$name called")
}

すると、今度は

error: Unit does not take parameters
    dc.method("Java")
             ^

となります。まあ、これは冷静に考えると、先のapplyDynamicの戻り値がUnitなのでこうなるわけで、極端な話

val dc = new DynaClass
dc.method("Java")

class DynaClass extends Dynamic {
  def apply(s: String): Unit =
    println(s)

  def applyDynamic(name: String): this.type =
    this
}

みたいなことをしても動きますが。


また、引数の部分をAnyにしたり、可変長引数にしたりしましたが、もっと具体的な型にしたりすることもできます。

とはいえ、こういう不整合な状態にすると

val dc = new DynaClass
dc.method("Java")

class DynaClass extends Dynamic {
  def applyDynamic(name: String)(value: Int): Unit =
    println(s"$name called, value[$value]")
}

普通にコンパイルエラーです。

error: type mismatch;
 found   : String("Java")
 required: Int
    dc.method("Java")
              ^

また、こんな感じに引数の数を2個にして

class DynaClass extends Dynamic {
  def applyDynamic(name: String)(value1: Int, value2: Int): Unit =
    println(s"$name called, value[$value1, $value2]")
}

呼び出し元と数が合わなくても、やっぱりコンパイルエラーです。

少なくとも、xxxDynamicメソッド定義と呼び出し元のコードの整合性が取れていないと、コンパイルが通らないということですね。

あと、パラメータ化も可能です。

class DynaClass extends Dynamic {
  def applyDynamic[T](name: String)(value: T): T =
    value
}

いろいろ使うのに悩みそうな機能ですよね…。少なくとも、乱用はするべきじゃないんでしょうね。