sid-earlydef-a

事前初期化メンバー定義 (Early Member Definitions)

Copyright c 2008-2009, Ingo Maier & Anders Bach Nielsen

抄録 (Abstract)


このドキュメントは、ミックスイン合成時のオブジェクト初期化という難題に対処する、事前初期化子(early initializers)と抽象事前初期化メンバー(abstract early members)に関する完全なセマンティクススを提案します。

目次


1 モチベーション (Motivation)
  • 1.1 目標 (Goals)
  • 1.2 構文変更 (Syntax change)
    • 1.2.1 抽象事前初期化メンバー (Abstract early members)
    • 1.2.2 コンテキスト (Context)
    • 1.2.3 コンストラクタパラメータとの比較 (Comparison to constructor parameters)
    • 1.2.4 クラス中の抽象事前初期化メンバー (abstract early members in classes)
    • 1.2.5 初期化の順番 (Initialization Order)
    • 1.2.6 オーバーライド解決 (Overriding resolution)
    • 1.2.7 未解決問題 : メンバー昇格 (Open question :Member promotion)
  • 1.3 この提案書の範囲を越えた将来の目標 (Future goals that extend beyond this proposal)

2 言語仕様変更 (Language specification changes)
  • 2.1 SLS 5.1 節
    • 2.1.1 SLS 5.1.1 節
    • 2.1.2 SLS 5.1.3 節
    • 2.1.3 SLS 5.1.4 節
    • 2.1.4 SLS 5.1.5 節
    • 2.1.5 SLS 5.1.6 節
  • 2.2 SLS 5.2 節

3.実装 (Implementation)


1 モチベーション (Motivation)


Scala では、クラス C のメンバーの初期化順は、 C のサブクラスと異なることがあります。それは、クラス線形化とオーバライドの順番に依存します。あるクラス中のフィールド宣言で、その初期値が同じクラス中の他の(非 lazy、非 final)フィールド値に依存するものについて考えてみます。クラスの文脈によって、参照されるフィールドは使われる前に既に初期化されているか、あるいは、別の文脈ではそうでないかもしれず、最終的に予期しない初期化値あるいは NullPointerExceptions をもたらすかもしれません。

初期化順をもっと分かり易くするために、事前初期化定義(early definition)が導入されました。それらは、他の非事前初期化フィールドより前に初期化されるものとして、ある特定のフィールドに旗を立てるのに使用できます。トレイトはコンストラクタ・パラメータを持っていませんから、事前初期化フィールド(early initialized fields)の一般的な使用ケースは、トレイト内の他のフィールドに対するパラメータとしての使用です。ある特定のフィールドを常に早く初期化することは、現在はできません。メンバー初期化子(member initializers)中でフィールドを使うクラスあるいはトレイトに関して、メンバーが使われる前に初期化されるという保証は、一般にそのサブクラスにまかされています。我々はまさにこの目的に対して抽象事前初期化フィールド(abstract early fields)の導入を提案します。それらはトレイトのクラスコンストラクタパラメータの代役を務め、そのセマンティクスはこのただ一つの用途で広く使われます。

さらに、Scala 言語仕様書では、複数の基底トレイトからの事前初期化定義がどういう順番で評価されるか曖昧です。我々は、事前初期化メンバーが現れるすべての状況をカバーする、事前初期化定義のより正確なセマンティクススを提案します。

この章の残りは、この提案書の意欲的な目標をカバーし、それら目標を際立たせる使用ケースとこの提案書を越える将来の目標に関する節を含みます。


1.1 目標 (Goals)


  • Scala 言語における事前初期化定義ブロックのセマンティクススを提示します。
  • 事前初期化定義ブロック中の抽象メンバーを含め、this を扱えるようにセマンティクススを拡張します。
  • クラス階層構造中の事前初期化定義ブロック中の具象および抽象メンバーの評価順を記述します。
  • SLS (Scala 言語仕様書)に必要な変更を提示し、事前初期化定義ブロック中の事前初期化定義と抽象メンバーの正確な記述を与えます。
  • ここで提案された変更のサンプル実装を提示します。


1.2.構文変更 (Syntax change)


次は、現在の SLS からとった事前初期化定義の例です。:

 trait T {
   val name:String
   val msg = "How are you, " + name
 }
 class C extends {
   val name = "Bob"
 } with T {
   println(msg)
 }

msg が初期化される前に、メンバー name が正しく初期化されることを保証するために、クラス C は事前初期化定義中で name を定義します。

まず初めに、構文の変更を提案します。現在の構文には少し驚きます。なぜなら、with の後にクラスを書ける可能性を導入しているからです。以前は、クラス定義中で with キーワードの後にはトレイトだけが許されていて、extends キーワードの後にただ一つの直接の基底クラスを書く必要がありました。さらにまた現在の構文は、無名クラスのインスタンス化中では、広範囲の先読みが潜在的に必要とされます。次の 2 つのインスタンス化を考えてみます。:

 new { val x = 2 } with A
 new { val x = 2 }

最初のは、事前に初期化されたメンバー x を用いて型 a のオブジェクトをインスタンス化し、他方 2 つめは、構造型のインスタンスを生成します。

改訂された構文では、上例は次のようになります。:

 trait T {
   val name:String
   val msg = "How are you, " + name
 }
 class C extends T {
   val name = "Bob"
   super
   println(msg)
 }

今度は、事前初期化定義は通常のメンバー宣言と同じブロックを共有します。しかし、キーワード super によって分離されており、それはスーパーコンストラクタを呼び出す場所を示します。しかしこれは、Java におけるような実際の super 呼び出しではありません。テンプレート中に super が現れなければ、すべてのメンバーが事前ではないと仮定されます。


1.2.1 抽象事前初期化メンバー (abstract early members)


上記の構文変更に加え、はじめに言及した問題を扱うために抽象事前初期化定義を含めることを提案します。抽象事前初期化メンバーは、サブクラスの事前初期化定義ブロックで実装されなくてはなりません。前節の例は、今度は、次のように書き直せます。:

 trait T {
   val name:String
   super
   val msg = "How are you, " + name
 }
 class C extends T {
   val name = "Bob"
   super
   println(msg)
 }

事前初期化定義は、同じテンプレート中の他のすべての非事前初期化定義の前に評価されるので、トレイト T 内でメンバー name は常に msg 定義中で使用される前に初期化される、と仮定できます。


1.2.2 コンテキスト (Context)


現在は、事前初期化定義中の this への参照は、実際にはテンプレートのすぐ外側の this を参照します。抽象事前初期化定義と提案された構文変更により、このスコープ規則は、次例で示すような驚くべき結果をもたらします。

 val name = "Zaphod"
 trait T {
   val name: String
   super
 }
 trait S extends T { this: S =>
   val msg = "What's up, " + this.name
   super
   println("msg");
 }
 new S { val name = "Doc"; super }     // "What's up, Zaphod" と印字

実際、既存のスコープ規則では、トレイト S 中の this.name はトレイト T の定義の直前に定義された値を参照します。事前初期化定義 ed の右辺が this 参照を通して構築中のオブジェクトを実際に参照できるが、同じテンプレート中の ed の前に定義されたメンバーにのみアクセスできるように、規則の変更を提案します。

上記ルールの結果、:もし基底トレイトからの事前初期化メンバーが具象の事前初期化定義のために必要なら、それは繰り返されなければなりません。このことを次例で示します。:

 trait T1 {
   val name: String
   super
 }
 trait T2 extends T1 {
   val name: String    // 省略すると、次で name が未定義となる
   val msg = "What's up, " + name + "?"
   super
 }


1.2.3 コンストラクタパラメータとの比較

(Comparison to constructor parameters)

抽象事前初期化メンバーは本質的に、トレイトのクラスコンストラクタパラメータとして役に立ちます。次の例は両方とも正しく、クラス D とトレイト S は同じ順番でそれらのメンバーを初期化します。:

 class D(val name: String) extends T {
   val msg = "What about " + name + "?"
   println(msg)
 }
 new D("Bob")
 
 trait S extends T {
   val name: String
   super
   val msg = "What about " + name + "?"
   println(msg)
 }
 new S { val name = "Bob"; super }


1.2.4 クラス中の抽象事前初期化メンバー

(Abstract early members in classes)

抽象クラス中の場合と同様に、具象クラス中の抽象事前初期化メンバーも文法違反です。コンパイラは、すべての抽象事前初期化メンバーに対する引数を追加で受け取るために、クラス中のすべてのクラスコンストラクタを抽象事前初期化定義を使って修正する必要があるでしょう。突き詰めると、クラスは、事前初期化メンバーと同じ目的で使えるコンストラクタ引数を既に持っています。クラスコンストラクタ引数は、テンプレートの事前初期化定義部のスコープ内にありますから、上の例が示すように、ユーザーは事前初期化メンバーからコンストラクタへ容易に乗り替えできます。


1.2.5 初期化の順番 (Initialization Order)


SLS は現在、クラス中の事前初期化定義はスーパークラスのコンストラクタが呼び出される前に評価される、と定義しているだけです。複数の基底トレイトからの事前初期化定義が評価される順番を定義していません。次の例を考えてみます。:

 trait T1 {
   val name: String
   val msg = "What's up, " + name + "?"
   super
 }
 trait T2 {
   val name: String = "Doc"
   super
 }
 class C extends T1 with T2

これを意図した通りに動作させるには、T2 の name は T1 メッセージの前に初期化されなくてはなりません。

We propose a simple solution that is in line with the order of class constructor invocations, which are called bottom-up in a class's linearization. We define the preinitialization of a trait as the evaluation of the early definitions in its template in the order they are defined. A class preinitializes its base traits bottom-up in class linearization order up to the first base class B before it calls its super class (B!) constructor. Since the linearization of C above is [C,T2,T1] bottom-up, the above now works as intended.
クラスコンストラクタ起動の順番 --- クラス線形化中でボトムアップと呼ばれる --- を用いて一直線に並べる、単純な解決を提案します。我々はトレイトの事前初期化を、テンプレート中でそれらが定義された順番で事前初期化定義を評価する、と定義します。クラスは、クラス線形化順のボトムアップでその基底トレイトを事前初期化します。その順番は、最初の基底クラスを B とすれば、そのスーパークラス(B!)コンストラクタを呼び出す前に、B の事前初期化を行います。上記 C の線形化はボトムアップ [C,T2,T1] ですから、今度は意図したとおりに動作します。しかし、次を考えてみます。:

 class C1 extends T2 with T1

T1's msg would be initialized before name. We disallow the above example, by adding the following rule. For an early definition ed in a class T extended by a concrete class C, every early member refered to in ed's right hand side must be implemented in a class T1 (possibly C) that is below T in C's linearization. Note that there does not exist such a rule for non-early members: an abstract member x in a base class T can be implemented by both a class T1 that comes below or above T in the concrete class's linearization.
T1 の msg は name の前に初期化されます。我々は、次のルールを加えることで、上例を許しません。具象クラス C によって拡張されたクラス T 中の事前初期化定義 ed について、ed の右辺中で参照されるすべての事前初期化メンバーは、C の線形化中で T より下位の T1 (Cでも可) クラス中で実装されなくてはなりません。非事前初期化メンバーに対してはこのようなルールが存在しないことに注意してください。:基底クラス T 中の抽象メンバー x は、T より下位の T1、あるいは具象クラスの線形化中で T の上位にくるクラス T1 の両方で実装できます。


1.2.6 オーバーライド解決 (Overriding resolution)


事前初期化メンバーをオーバライドすることは簡単です。次を考えてみます。:

 trait T1 {
   val name: String = "Doc"
   val msg = "What's up, " + name + "?"
   super
   println(msg)
 }
 trait T2 extends T1 {
   override val name: String = "Bob"
   super
 }
 class C extends T1 with T2   // "What's up, Bob?" と印字

ここで、T1 からのメンバー name は T2 によってオーバライドされます。オーバーライド解決は通常のクラスメンバに対するのと同じです。 質問 : オーバライドされた初期化指定子を評価しますか? プログラマが初期化指定子中の副作用を当てにできるようになるので、イエスの答えが多いでしょう。この SIP の残りでは、我々はイエスと仮定します。事前初期化クラスメンバーの完成。

しかし、加えなければならない付加的なチェックが 1 つあります。次の場合を考えてみます。:

 trait T1 {
   val name: String
   super
 }
 trait T2 extends T1 {
   override val name: String = "Bob"
   super
 }
 class C1 extends T1 {
   val name: String = "Doc"
   super
 }
 class C2 extends C1 with T2

これまで議論したことから、T2 が C1 からの name をオーバライドするので、C2 中の メンバー name は "Bob" に初期化されると仮定できます。残念なことに、この順番を保証する適切な実装は見当たりません。C2 のコンストラクタは最初に T2 を事前初期化し、次に C1 のコンストラクタを呼び出します。それは今度は、メンバー name を "Doc" に設定します。我々は C1 に name を再び設定しないように知らせる必要がありますが、それは現実的ではないと思われます。その代わりに、オーバーライド規則を 1 つ加えます。: 事前初期化定義は、(非トレイトの)スーパークラスからの他の事前初期化定義をオーバライドできない。このルールは基本的に、クラスのすべての事前初期化メンバーは自動的に final である、と述べています。


1.2.7.未解決の問題 : メンバー昇格

(Open question : Member promotion)

原理上は、非事前初期化メンバーは、サブクラス中で事前初期化メンバーへ昇格させることができます。逆は不可です。

 trait T1 {
   val name: String = "Doc"
   super
   val msg = "What's up, " + name + "?"
 }
 trait T2 extends T1 {
   // 正しい: 以後、安全に以下を使える
   override val msg: String = "What about me?"
   super
   println("I have " + msg.length + " letters for you.")
 }
 trait T3 extends T1 {
   // 正しくない:  msg の前に name が初期化される保証がない
   override
   val name: String = "Bob"
 }

後のケースは、サブクラス化原則に違反しますから、当然です。このことは、サブクラス中のメンバーに新しい制約を課します。

The first case is allowed for the opposite reason: an early member can be accessed in the same way as a non-early member but additionally in other early definitions. This is in line with class constructors that can turn class members into class parameters .
最初のケースは、逆の理由で許されます。: 事前初期化メンバーは、他の事前初期化定義中で追加されたもの以外は、非事前初期化メンバーと同じ方法でアクセスできます。それはクラスコンストラクタの直線的な並びの中にあり、クラスメンバをクラスパラメータのように使うことができます。
残念なことに、現在、JVM 上でこれを実行可能な実装は見当たりません。ここでオーバーライド解決が問題となります。現在の実装方式(下記参照)とトレイトコンストラクタ($init$ メソッド)の作成方法のため、上記 T2 中の msg フィールドは T1 の init メソッドによって事前初期化された後に上書きされます。これをうまく動作させるために、現在のテンプレート初期化方式から、次に記述したものと似たような方法に移る必要があります。トレイト実装クラスは、(ただ 1 つではなく)フィールド毎に 1 つの init メソッドを持つようにする。クラスコンストラクタは正しい順番で init メソッドを呼び出し、必要な場所で割り当てを行う責任を持つ。 たとえば、線形化された init 呼び出しからの副産物として得る代わりに、自分でオーバライド解決を実行する。


1.3.この提案書の範囲を越えた将来の目標

(Future goals that extend beyond this proposal)

  • トレイトのコンストラクタパラメータ
  • 事前初期化定義ブロック中のメソッド定義と型パラメータ


2 言語仕様変更 (Language specification changes)


本章では、この提案書に起因する SLS との差分をすべて記述します。変更は 5 章に制限されます。

2.1 SLS 5.1 節


   ClassTemplate     ::= ClassParents [TemplateBody]
   TraitTemplate     ::= TraitParents [TemplateBody]
   TemplateBody      ::= [nl] '{' [SelfType] [EarlyDefs] TemplateStat
                         {semi TemplateStat} '}'
   EarlyDefs         ::= EarlyDef {semi EarlyDef} 'super' semi
   EarlyDef          ::= {Annotation} {Modifier} PatVarDef
                       | AbsEarlyDef
   AbsEarlyDef       ::= {Annotation} {Modifier} 'val' ValDcl
                       | {Annotation} {Modifier} 'var' VarDcl

テンプレートは、トレイトあるいはクラス、オブジェクトあるいはシングルオブジェクトの振る舞いと初期状態、型シグニチャを定義します。テンプレートはインスタンス生成式、クラス定義とオブジェクト定義の一部を形成します。テンプレート sc with mt1 with ... with mtn { earlydefs super stats } は、テンプレートのスーパークラスを定義するコンストラクタ呼び出し sc、テンプレートのトレイトを定義するトレイト参照 mt1 ... mtn (n >= 0)、事前初期化定義シーケンス earlydefs、テンプレートの追加のメンバー定義と初期化コードを含む文並び stats 等から成ります。

Template Evaluation gets a new first point:
The preinitialization of a trait consists of the evaluation of its early definition sequence .
New first point: All base traits in the template's linearization up the template's super class denoted by sc are preinitialized. Preinitialization happens in linearization order, bottom-up, i.e., trait mtn gets preinitialized first .
Then, the superclass constructor sc is evaluated .
テンプレート評価(Template Evaluation)は新提案です。:
トレイトの事前初期化は、その事前初期化定義シーケンスの評価から成ります。
新提案: テンプレートの(sc によって表される)スーパークラスへ至る、テンプレート線形化中のすべての基底トレイトは事前初期化されます。事前初期化は線形化順、ボトムアップに起きます。すなわち、トレイト mtn が最初に事前初期化されます。
次にスーパークラスのコンストラクタ sc が評価されます。

2.1.1 SLS 5.1.1


この節はまったく変わりません。

2.1.2 SLS 5.1.3


クラスメンバについての節は、事前初期化メンバーに関することと、「普通の」メンバーと事前初期化メンバーの連携方法を含まなければなりません。

2.1.3 SLS 5.1.4


事前初期化定義に関して、次のオーバーライドに関する制限が適用されます。
  • M と M'は共に事前初期化メンバーであるか、あるいは共に非事前初期化メンバーでなければなりません。
  • もし M がオーバーライドする事前初期化メンバーなら、オーバライドされたメンバー M'は非トレイトの基底クラスのメンバーであってはなりません。

2.1.4 SLS 5.1.5


この節はまったく変わりません。

2.1.5 SLS 5.1.6


この節は削除され、上で示したように、直接 5.1 に組み込まれます。


2.2 SLS 5.2 節


我々は遅延評価事前初期化メンバー(lazy early members)を拒否する必要があります。質問: 事前初期化変数(early vals)を伴う遅延評価 val (lazy vals)をオーバライドできますか? 原理的には、イエス(Ingo)でしょう。しかし実装はどうでしょうか?

lazy 修飾子は非事前初期化変数定義に適用されます。・・・


3 実装 (Implementation)


事前初期化子は、各トレイト T と

   val x = e

の形の各具象の事前初期化定義に対して、次の形の静的な事前初期化子メソッドを T の実装クラス T$に 1 つ加えます。:

   def $preinit$x(args) = e

ここで args は、すべての事前初期化メンバーと e 中で参照されるコンストラクタ・パラメータからなるパラメータリストです。

クラス C 中の各クラスコンストラクタに対して、スーパークラスコンストラクタを呼び出す前に、拡張されたトレイトの事前初期化子メソッドを呼び出すシーケンスを加えます。次の形のテンプレートをもつクラス C と、

   sc with Ti ... Tn

線形化 [C,Tn,...,Ti,...T1,D,...] (ここで、D はスーパークラスコンストラクタ呼び出し sc によって表されるクラス) に対して、事前初期化子の呼び出しシーケンスは次のようになります。

 val   vn1 = Tn.$preinit$en1(argsn1)
 ...
 val   vnm = Tn.$preinit$enm(argsnm)
 ...
 val   v11 = T1.$preinit$e11(args11)
 ...
 val   v1p = T1.$preinit$e1p(args1p)

ここで eij はトレイト Ti 中の j 番目の事前初期化メンバーであり、argsij は事前初期化メンバーと eij の各定義が依存するコンストラクタパラメータです。eij のいくつかは同じメンバーを表しているかもしれません。それらはオーバライドされたメンバーを表します。実際には、単にそのようなメンバーの最初の事前初期化子に関する処理結果をセーブするだけです。preinit 呼び出しの後、preinit 呼び出しの結果を最終的にそれら各メンバーに割り当てるリストを加えます。:

 en1 = vn1
 ...
 e1p = v1p

オーバーライド解決の順番のせいで、preinit にそれら自身の値を割り当てさせることはできないことに注意してください。さらに、それらは初期値を返さなければなりません。なぜなら、JVM は、スーパークラスコンストラクタが呼び出される前に、クラスメンバを読むことを許さないからです。

タグ:

+ タグ編集
  • タグ:

このサイトはreCAPTCHAによって保護されており、Googleの プライバシーポリシー利用規約 が適用されます。

最終更新:2011年05月02日 09:36
ツールボックス

下から選んでください:

新しいページを作成する
ヘルプ / FAQ もご覧ください。