tkzwhr's notes

tkzwhrの技術、中国語などのメモです。

Scalaの変位パラメータ

前提

前提として、計数可能な性質を表すCountableを定義する。 Countableは説明と数量を保持する。

trait Countable {
  val description: String
  def number: Int
}

ここでは、上記の性質をテーブルゲームに適用し、具体化したものとして、将棋の駒数、麻雀牌の数をモデリングしてみる。

abstract class TableGame extends Countable {
  val players: Int
}

case object Shogi extends TableGame {
  override val description = "将棋の駒数"
  override val number = 40
  override val players = 2
}

case object Mahjong extends TableGame {
  override val description = "麻雀牌の数"
  override val number = 136
  override val players = 4
}

上限境界で与えられる型を限定する

Countableの数量が最大であることを表す型Maxを定義してみる。 Maxは型パラメータTを持っており、最大値を持つオブジェクトをvalueとして保持する。

また、与えられた値Tを自身と比較し、より大きい値を持つMaxを返却するメソッドmaxも実装する。

case class Max[T](initial: T) {
  val value = initial
  def max(x: T): Max[T] = if (x.number > value.number) Max(x) else this
}

ただし、このままだとTCountableを継承しない型をとれるため、maxコンパイルエラーとなってしまう。

value number is not a member of type parameter T

そこで、TCountableの性質をもつことを示すため、上限境界を設定する。

- case class Max[T](initial: T) {
+ case class Max[T <: Countable](initial: T) {  // <: が上限境界の指定
    val value = initial
    def max(x: T): Max[T] = if (x.number > value.number) Max(x) else this
  }

こうすることで、TCountableのサブクラスのインスタンスまたはミックスインされたオブジェクトに限定することができるため、numberが参照できるようになる。 次のコードを実行すると結果が表示される。

val max = Max(Shogi).max(Shogi)
println(s"${max.value.description}は${max.value.number}です")
// ==> "将棋の駒数は40です。"

maxをより有用な処理にするために

先ほどの例では、同じものを比較しており、意味を感じられるものではなかった。 では次のコードを実行するとどうなるだろうか。

val max = Max(Shogi).max(Mahjong)
println(s"${max.value.description}は${max.value.number}です")

少しは意味がありそうなコードになったが、実際に実行しようとするとコンパイルエラーとなる。

type mismatch;
    found : Mahjong.type
    required: Shogi.type

Max(Shogi)インスタンス型はMax[Shogi]なので、maxが受け取る引数の型も、返却するMaxの型パラメータもShogiを要求しているのである。これに対応するためにはCountableを取り扱うようにすれば良い。

  case class Max[T <: Countable](initial: T) {
    val value = initial
-   def max(x: T): Max[T] = if (x.number > value.number) Max(x) else this
+   def max(x: Countable): Max[Countable] = if (x.number > value.number) Max(x) else this
  }

しかし、依然としてコンパイルエラーとなる。

type mismatch;
    found : Max[T]
    required: Max[Countable]
Note: T <: Countable, but class Max is invariant in type T.
You may wish to define T as +T instead. (SLS 4.5)

これは、maxの定義として、Max[Countable]を返却する宣言なのにMax[T]を返却しているためである。

非変(invariant)と共変(co-variant)

このエラーに対処するためには非変と共変について理解しておく必要がある。

B extends Aであるとき、C[T]についてC[B] extends C[A]を満たすときのT共変という。 一方C[B]C[A]をそれぞれ独立した型として扱うときのT非変という。 Scalaでは型パラメータは基本的に非変である。そのためエラーとなったのである。

ところで、ここでMax[T]を返すことに何か問題はあるだろうか。 Tについては下限境界を設定しているため、Countableの性質を備えていることが自明であるため、Max[T]Max[Countable]のサブ型とみて問題はないだろう。 そのため、ここでは型パラメータを共変に変更することでMax[T] extends Max[Countable]であることを明示する。そうすれば、Max[Countable]を要求する場面で、より具体的なMax[T]を返却することができるようになる。

- case class Max[T <: Countable](initial: T) {
+ case class Max[+T <: Countable](initial: T) { // + が共変の指定
    val value = initial
    def max(x: Countable): Max[Countable] = if (x.number > value.number) Max(x) else this
  }

これで、maxの結果は常にMax[Countable]として得られるのである。

下限境界を使ってmaxをもっと便利に

maxにはもう少し改善の余地がある。

val max = Max(Shogi).max(Mahjong)

上記の変数maxの型はどうなるだろうか。 ShogiMahjongしか渡していないため、結果はMax[Shogi]Max[Mahjong]のどちらかにしかならないはずである。 つまり、Max[TableGame]であることは確実である。 ところが、実際にはMax[Countable]になってしまう。

いつでもCountableというのは型を決めすぎていて少し扱いが難しい。 今回の例で言えばMax[TableGame]として取り扱いたいところである。

これは新たな型パラメータと下限境界を導入することで改善可能である。

  case class Max[+T <: Countable](initial: T) {
    val value = initial
-   def max(x: Countable): Max[Countable] = if (x.number > value.number) Max(x) else this
+   def max[U >: T <: Countable](x: U): Max[U] = if (x.number > value.number) Max(x) else this // >: が下限境界の指定
  }

少しややこしいが、U >: T <: Countableと書くことで、このUの取るクラスの継承関係が下図の範囲であることを示している。

この状態で次のコードを実行すると、TableGameとして扱えていることがわかる。

val max = Max(Shogi).max(Mahjong)
println(s"${max.value.description}は${max.value.number}です。プレイヤー数は${max.value.players}です。")
// ==> "麻雀牌の数は136です。プレイヤー数は4です。"

反変(contravariant)

今回は取り上げなかったが、非変、共変の他に反変というものもある。 B extends Aであるとき、C[T]についてC[A] extends C[B]を満たすときのTを反変という。

反変の指定は-で行う。

trait Channel[-T] {
  def output(x: T)
}