generated at
invariant
covariantでもcontravariantでもないこと
以下のように捉えると直観に沿う
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
C<S> C<T> がinvariantであるということは、( S T が同等でない限り)両者が互いに何の関係もないものである
部分型関係が全く無い
S T の間に部分型関係があっても、wrapすることで全然関係ないものになる
だから、どちらかを、もう一方に代入するなんてことはできなくなる



Arrayにおけるwriteが共変だったらどういう問題が起きるのか?
以下のようなコードが書けてしまう
scala
val 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
他の例
scala
val 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の話なのかは知らないmrsekut
そのため、Javaは任意の配列の全ての破壊的代入においてruntime検査を行う
そのため、パフォーマンスが犠牲になっているらしい




非変もたぶん同じ意味
nonvariantは、日本語のwikipediaには出てくるが、英語のwikipediaには出てこない
TaPLでは、原著ではinvariantを使っており、訳では「非変」が使われている


参考
Kotlinのcovaritnとinvariantについて
わかりやすいmrsekut




上の例の 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
TaPLのRefに対する説明をもじったもの
例えば、 Array S <: Array T の時
文脈から与えられる新しい値は型 T を持つだろう
arr: Array<T> = [..]
arr[0] = t
もし配列の実際の型が Array<S> であれば、他の誰かが後にこの値を読み出し、型 S の値として使う可能性がある
a: S = arr[0]
これは、 T <: S が成立する場合のみ安全である
まったくわかりづらいmrsekut
/mrsekut-book-4274069117/180の続く説明でも似たようなことをしている
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 をつける
kt
interface List<out E> : Collection<E> { ... } interface MutableList<E> : List<E>, MutableCollection<E> { ... }
Listはcovariantなので out を付けている






他の例
ts
type 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']
こっちの例のほうがわかりやすい気がするmrsekut
参照とか絡まないのでノイズが少ない