2012年7月19日木曜日

[Scala] 変位指定: 共変、反変、非変

Scala のコードを見ているとよくこんなコードに当たる。

trait Traversable[+A] extends TraversableLike[A, Traversable[A]] ...

この +A ってなんだろう。ジェネリクスの指定関係だということはわかるのだが。
調べてみたら、共変、反変、非変というジェネリクスの性質を指定するもののようだ。


変位指定


Java では時々こういうコードを書きたくなる時がある。

ArrayList<Object> a;
ArrayList<String> b = new ArrayList<String>();
a = b;

String は Object のサブクラスなんだから問題ないと思うのに、

Type mismatch: cannot convert from ArrayList<String> to ArrayList<Object>

とエラーになってしまう。理不尽だ。
このタイプの変換を許す性質が「共変」というタイプで、ジェネリクスの型パラメータに変位指定アノテーション(variance annotations)を付与して指定する。 Traversable[+A] の「 + 」の部分がこの変位指定アノテーションになる。


共変、反変、非変


変位指定には3つのタイプがある。

 アノテーション  変位型  意味 
 + 共変 (covariant) 型パラメータがサブクラスなクラスはサブクラスとして扱う。
 - 反変 (contravariant)  型パラメータがスーパークラスなクラスはサブクラスとして扱う。
 なし 非変 (nonvariant) 型パラメータが違うクラスはサブクラスとして扱わない。

微妙に意味が違うかもしれない。Scala 本にはもう少し違う言い回しで書いてあるのだがこう理解した。きっとこれでいいのだと思う。

共変の例


traversable は 「 Traversable[+A] 」なので型パラメータ Aに対して共変である。なので、こういうことができる。

var a : Traversable[AnyRef] = null;
var b : Traversable[String] = null;
a = b;

Traversable[String]は、String が AnyRef 型のサブクラスなので、Traversable[AnyRef] のサブ型として扱われる。そのため、Traversable[AnyRef] 型の変数に代入することができる。
逆に、「 b=a 」をやろうとすると、

type mismatch; found : Traversable[AnyRef] required: Traversable[String]

ということになる。List などすべての コレクションは Traversable を実装しており型パラメータに対して同じ性質を持っている。

非変の例


一方でArray。 Arrayは、

final class Array[T](_length: Int) extends java.io.Serializable with java.lang.Cloneable {

と実装されており型パラメータに対して非変である。したがって、Trabersable と同じようなことをしても

var a: Array[AnyRef] = null;
var b: Array[String] = null;
a = b;

type mismatch; found : Array[String] required: Array[AnyRef] Note: String <: AnyRef, but class Array is invariant in type T. You may wish to investigate a wildcard type such as `_ <: AnyRef`. (SLS 3.2.10)

とエラーになってしまう。



変位指定された型パラメータの性質


ここからが本番。正しいかどうか今一つ自信がないが、自分の理解した内容について書いてみる。

変位指定された型パラメータは、メソッド定義における出現箇所に制約がある。

 共変  引数には指定不可。戻り値に指定可能。 
 反変  引数に指定可能。戻り値には指定不可。 
 非変  どこでも可能

意味的にはこんな感じか。
  • メソッドの戻り値が常に同じ型かサブクラスになるようなら、そのクラス自体を自身と同じように(サブクラスとして)扱える。
  • メソッドの引数に常に同じ型かスーパークラスを与えるなら、そのクラス自体を自身と同じように(サブクラスとして)扱える。
このルールを守らないと、例えば、

covariant type R occurs in contravariant position in type R of value r

といったエラーになる。 これは共変な型パラメータを引数に使用した場合で、共変な型 R が反変な所に出現していることを示している。

というわけでやってみよう。まずは共変。

object VarianceSample {
  def main(args : Array[String]) {
    var covariantStr = new Covariantable[String];
    var covariantAny : Covariantable[AnyRef] = null;
    covariantAny = covariantStr;
    var any : AnyRef = covariantAny.hoge();
  }
}
class Covariantable[+R] {
  def hoge(): R = null.asInstanceOf[R];
}


Covariantable は 型パラメータ R に対して共変なので、Covariantable[AnyRef] な変数 covariantAny に Covariantable[String]な値を代入することができる。
Covariantable[String] は hoge() 呼び出しに対して、AnyRef のサブクラスである String を返すので、 Covariantable[AnyRef] だと思って、AnyRef 変数で戻り値を受けても問題がない。

今度は反変。

object VarianceSample {
  def main(args : Array[String]) {
      var contraStr : Contravariantable[String] = null;
      var contraAny = new Contravariantable[AnyRef]();
      contraStr = contraAny;
      contraStr.hoge("hoge");
  }
}
class Contravariantable[-T] {
  def hoge(t : T) {};
}

Contravariantable は 型パラメータ T に対して反変なので、Contravariantable[String] な変数 contraStr に Contravariantable[AnyRef] な値を代入することができる。
Contravariantable[AnyRef] は hoge 呼び出しの引数に AnyRef を受け取れるので、 Contravariantable[String] だと思って、String の値を渡しても問題がない。

うーむ。なんとなくわかったような気がしないでもない。


共変な型パラメータを引数に受け取りたいとき


そうは言っても、List.indexOf 等のように共変な型パラメータで指定された型を引数の型に使いたいときはある。そういう場合は、そのメソッド用に型パラメータを導入して型境界を指定する。 たとえば、GenSeqLike.indexOf はこんな実装になっている。

  def indexOf[B >: A](elem: B): Int = indexOf(elem, 0)

つまり、「(クラスの)型パラメータ A で指定された型と同じかそれより下位クラスである B が引数の型になる。」という記述である。
実際、Scala の実装コードを見ていると(特に、Collection 周りでは)このような実装がいたるところに出現していて、意味が解らないと何が起こっているのか理解に苦しむことになる。


どうも、なんかいろんなところに無理があるような気がしてならないのだが・・・・


2 件のコメント:

  1. s/変異/変位/ ですね ( 重箱でスミマセン><

    返信削除
    返信
    1. ほんとだ。直しておきました。

      削除