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>();
とすると、型がミスマッチとなります。
つまり、ArrayListArrayList
逆に、ArrayListArrayList
ArrayList<? extends Animal> animals = new ArrayList<Dog>();
このあたりが参考になります。
そんなわけで、ワイルドカードを使って
Hoge<? extends A> a = new Hoge<B>();
といったようにしないといけない。ジェネリクスの<>の中は一般のJavaの型の代入互換性とは異なる。このことはよく覚えておかなくてはいけない。
また、ArrayListArrayList
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