Scala 2.9.0でDelayedInitトレイトというものが追加され、インスタンスの初期化シーケンスがカスタマイズできるようになったとか。
Parallel Collectionsとかと違って、あんまり注目されていない機能っぽい?ま、遊ぶだけ遊んでみましょうか、と。
なお、ちょっと前にScalaが2.9.0.1とかいう妙なバージョンをリリースしていたので、一応バージョンアップしておきました。そんなに致命的な問題があったのかね?
まあ、とりあえずサンプルコードを書いてみます。DelayedInitトレイトをMix-inしたクラスを作成して、そのクラスはdelayedInitメソッドを実装するようですよ。
object DelayedInitTest { def main(args: Array[String]): Unit = { val delayed = new Delayed("Hello World") } } class Delayed(value: String) extends DelayedInit { println("Constructor Arg[%s]".format(value)) def delayedInit(x: => Unit): Unit = { println("delayed init") } }
では、コンパイルして実行。
$ fsc DelayedInitTest.scala $ scala DelayedInitTest delayed init
おお、delayedInitメソッドしか実行されてない…。どうも、delayedInitメソッドの引数がコンストラクタのコードブロックっぽいので、こいつを評価すればいいのね。
じゃ、ちょっと修正して実行。
class Delayed(value: String) extends DelayedInit { println("Constructor Arg[%s]".format(value)) def delayedInit(x: => Unit): Unit = { println("delayed init") x } }
$ fsc DelayedInitTest.scala $ scala DelayedInitTest delayed init java.lang.NoSuchFieldError: value at Delayed.value(DelayedInitTest.scala:7) at Delayed$delayedInit$body.apply(DelayedInitTest.scala:8) at scala.Function0$class.apply$mcV$sp(Function0.scala:34) at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12) at Delayed.delayedInit(DelayedInitTest.scala:12) at Delayed.<init>(DelayedInitTest.scala:7) at DelayedInitTest$.main(DelayedInitTest.scala:3) at DelayedInitTest.main(DelayedInitTest.scala) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at scala.tools.nsc.util.ScalaClassLoader$$anonfun$run$1.apply(ScalaClassLoader.scala:78) at scala.tools.nsc.util.ScalaClassLoader$class.asContext(ScalaClassLoader.scala:24) at scala.tools.nsc.util.ScalaClassLoader$URLClassLoader.asContext(ScalaClassLoader.scala:88) at scala.tools.nsc.util.ScalaClassLoader$class.run(ScalaClassLoader.scala:78) at scala.tools.nsc.util.ScalaClassLoader$URLClassLoader.run(ScalaClassLoader.scala:101) at scala.tools.nsc.ObjectRunner$.run(ObjectRunner.scala:33) at scala.tools.nsc.ObjectRunner$.runAndCatch(ObjectRunner.scala:40) at scala.tools.nsc.MainGenericRunner.runTarget$1(MainGenericRunner.scala:56) at scala.tools.nsc.MainGenericRunner.process(MainGenericRunner.scala:80) at scala.tools.nsc.MainGenericRunner$.main(MainGenericRunner.scala:89) at scala.tools.nsc.MainGenericRunner.main(MainGenericRunner.scala)
盛大にエラーを吐いてくれましたが?なんか、valueがフィールドとして見つからないとか言ってるね?んじゃ、明示的にフィールドにすればいいの?
class Delayed(val value: String) extends DelayedInit { println("Constructor Arg[%s]".format(value)) def delayedInit(x: => Unit): Unit = { println("delayed init") x } }
$ fsc DelayedInitTest.scala $ scala DelayedInitTest delayed init Constructor Arg[Hello World]
お、動いた動いた。やっぱり、引数xを評価した時点でコンストラクタの本来のコードが実行されるようですね。んでもって、コンストラクタ引数をコンストラクタコードブロックの中で参照する時には、明示的にフィールドにしなければならないという制約がくっつくっぽいです。というか、実行時エラーになるくらいなら、コンパイルエラーにしてくれと思うのですが、それはおかしいでしょうか?
※他に宣言したメソッドとかが見る分には、問題なさそうです
んじゃ、コンストラクタコードブロックがなかったら?
class Delayed(val value: String) extends DelayedInit { //println("Constructor Arg[%s]".format(value)) def delayedInit(x: => Unit): Unit = { println("delayed init") x } }
$ fsc DelayedInitTest.scala $ scala DelayedInitTest (何も出力されない)
おお?delayedInit自体も評価されなくなっちゃったよ?つまり、DelayedInitトレイトを利用する場合は、コンストラクタコードブロックを持っていないとダメだと。
な〜んかクセが強そうですね、コレ。
今度は、他にフィールドを定義してみましょう。
class Delayed(val value: String) extends DelayedInit { println("Constructor Arg[%s]".format(value)) private val privateField: String = value def delayedInit(x: => Unit): Unit = { println("delayed init") println("PrivateField[%s]".format(privateField)) x println("PrivateField[%s]".format(privateField)) } }
$ fsc DelayedInitTest.scala $ scala DelayedInitTest delayed init PrivateField[null] Constructor Arg[Hello World] PrivateField[Hello World]
追加したフィールドが、コードブロックxを評価する前はまだ初期化されていないのが確認できます。
最後、継承を利用した場合は?
class Base(value: String) { println("Base Constructor Arg[%s]".format(value)) } class Delayed(val value: String) extends Base(value) with DelayedInit { println("Constructor Arg[%s]".format(value)) private val privateField: String = value def delayedInit(x: => Unit): Unit = { println("delayed init") println("PrivateField[%s]".format(privateField)) x println("PrivateField[%s]".format(privateField)) } }
$ fsc DelayedInitTest.scala $ scala DelayedInitTest Base Constructor Arg[Hello World] delayed init PrivateField[null] Constructor Arg[Hello World] PrivateField[Hello World]
なるほど、評価が遅延できるのは自分のコードブロックだけってことですか。
では、親がDelayedInitをMix-inしていた場合は?
class Base(val value: String) extends DelayedInit { println("Base Constructor Arg[%s]".format(value)) def delayedInit(x: => Unit): Unit = { println("delayed init") x } } class Delayed(override val value: String) extends Base(value) { println("Constructor Arg[%s]".format(value)) }
$ fsc DelayedInitTest.scala $ scala DelayedInitTest delayed init Base Constructor Arg[Hello World] delayed init Constructor Arg[Hello World]
delayedInitが2回実行されとる…。
う〜ん、なんか面白い仕組みなんですけど、実際に使うとハマるんじゃね?という気がします。つか、どういう時に使って欲しいんでしょ、コレ。