Scalaの型パラメータと変位指定のメモ

変位指定がぜんぜん理解できてなかったので、いろいろ試してある程度理解したメモ。 型パラメータとは?とか変位指定とは?とかの詳細は別の記事をあたってください。 圏論がさっぱりな状態で書いているので、圏論的に変なこと言ってても気にしないでください。

trait Protocol

sealed trait Req extends Protocol

case class AddRequest() extends Req
case class UpdateRequest() extends Req

sealed trait Res

みたいな型定義があったときに、AddRequest, UpdateRequestを対象にした処理を書きたかった。 Req から変位指定して型パラメータつけてあげればできるんじゃないかと思ってたけど全然コンパイルが通らず、理解できなかったのでいろいろ試してみた。

非変は自明なのでいいとして

  • 共変(covariance)
class C[+T]
val v1: C[Req] = new C[Req]
val v2: C[Req] = new C[AddRequest]
val v3: C[Req] = new C[Protocol] // NG

AddRequest はReqを継承してるので共変ならば代入可能。 Protocol はReqを継承していないので代入不可。 これはまぁ分かりやすい。

  • 反変(contravariance)
class C[-T]
val v1: C[Req] = new C[AddRequest] // NG
val v2: C[AddRequest] = new C[Req] // OK

共変の逆。 関数を例にして挙動を説明される記事が多い。今のところまだ自分で指定したことは無いけど必要なケースはそのうちありそう。

  • 上限型境界(upper type bounds)
class C[T <: Req]
val v1: C[Req] = new C[Req]
val v2: C[Req] = new C[AddRequest] // NG

これの

val v2: C[Req] = new C[AddRequest]

コンパイルが通らない理由が全然わからなかったが

class C[+T <: Req]
val v: C[Req] = new C[AddRequest]

上限型境界に共変性を持たせるとコンパイルが通ることが分かった。

これに気づいて、自分が何を理解して何を理解していなかったがはっきりした。

共変と上限型境界、反変と下限型境界を似たような概念のなんか少し違うもの、くらいでしか理解していなかった。 共変と反変は明確に型同士の関係性を示すものだった。 一方で上限型境界、下限型境界は型に指定できるものを制限する目的で使われるものだった。

class C[T <: Req]

と書くと、TにはReqのサブタイプなら入れられると思っていたが、例えばTに AddRequest を入れると TAddRequest で束縛されるので、 C[AddRequest] 型となる、ということが分かっていなかった。

もともと自分がやりたかったことは

def dispatch[T <: Req](request: T) = {
  request match {
    case _: AddRequest =>
    case _: UpdateRequest =>
  }
}

のようなことだったが、 AddRequest, UpdateRequest に応じた処理を外から注入して合成しようとすると、どうにも型が不一致してコンパイルできなかった。

def dispatch[+T <: Req](request: T) = {
  ...
}

として、TをReqの共変として定義してあげればきっと通るだろう(未確認)