共変・反変・上限境界・下限境界の関係性まとめ

  • 91
    いいね
  • 0
    コメント

はじめに

ScalaはJavaと違って共変・反変が定義できるため、型パラメータの取り扱いが一段と複雑になっている(なおJavaでは配列のみ共変の振る舞い)。
Scaladocを読んでは[+A]、[-B]、[A1 >: A]、[B1 <: B]のようなものを華麗にスルーする毎日にさよならをつげるためにまとめてみた。
※なお、共変・反変・上限境界・下限境界とはなにか、どんな効能があるのかについては事前に知っているものとし細かい説明は省略している。

とりあえず先にまとめておくと、これを覚えておけばScalaにおける型パラメータを使ったプログラムにおいて困ることはだいぶ減ると思う。
以降でそれぞれの理由を見ていくことにする。

変位 指定可能位置 制約 制約突破条件
共変[+T] 主に戻り値 引数型に使用出来ない 下限境界定義により設定可能 [T1 >: T]
非変[T] どこでも 共変や非変のような代入が出来ない 無し
反変[-T] 主に引数 戻り値型に使用出来ない 上限境界定義により設定可能 [T1 <: T]

なお、説明のために以降では以下のクラスを使うこととする。

class Creature
class Animal extends Creature
class Cat extends Animal

反変と共変の制約

なぜ反変定義の型は戻り値型に使用できないのか

上記のCreature - Animal - Catといったクラスを取りうるコンテナクラスとして以下のものを用意した。

class Container[-T]

ここでContainer[-T]の型Tが戻り値型に指定できるとするとどういう問題が起こるかを考えてみる。

その前に当たり前すぎるかもしれないがひとつ確認しておきたいことがある。
仮にこのような定義の関数があったとする。

def foo(x: Animal): Animal

この場合、引数および戻り値について以下の二点の事が言える。

  • 【原則1】 引数xの型はAnimalなのでAnimal型のインスタンスかCat型のインスタンスを渡すことが可能。間違ってもCreature型インスタンスを渡すことは出来ない。
  • 【原則2】 戻り値の型はAnimalなので実際にnewして返すことが出来るのはAnimal型のインスタンスかCat型のインスタンス。間違ってもCreature型インスタンスを返すことは出来ない。

この当たり前過ぎることを踏まえた上で、反変の続きに戻ることとする。

val c: Container[Animal] = new Container[Creature]が出来るのが反変だったので表向き、利用者からはTはAnimal型に見えるが実体はCreatureである。
戻り値にTが使えるとすると実体の方からはCreatureインスタンスが返ってくることとなるが、表向きはAnimal。ここで型の矛盾を起こしてしまう(【原則2】に違反する)。これが反変に指定された型が戻り値に使用できない理由である。

一方引数はというと、利用者からはAnimalに見えているので引数に渡ってくる可能性があるのはAnimalとCat、しかし実体はCreatureなので当然それら2つはCreatureのサブクラスであり問題なく受け付けることが出来る。
よって反変は戻り値型には指定できないが引数型には指定できるということになる。

なぜ共変定義の型は引数型に使用できないのか

基本的には反変の説明の逆となる。

val c: Container[Animal] = new Container[Cat]が出来るのが共変だったので表向き、利用者からはTはAnimal型に見えるが実体はCatである。

先ほど同様以下のコードで考えてみると

def foo(x: Animal): Animal

引数にT(今回はAnimal)が使えるとすると、表向きはAnimalに見えているので利用者が渡してくる可能性があるのはAnimalとCat。
しかしTの実体はCatなのでAnimalが渡された場合に対応できない(【原則1】に違反する)。

一方、戻り値の方は、Animal型なので許容できるのはAnimalかCat。今回はCatが返ってくるがAnimalとして利用者側は受け取っても何の問題もない。

上限境界と下限境界

なぜ上限境界を設定すると反変定義した型が戻り値で使えるようになるのか

先に定義を書いておくと、こうすれば使えるようになる。

class Container[-T] {
  def foo[T1 <: T](x: T1): T1 = x
}

【原則2】に則ると、戻り値にはTの型およびそのサブ型のみ来ることを保証してやれば良い。
合わせて、今回の例では来たものをそのまま返す事をやりたいので引数もT1にする必要がある(Tで受け取ってT1で返すことは定義上矛盾するのでコンパイルエラーとなる)。

val c: Container[Animal] = new Container[Creature]

c.foo(new Creature) // コンパイルエラー inferred type arguments [Creature] do not conform to method foo's type parameter bounds [T1 <: Animal]
c.foo(new Animal) // OK
c.foo(new Cat) // OK

なぜ下限境界を設定すると共変型が引数で使えるようになるのか

基本的には反変の逆になるだけ。
定義はこう。

trait Container[+T] {
  def foo[T1 >: T](x: T1): Unit
}

こちらは【原則1】に準拠するというよりは、最初から色んな物が来ることを想定するように定義しておく、といったところか。

定義上はTまたはTのスーパークラスということだが、Tにはもともとサブクラスも含まれるのでCatも渡せる。
T(Animal)のスーパークラスということはAnyRefなんかも含まれるので、ということはStringもOK。
つまりなんでもOK。

val c: Container[Animal] = new Container[Cat]

c.foo(new Creature) // OK
c.foo(new Animal) // OK
c.foo(new Cat) // OK

c.foo("hello") // これもOK

Scalaのクラス

Function1

引数型のT1は反変で戻り値方のRは共変定義。

trait Function1[-T1, +R] extends AnyRef

abstract def apply(v1: T1): R

List

sealed abstract class List[+A] extends ...

def contains[A1 >: A](elem: A1): Boolean