かとじゅんの技術日誌

技術の話をするところ

Scalaのジェネリックスを学ぶ

Scalaジェネリックスを少し学んでみました。(なんか違うんじゃね?とかあれば、ツッコミお願いしますm(_ _)m)

Javaジェネリックスでは、型パラメータは共変/反変ではない

Javaでこんなコードを書いてコンパイラに怒られたことないですか。

public interface Animal {
}
public class Dog implements Animal{
}
public class App {
    public static void main(String[] args) {
        ArrayList<Animal> animals = new ArrayList<Dog>();
    }
}
ArrayList<Animal> animals = new ArrayList<Dog>();

とすると、型がミスマッチとなります。

つまり、ArrayListのサブ型として、ArrayListは扱えません。ArrayListに、ArrayListは代入できません。ArrayListしか認められません。型パラメータは変わらないわけです。このような性質を「非変」といいます。Javaの型パラメータは非変です。

逆に、ArrayListのサブ型として、ArrayListを扱うArrayListに、ArrayListを代入する場合は、「共変」という性質を用います。Javaでは、共変に近いことを実現するためには、?(ワイルドカード)とextendsを使って以下のように記述します。

ArrayList<? extends Animal> animals = new ArrayList<Dog>();

このあたりが参考になります。

そんなわけで、ワイルドカードを使って
Hoge<? extends A> a = new Hoge<B>();
といったようにしないといけない。ジェネリクスの<>の中は一般のJavaの型の代入互換性とは異なる。このことはよく覚えておかなくてはいけない。

また、ArrayListのサブ型としてArrayListを扱うArrayListにArrayListを代入する場合は、「反変」という性質を用います。この反変に近いことを実現するには、?(ワイルドカード)とsuperを使って以下のように記述します。

ArrayList<? super Dog> animals = new ArrayList<Animal>();

このあたりも参考になります。

Existential Type
 JavaのGenericsは共変/反変ではないと述べましたが,Javaでもワイルドカードという機能を使うことで,それに近い効果を得ることができます。ワイルドカードは,型を定義する側ではなく,使う側に付加するもので,型を使う個所で,G<? extends T1>や,G<? super T1>のようにして使います。

Scalaのジェネリックスでの、非変、共変、反変

さて、本題のScalaのジェネリックスでは、非変、共変、反変がどうなるかって話ですが、

scala> trait Animal
defined trait Animal

scala> class Dog extends Animal
defined class Dog

scala> val dogs = List(new Dog) 
dogs: List[Dog] = List(Dog@51da6868)

scala> val animals:List[Animal] = dogs
animals: List[Animal] = List(Dog@51da6868)

Animal型の型パラメータを取るListに、Dog型のリストをそのまま代入できます。

ScalaのListは

class List[+A] ...

と宣言されています。
型パラメータに+を付けるとその型パラメータのサブ型との関係が共変になります。上記の例では、List[Dog]は、List[Animal]のサブ型として扱えるに代入できるようになっています。

逆に、配列型のArrayは、+がない非変で定義されているので、上記のListと同じようにした場合は、型がミスマッチになります。

class Array[T] ...
scala> val dogArray = Array(new Dog)
dogArray: Array[Dog] = Array(Dog@2d74e4b3)

scala> val animalArray:Array[Animal] = dogArray
<console>:8: error: type mismatch;
 found   : Array[Dog]
 required: Array[Animal]
       val animalArray:Array[Animal] = dogArray
                                       ^

ちなみに、Javaの配列は共変です。これは、Javaとは違うところなんで気をつけないといけません。

共変
配列は共変(covariant)な変換が出来るらしい。[2008-05-21]
つまり、要素のクラスに継承関係があるとき、その配列は親クラスの配列に代入できる。
Integer arri = new Integer[10];
Number
arrn = arri; //代入可能
でもこれはタイプセーフ(型安全)でなくなってしまうので、危険行為(違う型を入れていることがコンパイル時に分からない為)。
Javaの失敗と言われているらしい。
Integer arri = new Integer[10];
Number
arrn = arri;
arrn[0] = new Long(0); //コンパイルエラーにならず、実行時にjava.lang.ArrayStoreExceptionが発生する。

次に反変です。

scala> class Container[-T](value:T)
defined class Container

scala> class User
defined class User

scala> class Admin extends User
defined class Admin

scala> val c1 = new Container(new User)
c1: Container[User] = Container@6e820a0c

scala> val c2:Container[Admin] = c1
c2: Container[Admin] = Container@6e820a0c

この例では、UserがAdminの親ですが、Containerの方では、それとは反対にContainer[Admin]がContainer[User]の親になります。
Javaと比べると、クライアントのコードで?(ワイルドカード)とextends,superなどを指定しなくてよいので、直感的に扱えますね。

型パラメータの上限境界と下限境界について

型パラメータの上限境界と下限境界を指定する方法を紹介。
上限境界とは、型パラメータに指定できる型の、上限の型のことを指します。

scala> class User
defined class User

scala> class Admin extends User
defined class Admin

scala> class SuperAdmin extends Admin
defined class SuperAdmin

scala> class Container[T <: Admin](value:T)
defined class Container

scala> val c1:Container[User] = new Container(new User)
<console>:8: error: inferred type arguments [User] do not conform to class Container's type parameter bounds [T <: Admin]
       val c1:Container[User] = new Container(new User)
                                ^

scala> val c2:Container[Admin] = new Container(new Admin)
c2: Container[Admin] = Container@7519ca2c

scala> val c3:Container[SuperAdmin] = new Container(new SuperAdmin)
c3: Container[SuperAdmin] = Container@4bcd2d49

型バラメータで、

T <: Admin

と指定した場合は、Adminを上限とするので、Tは、Adminか、Adminのサブクラスでなければなりません。

val c1 = new Container(new User)

とした場合は、当然、UserはAdminのサブクラスではないので、コンパイルエラーになります。

次は下限境界。下限境界とは、型パラメータに指定できる型の、下限の型のことを指します。
上記のContainerのTをAdminを下限の型に指定してみます。

scala> class Container[T >: Admin](value:T)
defined class Container

scala> val c1 = new Container(new User)            
c1: Container[User] = Container@1753d79c

scala> val c2 = new Container(new Admin)
c2: Container[Admin] = Container@6521f956

scala> val c3 = new Container(new SuperAdmin)
c3: Container[Admin] = Container@418bdc7a

scala> val c4 = new Container(10)    
c4: Container[Any] = Container@27d08e21

型バラメータで、

T >: Admin

と指定した場合は、Adminを下限とするので、Tは、Adminか、Adminのスーパークラスでなければなりません。UserはAdminの上位なので代入が可能です。

val c3 = new Container(new SuperAdmin)

とした場合は、SuperAdminはAdminとして扱われます。また、Intの場合はAnyとして扱われます。

上記は非変での例でしたが、下記のように共変、反変を組み合わせ宣言することも可能です。

class Container[+T <: Admin](value:T)
class Container[-T <: Admin](value:T)
class Container[+T >: Admin](value:T)
class Container[-T >: Admin](value:T)

少し触った感触では、多少新しい概念を身につけないといけないけど、それほど難しくない感じ。習うより慣れろってことで。

追記:
id:xuweiさんに代入互換性について指摘された通りだと思ったので、表現を修正しました。これは非常に重要なポイントだと思いました。ありがとうございました。

1. "型に互換性がある"(ある型のものが他の型の変数に代入できる)
ということと
2. スーパークラスの型の変数に、サブクラスのものが代入できる

参考リンク:
Javaの理論と実践: Generics のワイルドカードを使いこなす、第 1 回
Java の理論と実践: Generics のワイルドカードを使いこなす、第 2 回
Javaジェネリクス再入門 - プログラマーの脳みそ
ジェネリクスの代入を理解する その1
ジェネリクスの代入を理解する その2
第6回 Scala言語を探検する(4)Scalaの型システム - 刺激を求める技術者に捧げるScala講座:ITpro