前提
前提として、計数可能な性質を表す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
}
ただし、このままだとT
がCountable
を継承しない型をとれるため、max
でコンパイルエラーとなってしまう。
value number is not a member of type parameter T
そこで、T
がCountable
の性質をもつことを示すため、上限境界を設定する。
- 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
}
こうすることで、T
はCountable
のサブクラスのインスタンスまたはミックスインされたオブジェクトに限定することができるため、number
が参照できるようになる。
次のコードを実行すると結果が表示される。
val max = Max(Shogi).max(Shogi)
println(s"${max.value.description}は${max.value.number}です")
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
の型はどうなるだろうか。
Shogi
とMahjong
しか渡していないため、結果は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}です。")
反変(contravariant)
今回は取り上げなかったが、非変、共変の他に反変というものもある。
B extends A
であるとき、C[T]
についてC[A] extends C[B]
を満たすときのT
を反変という。
反変の指定は-
で行う。
trait Channel[-T] {
def output(x: T)
}