Edited at

実際にあった scala.App の怖い間違い (解説編)

More than 1 year has passed since last update.

まだの方は 実際にあった scala.App の怖い間違い からご覧ください。


何が変なの?

最後のコードを repl で動かしてみます。

scala> :paste

// Entering paste mode (ctrl-D to finish)

trait SuperMain extends App {
def printClassName(): Unit

println("Start")
printClassName()
println("Finish")
}
object Main1 extends SuperMain {
def printClassName(): Unit = println("Main1")
}
object Main2 extends SuperMain {
def printClassName(): Unit = println("Main2")
}

// Exiting paste mode, now interpreting.

defined trait SuperMain
defined object Main1
defined object Main2

scala> Main1.main(Array())
Start
Main1
Finish

ちゃんと動いている様に見えます。ここで続けてもう一度 Main1.main(Array()) を実行してみます。

scala> Main1.main(Array())

// なにも表示されない!?

2度目以降の main() 呼び出しでは何も表示されません。 SuperMain にまとめる前の次のコードでは動いていたのに。

scala> :paste

// Entering paste mode (ctrl-D to finish)

object Main1 extends App {
def printClassName(): Unit = println("Main1")

println("Start")
printClassName()
println("Finish")
}

object Main2 extends App {
def printClassName(): Unit = println("Main2")

println("Start")
printClassName()
println("Finish")
}

// Exiting paste mode, now interpreting.

defined object Main1
defined object Main2

scala> Main1.main(Array())
Start
Main1
Finish

scala> Main1.main(Array()) // 2度目も動く
Start
Main1
Finish

SuperMain で共通化したコードでは、実は Main1 object を参照したタイミングで println() が実行されています。 main() を実行しても何も起こりません。


なぜそうなるか?

なぜ main() 呼び出し時に println() が実行されないか探るために、 App.scala を見てみましょう。注目したい点だけを抜き出すと次のようになっています。

trait App extends DelayedInit {

private val initCode = new ListBuffer[() => Unit]

override def delayedInit(body: => Unit) {
initCode += (() => body)
}

def main(args: Array[String]) = {
// ...
for (proc <- initCode) proc()
// ...
}
}

DelayedInit を継承し、 delayedInit()initCode に処理をあつめ、 main() で実行しています。

Scala言語仕様 5.1 の Delayed Initialization に DelayedInit の説明があります。


The initialization code of an object or class (but not a trait) that follows the superclass constructor invocation and the mixin-evaluation of the template's base classes is passed to a special hook, which is inaccessible from user code. Normally, that hook simply executes the code that is passed to it. But templates inheriting the scala.DelayedInit trait can override the hook by re-implementing the delayedInit method,


「object または class の初期化コード (ただし trait は除く)は、」 という書き出しが目に止まったでしょうか?

そうです。trait の初期化コードは delayedInit() には渡されない。 delayedInit() に渡されなければ、App.scalainitCode に追加されることもなく main() 呼び出し時に実行されることもありません。


どうすれば良かったか?


似たような処理が複数箇所にあるのは良くないので、共通の親クラス(SuperMain)にまとめます。


と言いながら trait SuperMain にしたのが原因です。 class なら大丈夫なはずです。試してみましょう。

scala> :paste

// Entering paste mode (ctrl-D to finish)

// trait ではなく class にする
abstract class SuperMain extends App {
def printClassName(): Unit

println("Start")
printClassName()
println("Finish")
}
object Main1 extends SuperMain {
def printClassName(): Unit = println("Main1")
}
object Main2 extends SuperMain {
def printClassName(): Unit = println("Main2")
}

// Exiting paste mode, now interpreting.

defined class SuperMain
defined object Main1
defined object Main2

scala> Main1.main(Array())
Start
Main1
Finish

scala> Main1.main(Array()) // 二度目も実行される
Start
Main1
Finish

怖いですね。こんなこと気にしなければいけないくらいなら、自分は extends App の代わりに def main(args: Array[String]): Unit = {} と書きます。たいしてタイプ数変わらないしね。