勉強日記

チラ裏

ジェネリクスと変位について覚え書き -- なぜimmutableに書くのか

「setterをむやみに生やさずにimmutableに書こう」

という理由のひとつとして、「共変にできる」というのもあるんだなぁと思ったのでメモ。


よく聞く「T[]は不変(invariant)だよ」というやつ

  • 電車で考えてみる
    • Male extends Human, Female extends Human
    • Train<Human> ... 一般車両
    • Train<Female> ... 女性専用車両
  • Train<Female> extends Train<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): FemaleTrain<Human>::at<Human>(index:int): Human として使えるということ
    • 「共変戻り値」という
  • Train<T>::push<T>(t: T): T これが駄目
    • 引数の場合、型を拡大できても、狭めることはできない
    • 「反変引数」という
    • 電車の例でいうと、女性が通常車両に乗るぶんには問題ない
  • 「共変かつ反変」というのが無理なので不変になるよ、という話
  • 反変なメソッドさえ生えていなければ、共変になりうる

共変なジェネリクスの例: ScalaのimmutableのList[+A]

  • Scalaだと型パラメータを[]で書くらしい
    • +が共変、-が反変を表す

www.scala-lang.org

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とPHPDocコメントを使って共変の変位指定ができるらしい