2012年8月30日木曜日

Scala Tips / Scala 2.10味見(2) - Type Dynamic

Type DynamicはRubyのmethod_missingのようなオブジェクトに動的にメソッドやフィールドを追加する機能です。トレイトscala.Dynamicを拡張したクラスに、動的にメソッドやフィールドを定義する機能を追加します。

Scala 2.9でexperimentalとしてサポートされていましたが、Scala 2.10から正式にサポートされることになりました。ただし、無軌道には使って欲しくはないということだと思いますが、2.10からサポートされるModularizing Language Featuresで指定した時のみ使用できるようになっています。

Type DynamicはScalaの静的型付けを破壊するので積極的には使うべきではありませんが、ActiveRecordなどの実装では効果抜群なのでDSLの実装技術としては押さえておきたいところです。

Type Dynamic

ActiveRecordをイメージして、トレイトDynamicを拡張したクラスRecordを定義します。実装は、入力されたパラメタをコンソールに表示するのみになっています。

import language.dynamics

class Record extends Dynamic {
  def applyDynamic(name: String)(args: Any*): Any = {
    println(s"applyDynamic(${name})(${args})")
  }

  def applyDynamicNamed(name: String)(args: (String, Any)*): Any = {
    println(s"applyDynamicNamed(${name})(${args})")
  }

  def selectDynamic(name: String): Any = {
    println(s"selectDynamic(${name})")
    name
  }

  def updateDynamic(name: String)(arg: Any) {
    println(s"updateDynamic(${name})(${arg})")
  }
}

最初に「import language.dynamics」でDynamicの使用を宣言します。

クラスRecordでは、以下の4つのメソッドを定義しています。トレイトDynamicを拡張したクラスに対して定義されていないメソッドやフィールドへのアクセスが発生すると、これらのメソッドが呼びだされます。

applyDynamic
メソッド呼び出しをインターセプト
applyDynamicNamed
名前付きパラメタを持ったメソッド呼び出しをインターセプト
selectDynamic
フィールドへの参照をインターセプト
updateDynamic
フィールドの更新をインターセプト

それぞれの実装は、Interpolationで受け取った引数を整形して、printlnでコンソールに表示するものになっています。この部分のコードを実装すれば、ActiveRecordも容易に実現できそうです。

実行

それでは、実際に動作させてみましょう。

まずRecordのインスタンスを生成します。

scala> val r = new Record
r: Record = Record@320ba1cc

メソッド呼び出しは以下になります。applyDynamicメソッドが呼ばれているのがわかります。

scala> r.foo("bar", "boo")
applyDynamic(foo)(WrappedArray(bar, boo))

名前付きパラメタのメソッド呼び出しは以下になります。applyDynamicNamedメソッドが呼ばれているのがわかります。

scala> r.foo(x = "bar", "boo")
applyDynamicNamed(foo)(WrappedArray((x,bar), (,boo)))

フィールドの参照は以下になります。selectDynamicメソッドが呼ばれているのがわかります。

scala> r.foo
selectDynamic(foo)
res153: String = foo

フィールドの更新は以下になります。updateDynamicメソッドが呼ばれているのがわかります。

scala> r.foo = 10
updateDynamic(foo)(10)
selectDynamic(foo)
r.foo: String = foo

updateDynamicメソッドの後に、selectDynamicメソッドが呼ばれていますが、これはREPLがrオブジェクトのfooフィールドの内容を表示するために呼び出しているものと考えられます。

注意

updateDynamicメソッドはREPLでは動作していますが、プログラム内で使うと「r.foo = 10」のところが以下のコンパイルエラーになってしまいました。

[error] .../DynamicInvocation.scala:14: value update is not a member of String
[error]     r.foo() = 10
[error]       ^

以下のようなバグがまだ残っているようです。

ノート

applyDynamic, applyDynamicNamed, selectDynamicの各メソッドはここでは仮に返却値の型をAnyにしていますが、これをどうするのかというのが実装上の課題です。

仕様上は「def applyDynamic[T](name: String)(args: Any*): T」というように型パラメータを使うのも可能なので、外部仕様上これをどう見せていくのか、内部実装はどうやっていくのかという所は考えどころになります。

諸元

  • Scala 2.10.0-M7

開発環境の都合で、REPLはScala 2.10.0-M6を使っています。

0 件のコメント:

コメントを投稿