読者です 読者をやめる 読者になる 読者になる

( ꒪⌓꒪) ゆるよろ日記

( ゚∀゚)o彡°オパーイ!オパーイ! ( ;゚皿゚)ノシΣ フィンギィィーーッ!!!

ほとんど使われていないマニアックな機能「事前定義 (Early Definitions)」- Scala Advent Calendar jp 2011 Day 5

このエントリは Scala Advent Calendar jp 2011 の5日目です。

Scalaやってる人なら一度はScala言語仕様に目を通したことがあると思います無いとは言わせない。

この言語仕様を見ると、たまに思いもよらない発見があったりしますが、その中でほとんど利用されているところを見たことがない不遇な機能「5.1.6 事前定義 (Early Definitions)」について書こうと思います*1

事前定義 (Early Definitions)とは?

テンプレートを事前フィールド定義(early field definition)節で始めることができ、それにより スーパー型のコンストラクタがコールされる前に、ある特定のフィールド値を定義できます。
次のテンプレート中で

{ val p1 : T1 = e1 ...
val pn : Tn = en
} with sc with mt1 with mtn {stats}

p1, ..., pn 定義の最初のパターンは事前定義(early difinition)と呼ばれます。 それらはテンプ レートの一部をなすフィールドを定義します。 すべての事前定義は、少なくとも 1 つの変数を 定義していなくてはなりません。

Scala言語仕様

つまり、クラス宣言で、withなどでtraitを宣言する前に、特殊な初期化ブロックを記述することが許されている、というワケですが、 なんだかわかりませんね。実例をみましょう。

よくあるoverride and lazy val問題

ある程度Scalaやるとハマる問題の一つとして、スーパークラスの初期化ブロックから参照されているフィールド、メソッドを初期化ブロック内でオーバーライドするとnullになってしまう問題があります。実例を見ましょう。

trait Train {
  val announce:String
  println( announce )
}

class KQ extends Train {
  val announce:String = "ダァ!! シエリイェッス!!シエリイェッス!!"
}

Trainトレイトは、初期化ブロック内で抽象メンバーannounceをprintlnで出力します。クラスKQは、Trainトレイトを継承してannounceにあの言葉を設定します。これで、KQクラスをnewすると、みんな大好きなあの言葉が出力されるハズです。やってみましょう*2

scala> :paste
// Entering paste mode (ctrl-D to finish)

trait Train {
  val announce:String
  println( announce )
}

class KQ extends Train {
  val announce:String = "ダァ!! シエリイェッス!!シエリイェッス!!"
}

// Exiting paste mode, now interpreting.

defined trait Train
defined class KQ

scala> new KQ
null
res3: KQ = KQ@48bc9f58

アレ、nullになっとるやん。ダァがシエリィエさないじゃないですかー!?なぜぜ?

これは、継承先のTrainの初期化ブロックが呼ばれた後に、KQの初期化ブロックが呼ばれるからです。この問題を解決するには、annouceをlazy valにすれば解決できます。

class KQ extends Train {
  lazy val announce:String = "ダァ!! シエリイェッス!!シエリイェッス!!"
}


lazy valにしてもよいのですが、オーバーライドするメンバーがすでに継承先で具体的な値を与えられている場合は、lazy valでオーバーライドし直すことはできません。\(^o^)/

scala> :paste
// Entering paste mode (ctrl-D to finish)

trait Train {
  val announce:String = ""
  println( announce )
}

class KQ extends Train {
  override lazy val announce:String = "ダァ!! シエリイェッス!!シエリイェッス!!"
}

// Exiting paste mode, now interpreting.

<console>:14: error: overriding value announce in trait Train of type String;
 lazy value announce cannot override a concrete non-lazy value
         override lazy val announce:String = "ダァ!! シエリイェッス!!シエリイェッス!!"

事前定義で解決する

この問題は、事前定義を利用することでシエリイェッス!することができます。KQの宣言を、このように行います。

class KQ extends {
  override val announce:String = "ダァ!! シエリイェッス!!シエリイェッス!!"
} with Train

実行してみましょう。

scala> :paste
// Entering paste mode (ctrl-D to finish)

trait Train {
  val announce:String = ""
  println( announce )
}

class KQ extends {
  override val announce:String = "ダァ!! シエリイェッス!!シエリイェッス!!"
} with Train

// Exiting paste mode, now interpreting.

defined trait Train
defined class KQ

scala> new KQ
ダァ!! シエリイェッス!!シエリイェッス!!
res6: KQ = KQ@49164555


ダァ!! シエリイェッス!!シエリイェッス!!

ダァ!! シエリイェッス!!シエリイェッス!!

ダァ!! シエリイェッス!!シエリイェッス!!

ダァ!! シエリイェッス!!シエリイェッス!!

ダァ!! シエリイェッス!!シエリイェッス!!

ダァ!! シエリイェッス!!シエリイェッス!!

ダァ!! シエリイェッス!!シエリイェッス!!

*1:いちおうコップ本にも書いてある

*2:余談だけどREPLでコンパニオンなどの動作確認したい場合は:paste使うとよい。:pasteで入力した範囲が同一スコープで扱われる