いわゆるGenericsのメカニズムについては、ごく基本的な部分についてはJavaと同じように見える。型パラメータの境界(親の型としてXXXを持ってないといけないとか、子の型としてXXXを持ってないといけないとか)についても同じ。大きく違うのは、型パラメータの継承関係と、パラメータバウンドされた総称クラス(以下、面倒なので"総称クラス"と書く)自体の継承関係との関係、すなわち共変(Co-Variant)に関する部分。
といっても、共変自体にあまり詳しくないので、しばらく共変について勉強してみる。
共変
調べても定義らしい定義が出てこなくて困った。とりあえずは関連ありそうな記事として以下のようなものがあった。
それにしても、JavaのGenricsは既にものすごくややこしい…ワイルドカード、キャプチャ、境界、このあたりを正確に理解している人間はいったいどれくらいいるのだろうか。
Variance-Annotation
訳し方がわからなかったのでそのままのタイトルにしてしまった。これは要するに、共変に関する性質を変更できる注釈(Annotation)のこと。どのように変更できるかというと…
Non-Variant
これは、Stack[AnyRef]型の変数にStack[String]型のオブジェクトを代入しようとするとエラーになるということ。Scalaの総称クラスはデフォルトでは共変しない。JavaのGenericsも同様。実際にこんな風に書いてみると…
Co-Variant
繰り返しになるがこれは、「型パラメータが継承関係にあれば、総称クラスも継承関係を持つ」というもの。具体的には、Stack[AnyRef]がStack[String]のスーパータイプになるということ。型パラメータにAnnotation「+」を付加することで、共変になる。Javaで言うと、配列が共変といえる。Object[] array = new String[5]はOKでしょ?じゃ、Scalaのサンプルいきます。
まずは、Stackの定義をこんな風に書き換えてみる。といっても、クラス定義の型パラメータに「+」をつけただけ。
実際にはpushは内部を変更するわけではなく、新しいStackを作って返すだけなので、エラーになるのは納得いかない。これは、Lower Boundsで解決方法が書かれるらしいので、とりあえずは先まわし。
さて、気をとりなおして…Stackからpushメソッドを削除し、以下のようにする。もうStackでもなんでもないが、型システムを理解するのが目的なのでこれはこれでよしとする。
Contra-Variant
これは今までの常識を超えた感じのもので、共変の反対になる。何が反対かというと継承階層の上下で、型パラメータがスーパータイプのものは総称クラスでサブタイプになり、型パラメータがサブタイプのものは総称クラスでスーパータイプになる。思わずポルナレフのAAが出てきそうになるが、例を出すとわかりやすい。Contra-Variantとは…
といっても、共変自体にあまり詳しくないので、しばらく共変について勉強してみる。
共変
調べても定義らしい定義が出てこなくて困った。とりあえずは関連ありそうな記事として以下のようなものがあった。
- Javaの理論と実践: Generics、了解!
- Javaの理論と実践: Genericsのワイルドカードを使いこなす、第1回
- Javaの理論と実践: Genericsのワイルドカードを使いこなす、第2回
- Java Genericsにみるvariance
それにしても、JavaのGenricsは既にものすごくややこしい…ワイルドカード、キャプチャ、境界、このあたりを正確に理解している人間はいったいどれくらいいるのだろうか。
Variance-Annotation
訳し方がわからなかったのでそのままのタイトルにしてしまった。これは要するに、共変に関する性質を変更できる注釈(Annotation)のこと。どのように変更できるかというと…
- Non-Variant(共変しない(Javaと同じ))
- Co-Variant(共変する)
- Contra-Variant(マイナス方向に共変する)
Non-Variant
これは、Stack[AnyRef]型の変数にStack[String]型のオブジェクトを代入しようとするとエラーになるということ。Scalaの総称クラスはデフォルトでは共変しない。JavaのGenericsも同様。実際にこんな風に書いてみると…
package genericsで、まずは以下のように書いてみる。
abstract class Stack[A] {
def push(x: A): Stack[A] = new NonEmptyStack[A](x, this)
def isEmpty: Boolean
def top: A
def pop: Stack[A]
}
class EmptyStack[A] extends Stack[A] {
def isEmpty = true
def top = error("EmptyStack.top")
def pop = error("EmptyStack.pop")
}
class NonEmptyStack[A](elem: A, rest: Stack[A]) extends Stack[A] {
def isEmpty = false
def top = elem
def pop = rest
}
val stack : Stack[String] = new EmptyStack[String]当然コンパイルは通る。型パラメータは同じなんだから当然。で、実行すると…
println(stack.push("aiueo").top)
aiueoこうなる。今度はこんな風に書いてみると…
val stack : Stack[AnyRef] = new EmptyStack[String]今度はコンパイルエラーになり、以下のようなエラーメッセージが出る。
type mismatch;型パラメータどうしは継承関係にあるけど、総称クラスどうしは継承関係にはならないことがわかる。ちなみに、このように書くと…。
found : generics.EmptyStack[String]
required: generics.Stack[AnyRef]
val stack = new EmptyStack[AnyRef]当たり前だがコンパイルは通り、以下のように結果が出力される。
println(stack.push("aiueo").top)
aiueoこの場合は型推論が働いて、stackの型はEmptyStack[AnyRef]になるので型の不一致は起きない。"aiueo"はAnyRef型のサブタイプなので、メソッド呼び出しに関しても問題なし。これが、共変なしの状態。次。
Co-Variant
繰り返しになるがこれは、「型パラメータが継承関係にあれば、総称クラスも継承関係を持つ」というもの。具体的には、Stack[AnyRef]がStack[String]のスーパータイプになるということ。型パラメータにAnnotation「+」を付加することで、共変になる。Javaで言うと、配列が共変といえる。Object[] array = new String[5]はOKでしょ?じゃ、Scalaのサンプルいきます。
まずは、Stackの定義をこんな風に書き換えてみる。といっても、クラス定義の型パラメータに「+」をつけただけ。
abstract class Stack[+A] {すると、
def push(x: A): Stack[A] = new NonEmptyStack[A](x, this)
def isEmpty: Boolean
def top: A
def pop: Stack[A]
}
covariant type A occurs in contravariant position in type A of value xこんなコンパイルエラーが出る。なんのこっちゃ?という感じだが、少し説明を。Javaの配列が共変だということは既に触れたわけだけど、Javaの配列でこーんなことをすると、コンパイルエラーは起きず、実行時例外が発生する。
Object[] array = new String[5];仮にこのコードが実行可能だとすると、arrayの実体であるString配列にintを格納することになり、型システムが崩れてしまう。Javaではこれを、コンパイル時のチェックではなく実行時チェックによって防いでいるためRuntimeExceptionが発生するわけだが、Scalaはコンパイル時にチェックするようになっている。言語仕様的には、共変の型を配置できる場所が決まっており、これはCo-Variantポジションと言うらしい。Co-Variantポジションは…
array[0] = 0;
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
at generics.ArraySample.main(ArraySample.java:6)
- 変数の型
- メソッドの戻り値の型
- 他のcovariant型への引数
実際にはpushは内部を変更するわけではなく、新しいStackを作って返すだけなので、エラーになるのは納得いかない。これは、Lower Boundsで解決方法が書かれるらしいので、とりあえずは先まわし。
さて、気をとりなおして…Stackからpushメソッドを削除し、以下のようにする。もうStackでもなんでもないが、型システムを理解するのが目的なのでこれはこれでよしとする。
abstract class Stack[+A] {使う側のコード。
def isEmpty: Boolean
def top: A
def pop: Stack[A]
}
class EmptyStack[A] extends Stack[A] {
def isEmpty = true
def top = error("EmptyStack.top")
def pop = error("EmptyStack.pop")
}
class NonEmptyStack[A](elem: A, rest: Stack[A]) extends Stack[A] {
def isEmpty = false
def top = elem
def pop = rest
}
val stack : Stack[AnyRef] = new NonEmptyStack[String]("aiueo", new EmptyStack[String])これは、コンパイルを通る。Stack[AnyRef]がNotEmptyStack[String]のスーパータイプになっていることがわかる。実行すると…
println(stack.top)
aiueo正常に出力される。最後、Contra-Variant。
Contra-Variant
これは今までの常識を超えた感じのもので、共変の反対になる。何が反対かというと継承階層の上下で、型パラメータがスーパータイプのものは総称クラスでサブタイプになり、型パラメータがサブタイプのものは総称クラスでスーパータイプになる。思わずポルナレフのAAが出てきそうになるが、例を出すとわかりやすい。Contra-Variantとは…
val stack : Stack[String] = new Stack[AnyRef]このコンパイルが通るようなもの。逆に、
val stack : Stack[AnyRef] = new Stack[String]これはコンパイルエラーになる。
type mismatch;どこで使うんでしょ、これw Contra-Variantな型にするためには、型パラメータに「-」をつける。Co-Variantと同様、Non-VariantなStackを改良してみる。同じく、クラス定義の型パラメータに「-」をつけただけ。
found : generics.contra_variant.EmptyStack[String]
required: generics.contra_variant.Stack[AnyRef] StackMain.scala
abstract class Stack[-A] {すると、予想通り(?)コンパイルエラーになる。
def push(x: A): Stack[A] = new NonEmptyStack[A](x, this)
def isEmpty: Boolean
def top: A
def pop: Stack[A]
}
class EmptyStack[A] extends Stack[A] {
def isEmpty = true
def top = error("EmptyStack.top")
def pop = error("EmptyStack.pop")
}
class NonEmptyStack[A](elem: A, rest: Stack[A]) extends Stack[A] {
def isEmpty = false
def top = elem
def pop = rest
}
contravariant type A occurs in covariant position in type => A of method topもうわかったと思うが、Contra-Variantな型もCo-Variantな型と同様に記述できる場所が決まっており、これはContra-Variantポジションと言うようだ。サンプルのメッセージとこれまでの説明からすると、恐らくこれはCo-Variantと反対で、引数パラメータの中にしか許されない…のかな。ScalaByExampleでは深く触れられていないのでここでは深入りしないことにする。topを削除して…
abstract class Stack[-A] {これでコンパイルは通った。使う側のコードは…
def push(x: A): Stack[A] = new NonEmptyStack[A](x, this)
def isEmpty: Boolean
def pop: Stack[A]
}
class EmptyStack[A] extends Stack[A] {
def isEmpty = true
def pop = error("EmptyStack.pop")
}
class NonEmptyStack[A](elem: A, rest: Stack[A]) extends Stack[A] {
def isEmpty = false
def pop = rest
}
val stack : Stack[AnyRef] = new EmptyStack[String]まずはこう書くと、コンパイルエラーになる。まぁ予想どおり。で、こうすると…
val stack : Stack[String] = new EmptyStack[AnyRef]コンパイルを通る。やっぱり妙な感じ。
コメント