ジェネリクスと変位について覚え書き -- なぜimmutableに書くのか
「setterをむやみに生やさずにimmutableに書こう」
という理由のひとつとして、「共変にできる」というのもあるんだなぁと思ったのでメモ。
よく聞く「T[]
は不変(invariant)だよ」というやつ
- 電車で考えてみる
Male
extendsHuman
,Female
extendsHuman
Train<Human>
... 一般車両Train<Female>
... 女性専用車両
Train<Female>
extendsTrain<Human>
と言えるか?- 一般には言えない
- 反例:
train.push(aMale)
Train<Female>
はpush<Human>(aHuman: Human)
を実装できないのでTrain<Human>
としては使えない- is-a関係が崩れるので、とくに継承関係のない無関係な型になる(invariant)
必ずinvariantかというと、そうではない
Train<T>::at<T>(index:int): T
これは問題ないTrain<Female>
から取り出したFemale
はもれなくHuman
として使えるTrain<Female>::at<Female>(index:int): Female
はTrain<Human>::at<Human>(index:int): Human
として使えるということ- 「共変戻り値」という
Train<T>::push<T>(t: T): T
これが駄目- 引数の場合、型を拡大できても、狭めることはできない
- 「反変引数」という
- 電車の例でいうと、女性が通常車両に乗るぶんには問題ない
- 「共変かつ反変」というのが無理なので不変になるよ、という話
- 反変なメソッドさえ生えていなければ、共変になりうる
共変なジェネリクスの例: ScalaのimmutableのList[+A]
- Scalaだと型パラメータを
[]
で書くらしい+
が共変、-
が反変を表す
def head: A Selects the first element of this iterable collection.
- 要素取得。共変戻り値
final def map[B](f: (A) => B): List[B] Builds a new list by applying a function to all elements of this list.
- コールバック関数
f
の型が(A) => B
- 「反変引数」なので、引数型を広げるぶんには問題ない。
(Female) => B
として(Human) => B
を使うことができる - つまり、
List[Female]
はList[Human]
の機能を満たす。まだ共変
def appended[B >: A](elem: B): List[B] A copy of this sequence with an element appended.
- リストに変更を加えるのではなく、新しいリストを返す
- 引数型にも戻り値型にも
List[A]
の型パラメータA
が出てこないので、変位にはとくに影響しない。共変を崩さない
そんなこんなでScalaのimmutableのList[A]
は共変らしい
追記
PHP でいうと psalm にも template-covariant というのがあったりします https://t.co/ltMbK4vvo0
— sji (@sji_ch) 2020年8月14日
PHPでは、PsalmとPHPDocコメントを使って共変の変位指定ができるらしい