CLOVER🍀

That was when it all began.

Scala 2.9.0で追加された、DelayedInitトレイトを使ってみる

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回実行されとる…。

う〜ん、なんか面白い仕組みなんですけど、実際に使うとハマるんじゃね?という気がします。つか、どういう時に使って欲しいんでしょ、コレ。