Example8.2

8.2 変位指定アノテーション (Variance Annotations)

型パラメータとサブタイプ化の組み合わせは興味深い問題を引き起こします。たとえば、Stack[String] は Stack[AnyRef] のサブタイプであるべきでしょうか? 直感的には OK のように思われます。String のスタックは AnyRef のスタックの特別な場合だからです。より一般的に言うと、もし T が型 S のサブタイプなら、Stack[T] は Stack[S] のサブタイプであるべきです。この性質は 共変 (co-variant)サブタイプ化と呼ばれます。

Scala では、ジェネリック型はデフォルトでは非変(non-variant)サブタイプ化です。それは、前に定義した Stack については、異なる要素型のスタックは決してサブタイプ関係にないということです。しかし、クラス Stack の定義の一行目を次のように変更すれば、共変サブタイプ化を強制できます。

class Stack[+A] {

形式上の型パラメータの前に + を置くことで、このパラメータについてサブタイプ化が共変であることを示します。+ に加えて、反変サブタイプ化を示す接頭辞 - もあります。もし Stack を class Stack [-T] ... と定義すれば、T が型 S のサブタイプなら、Stack[S] は Stack[T] のサブタイプになります (スタックの場合には驚くべきことでしょう!) 。

純粋関数的な世界では、すべての型は共変となることができます。しかし、もしミュータブル(変更可能)なデータを導入すると状況は変わります。Java や .NET の配列について考えてみましょう。そのような配列は、Scala ではジェネリックなクラス Array で表現されます。次はこのクラスの定義の一部です。

class Array[A] { 
  def apply(index: Int): A 
  def update(index: Int, elem: A) 
} 

このクラスは、Scala のユーザープログラムから Scala の配列がどのように見えるかを定義します。Scala コンパイラはこの抽象化を、基礎となっているホストシステムの配列へ、それが可能なほとんどの場合、マップします。

実際 Java では配列は共変です。つまり、参照型 T と S について、もし T が S のサブタイプなら、Array[T] は Array[S] のサブタイプです。これは自然なことに思えるかもしれませんが、特別な実行時チェックを必要とする安全上の問題をもたらします。次がその例です。

val x = new Array[String](1) 
val y: Array[Any] = x 
y(0) = new Rational(1, 2) // これは次の糖衣構文
                                      // y.update(0, new Rational(1, 2)) 

最初の行で、新しい文字列配列が生成されます。二行目で、この配列は Array[Any] 型の変数 y に束縛されます。配列が共変だということを仮定すると、これは OK です。なぜなら Array[String] は Array[Any] のサブタイプだからです。最後に、最終行で有理数がこの配列に代入されます。これも OK です。なぜなら型 Rational は、配列 y の要素型 Any のサブタイプだからです。そして結局最後には、文字列配列に有理数が格納され、明らかに型健全性を破ります。

Java はこの問題を、三行目に実行時チェックを導入して、格納された要素と配列が作られた要素型との互換性をテストし、解決します。例で見たようにこの要素型は、必ずしも更新される配列の静的な要素型ではありません。テストが失敗すると ArrayStoreException が発生します。

Scala はその代わりに、この問題を静的に解決します。Scala では、配列は非変サブタイプ化なのでコンパイル時に二行目を許可しないことで、解決します。これは、Scala はどうやって変位指定アノテーションの正しさを検証するのか、という疑問を生じさせます。もし単に配列を共変に宣言したら、どうやって潜在的な問題を見つけるのでしょうか?

Scala は変位指定アノテーションの健全性を検証するために、用心深い推定をします。クラスの共変的型パラメータは、クラス内の共変な場所にだけ現れることができます。クラス内の値の型で共変な場所は、クラス内のメソッドの結果型と他の共変的型への型引数です。形式上のメソッドパラメータの型は共変ではありません。したがって次のクラス定義は却下されます。

class Array[+A] { 
  def apply(index: Int): A 
  def update(index: Int, elem: A) 
                                            ^ 共変的型パラメータ A が
                                              反変的位置に現れる。
}

これまでのところは問題ありません。直感的には共変的なクラスの update 手続きを却下する点でコンパイラは正しいです。なぜなら update は状態を変更することができ、したがって共変サブタイプ化の健全性の土台を揺るがすからです。

しかしながら、状態を変更しないメソッドで、型パラメータが反変的な位置に現れるものがあります。例は型 Stack の push です。Scala コンパイラは共変的スタックに対するこのメソッド定義を再び却下します。

class Stack[+A] { 
  def push(x: A): Stack[A] = 
                    ^ 共変的型パラメータ A が
                      反変的位置に表れる。

これは残念です。なぜなら配列と違ってスタックは純粋関数的なデータ構造であり、共変サブタイプ化できるべきだからです。しかしこの問題を、下限境界付きの型パラメータをもつ多相的メソッドで解決する方法があります。


名前:
コメント:

タグ:

+ タグ編集
  • タグ:

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

最終更新:2011年02月24日 08:42
ツールボックス

下から選んでください:

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