hogepiyoエンジニアリング

トラブルシューティングからノウハウ、作ってみた系まで扱う情報系ブログ

Scalaで学ぶ共変と反変

最近コップ本を読みながらScalaをちょろちょろ勉強しているんですが、共変と反変のところで頭がこんがらがってきたので頭の整理の意味もこめて記事にします。

こちらがまとまっていて非常に助かりました。qiita.com
基本的にこちらの記事を参照すればいいと思うけど、反変は戻り値に使えないとか共変は引数に使えないという部分が理解するのに時間かかったので具体的なソースコードを交えてまとめます。

まず具体的なソースコードはこちら。
コンパイルエラーが出ます。

class Creature

class Animal extends Creature

class Cat extends Animal

// +Tが共変、-Uが反変の意味
class Container[+T, -U] {

  //引数でエラー
  def f(arg: T): T = {
    new T()
  }

  //戻り値でエラー
  def f2(arg: U): T = {
    new T()
  }
  
  //戻り値でエラー  
  def f3(): U = {
    new U()
  }

}

さて、なんでこれでコンパイルエラーになるのかっていう話ですね。

共変を引数にできない話

f関数から見て行きましょう。
これは引数でエラーが出ています。

変数cの左辺の型はContainer[Animal, Animal]です。
Tは共変であるから、左辺のAnimal型に対して右辺をAnimalかCatで指定できます。
しかしCat型で定義した場合に、Containerクラスのf関数の引数がCat型を受け入れるのに対し、変数cがContainer[Animal, Animal]型であるためf関数にAnimal型を渡せてしまいます。
ここで、Cat型にAnimal型は入れられないので矛盾が発生します。
そのため共変は引数に使えません。

def main(args: Array[String]): Unit = {
  val c: Container[Animal, Animal] = new Container[Cat, Animal]()
  c.f(new Animal()) //Cat型の定義にAnimal型を渡せてしまう
}

//共変の型にCatを指定した場合のContainer定義
class Container[Cat, Animal] {
  //f2, f3関数は省略

  def f(arg: Cat): Cat = { //引数の型がCat型
    new Cat()
  }
}
共変を戻り値にできる話

f2関数を見ます。
これは引数も戻り値もエラーは出ていません。

変数cの左辺の型はContainer[Animal, Animal]であるから、戻り値の型はAnimal型です。
共変の場合、右辺として許される型はAnimalかCatなので戻り値として許される型もAnimal型かCat型になります。
これは左辺と矛盾しません。
よって共変は戻り値として使えます。

def main(args: Array[String]): Unit = {
  val c: Container[Animal, Animal] = new Container[Cat, Animal]()
  val a: Animal = c.f(new Animal()) //共変のため戻り値としてはAnimalかCatしかありえないのでAnimal型変数aで受け取れる。
}

//TにCatを指定した場合のContainer定義
class Container[Cat, Animal] {
  //f, f3関数は省略

  def f2(arg: Animal): Cat = {
    new Cat()
  }
}
反変を戻り値にできない話

f3関数を見てみましょう。
これは戻り値でエラーが出てます。

Uは反変ですから、左辺のAnimal型に対して右辺をAnimalかCreatureで指定できます。
変数cの左辺はAnimal型が指定されているので「戻り値としてAnimal型が返ってきますよ」と定義されています。
しかし、右辺としてCreature型を指定した場合、戻り値がCreature型になってしまうのでこれは左辺と矛盾します。
よって反変は戻り値として使えません。

def main(args: Array[String]): Unit = {
  val c: Container[Animal, Animal] = new Container[Animal, Creature]()
  val a: Animal = c.f3()
}

//UにCreatureを指定した場合のContainer定義
class Container[Animal, Creature] {
  //f, f2関数は省略

  def f3(): Creature = {
    new Creature()
  }
}
反変を引数にできる話

f2関数を見ます

左辺の型がAnimalなので、引数として渡される可能性があるのはAnimal型かCat型です。
反変なので、右辺のジェネリック型としてCreature型かAnimal型を指定できます。
つまり「Creature型かAnimalを引数にとるように定義されている関数に対して、Animal型かCat型を渡して実行することができる」ということなので、これは矛盾しません。
よって反変の型は引数に指定できます。

def main(args: Array[String]): Unit = {
  val c: Container[Animal, Animal] = new Container[Animal, Creature]()
 c.f2(new Animal())
  c.f2(new Cat())
}

//UにCreatureを指定した場合のContainer定義
class Container[Animal, Creature] {
  //f関数は省略

  def f2(arg: Creature): Creature = {
    new Creature()
  }
}