invariant
以下のように捉えると直観に沿う
readする時は、covariantが必要
writeする時は、contravariantが必要
read/writeの両方を持ち合わせているものは、invariantである必要がある
例
Ref型
Array型
$ \frac{S <: T \; T<: S}{\mathrm{Array}S <: \mathrm{Array} T}
Array S <: Array Tとなるためには、SとTが部分型関係の下で同等である必要がある
上の例のRefもArrayも、immutableな型なので、invariantであることが要請される
$ \frac{S <: T \; T<: S}{\mathrm{Array}S <: \mathrm{Array} T}の解釈
SとTが部分型関係の下で同等である場合のみ、C<S>とC<T>に部分型関係が入る
逆に言ったほうがわかりやすいmrsekut.icon
C<S>とC<T>がinvariantであるということは、(SとTが同等でない限り)両者が互いに何の関係もないものである
部分型関係が全く無い
SとTの間に部分型関係があっても、wrapすることで全然関係ないものになる
だから、どちらかを、もう一方に代入するなんてことはできなくなる
Arrayにおけるwriteが共変だったらどういう問題が起きるのか?
以下のようなコードが書けてしまう
code:scala
val arr: ArrayInt = ArrayInt(1,2,3) arr2(0) = 4.2
参照を持っているから
arr2(0)=4.2とすると
arr2(0)とarr(0)の両方が、4.2になる
すると、arrはIntであるはずなのに、Floatが入ってしまう
実際はScalaのArrayはinvariantなのでちゃんとコンパイルエラーになる
code:scala
// Listは共変
val lis: ListInt = ListInt(1,2,3) // Arrayは不変
val arr: ArrayInt = ArrayInt(1,2,3) val arr2: ArrayAny = arr // error arr2(0) = 4.2
他の例
code:scala
val add = (arr: ArrayAny) => arr :+ 1.4 val arr: ArrayInt = ArrayInt(1,2,3) add(arr) // error
Array[Any]を受け取る関数に、Array[Int]を渡している
TypeScriptのArrayはcovariantになっている
そのため健全性が壊れている
JavaもそうだとTaPLに書かれている
どのversionの話なのかは知らないmrsekut.icon
そのため、Javaは任意の配列の全ての破壊的代入においてruntime検査を行う
そのため、パフォーマンスが犠牲になっているらしい
TaPLでは、原著ではinvariantを使っており、訳では「非変」が使われている
参考
Kotlinのcovaritnとinvariantについて
わかりやすいmrsekut.icon
上の例のRefもArrayも、immutableな型なので、invariantであることが要請される
一方で、Listはmutableなので、covariantとするのが型安全になる
Arrayをreadをする時を考える
Arrayのreadのみを考える時は、mutableなListと同等とみなせる
S <: Tの時、Array S <: Array Tになる
つまり、共変になる
例えば、Array S <: Array Tの時
a = arr[1]のようにreadした時、aはT型となることを期待する
もしarr[1]の中身が実際はS型だった場合、S <: Tである必要がある
Arrayをwriteする時を考える
反変になる
こっちのわかりやすい説明を書けないmrsekut.icon
TaPLのRefに対する説明をもじったもの
例えば、Array S <: Array Tの時
文脈から与えられる新しい値は型Tを持つだろう
arr: Array<T> = [..]
arr[0] = t
もし配列の実際の型がArray<S>であれば、他の誰かが後にこの値を読み出し、型Sの値として使う可能性がある
a: S = arr[0]
これは、T <: Sが成立する場合のみ安全である
まったくわかりづらいmrsekut.icon
mutableなRefを2つの型に分ける
読み出しのみできるSource型
こちらはcovariantである
書き込みのみできるSink型
こちらはcontravariantである
そして、以下の部分型関係が成り立つ
Ref T <: Source T
Ref T <: Sink T
kotlin
covariantにするためにoutつける
出力用にしか使われないことを明示する
contravariantにするためにinをつける
消費する用途にしか使われないことを明示する
何も付けないとinvariantになる
既に定義されているgenericなclassを使う時に、型引数にout/inをつける
つまり、genericな関数の定義時に使う
classやinterfaceの宣言時にout/inをつける
code:kt
interface List<out E> : Collection<E> { ... }
interface MutableList<E> : List<E>, MutableCollection<E> { ... }
Listはcovariantなのでoutを付けている
他の例
code:ts
type Animal = Cat | Dog;
type Cat = 'cat';
type Dog = 'dog';
const cats: Cat[] = 'cat'; add(cats);
こっちの例のほうがわかりやすい気がするmrsekut.icon
参照とか絡まないのでノイズが少ない