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>
に部分型関係が入る
逆に言ったほうがわかりやすい

C<S>
と C<T>
がinvariantであるということは、( S
と T
が同等でない限り)両者が互いに何の関係もないものである
部分型関係が全く無い
S
と T
の間に部分型関係があっても、wrapすることで全然関係ないものになる
だから、どちらかを、もう一方に代入するなんてことはできなくなる
Arrayにおけるwriteが共変だったらどういう問題が起きるのか?
以下のようなコードが書けてしまう
scalaval arr: Array[Int] = Array[Int](1,2,3)
val arr2: Array[Any] = arr
arr2(0) = 4.2
参照を持っているから
arr2(0)=4.2
とすると
arr2(0)
と arr(0)
の両方が、 4.2
になる
すると、 arr
は Int
であるはずなのに、 Float
が入ってしまう
実際はScalaのArrayはinvariantなのでちゃんとコンパイルエラーになる
scala// Listは共変
val lis: List[Int] = List[Int](1,2,3)
val lis2: List[Any] = lis
// Arrayは不変
val arr: Array[Int] = Array[Int](1,2,3)
val arr2: Array[Any] = arr // error
arr2(0) = 4.2
他の例
scalaval add = (arr: Array[Any]) => arr :+ 1.4
val arr: Array[Int] = Array[Int](1,2,3)
add(arr) // error
Array[Any]
を受け取る関数に、 Array[Int]
を渡している
TypeScriptのArrayはcovariantになっている
そのため健全性が壊れている
JavaもそうだとTaPLに書かれている
どのversionの話なのかは知らない

そのため、Javaは任意の配列の全ての破壊的代入においてruntime検査を行う
そのため、パフォーマンスが犠牲になっているらしい
TaPLでは、原著ではinvariantを使っており、訳では「非変」が使われている
参考
Kotlinのcovaritnとinvariantについて
わかりやすい

上の例の 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する時を考える
反変になる
こっちのわかりやすい説明を書けない

TaPLのRefに対する説明をもじったもの
例えば、 Array S <: Array T
の時
文脈から与えられる新しい値は型 T
を持つだろう
arr: Array<T> = [..]
arr[0] = t
もし配列の実際の型が Array<S>
であれば、他の誰かが後にこの値を読み出し、型 S
の値として使う可能性がある
a: S = arr[0]
これは、 T <: S
が成立する場合のみ安全である
まったくわかりづらい

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
をつける
ktinterface List<out E> : Collection<E> { ... }
interface MutableList<E> : List<E>, MutableCollection<E> { ... }
Listはcovariantなので out
を付けている
他の例
tstype Animal = Cat | Dog;
type Cat = 'cat';
type Dog = 'dog';
const add = (animals: Animal[]) => [...animals, 'cat']
const cats: Cat[] = ['cat'];
add(cats);
console.log(cats); // ['cat','cat','dog']
こっちの例のほうがわかりやすい気がする

参照とか絡まないのでノイズが少ない