Scala 型パラメータ、変位指定と下限境界の勉強

個人開発したアプリの宣伝
目的地が設定できる手帳のような使い心地のTODOアプリを公開しています。
Todo with Location

Todo with Location

  • Yoshiko Ichikawa
  • Productivity
  • Free

スポンサードリンク

懸念を整理したので、自分なりの理解を記録しておきます。

おもちゃ箱、ToyBoxにおもちゃToyのサブクラス、MiniCarCharacterCardインスタンスを格納するという実装で考える。


初期実装

型パラメータを使わない各初期実装は以下の通り。ToyBoxにはToyを抽象として格納すれば、Toyを継承したインスタンスが全て格納できる。

class Toy(name:String) {
  def printName = println(name)
}
case class MiniCar(name:String) extends Toy(name)
case class CharacterCard(name:String) extends Toy(name)

class ToyBox(val toys: List[Toy]) {
  def push(toy: Toy) = {
    val list = toys :+ toy
    new ToyBox(list)
  }
}

object Test {
  def main() = {
    val car = new MiniCar("4wd")
    val card = new CharacterCard("super man")
    val o1 = new ToyBox( List(car, card))
    val o2 = o1.push( new MiniCar("sports car") )
    println(o2.toys)
  }
}


要求の追加

ある日、ToyBoxをToyの抽象だけではなく、ゲームソフトGameSoftを格納する箱にも使用するという変更が生じたとする(まあ、あれな設計なんだけど(^_^;)、うまい例えが思い浮かばなかった...)。


GameSoftクラス
case class Famicon(name:String) extends GameSoft(name)


ToyBoxの変更

ToyBoxToy抽象に依存した実装なので、これをインスタンス生成時に型を確定する型パラメータとして指定できるよう変更を行う。

class ToyBox[A](val toys: List[A]) {
  def push(toy: A):ToyBox[A] = {
    val list = toys :+ toy
    new ToyBox(list)
  }
}

object Test {
  def main() = {
    val car = new MiniCar("4wd")
    val card = new CharacterCard("super man")
    val o1 = new ToyBox( List(car, card))
    val o2 = o1.push( new MiniCar("sports car") )
  }
}

これでうまく抽象化できたかのように見える。


型パラメータについて

ToyBox[A]のインスタンス化した時、

val o1 = new ToyBox( List(car))
val o2 = new ToyBox( List(card))
val o3 = new ToyBox( List(car, card))

これはそれぞれ以下の型のインスタンスになることを意識しておけば理解しやすいと思います。

val o1:ToyBox[MiniCar] = new ToyBox( List(car))
val o2:ToyBox[CharacterCard]  = new ToyBox( List(card))
val o3:ToyBox[Any] = new ToyBox( List(car, card)) //これは現時点ではToyではなくAny。Listの変位指定が効いているので動作する。

つまり型パラメータをつけることによって、インスタンス型はToyBoxではなく、ToyBox[MiniCar]型となる。よって、ToyBox[MiniCar]型とToyBox[CharacterCard]型は全く違う型としてコンパイラは解釈する。

この型を抽象化できるよう指定するのが変位指定。だと思う。


ToyBox[]型は抽象されていない

上の例のように、val o1 = new ToyBox( List(car, card))ToyBox[Any]を返すのでo1.push( new MiniCar("sports car") )が動作した。

しかし、以下のようにo1のインスタンスをToyBox[CharacterCard]にするとコンパイルは通らない。

object Test {
  def main() = {
    val card = new CharacterCard("super man")
    val o1 = new ToyBox( List(card) )
    val o2 = o1.push( new MiniCar("sports car") )
  }
}
---
Test.scala:75: error: type mismatch;
 found   : MiniCar
 required: CharacterCard
    val o2 = o1.push( new MiniCar("sports car") )

これはpushメソッドが型パラメータのCharacterCard型のインスタンスを求めているのに対して、MiniCar型のインスタンスを与えたからだ。

解決策の一つとして、以下のようにToyBox[Toy]型にアップキャストすることで解決ができる。

val o1 = new ToyBox[Toy]( List(card) )
val o2 = o1.push( new MiniCar("sports car") )

しかしながら、val o1:ToyBox[CharacterCard] = new ToyBox( List(card) )として、インスタンス生成時はToyBox[CharacterCard]型として扱いたい場合やアップキャストする前のインスタンスをコンストラクタで扱いたい時もあるかもしれない。

その時に下限境界と変位指定が必要になります。



下限型境界

まずは、o1.pushが引数の型に応じて、ToyBox[Toy]を返せるようなメソッドを考える。

val o1:ToyBox[CharacterCard] = new ToyBox( List(card) )
val o2:ToyBox[Toy]  = o1.push( new MiniCar("sports car") )


メソッドの修正

pushメソッドの型パラメータに下限型境界を設ける。

def push[B >: A](toy: B):ToyBox[B] = {
  val list = toys :+ toy
  new ToyBox(list)
}

これは、BAのスーパークラスである性質を意味する。つまり上記で、引数にToy型パラメータ受け取り、ToyBox[Toy]を返すようなメソッドとして振る舞える。

つまり、型指定つきで考えると、

val o1:ToyBox[CharacterCard] = new ToyBox( List(card) )
val o2:ToyBox[Toy]  = o1.push[Toy]( new MiniCar("sports car") ) //ToyはCharacterCardのスーパークラス

となる。


型パラメータの変位指定

今度は、

val o1:ToyBox[Toy] = new ToyBox[CharacterCard]( List(card) )

としてインスタンス生成後にアップキャストしたい場合。多分、ToyBoxのコンストラクタではCharacterCard型が渡ってくるんじゃないのかな。試してないけど(^_^;)

で、このままで実行すると、このようなエラーが出る。

Test.scala:75: error: type mismatch;
 found   : ToyBox[CharacterCard]
 required: ToyBox[Toy]
Note: CharacterCard <: Toy, but class ToyBox is invariant in type A.
You may wish to define A as +A instead. (SLS 4.5)
    val o1:ToyBox[Toy] = new ToyBox[CharacterCard]( List(card) )

これは、受け取り側の型と生成したインスタンスの型が合っていないということを言っている。

受け取り側のToyBox[Toy]型はそのサブクラスも含めることができる。というような考え方で解決できる。

つまり変位指定[+A]と指定すればよい。

class ToyBox[+A](val toys: List[A]) {
  def push[B >: A](toy: B):ToyBox[B] = {
    val list = toys :+ toy
    new ToyBox(list)
  }
}


これで以下のように変位指定でサブクラスを管理できるToyBoxと仕上がった。

object Test {
  def main() = {
    val car = new MiniCar("4wd")
    val card = new CharacterCard("super man")
    val o1:ToyBox[Toy] = new ToyBox[CharacterCard]( List(card) )
    val o2:ToyBox[Toy]  = o1.push( new MiniCar("sports car") )
    println(o2.toys)
  }
}

また、val o2ToyBox[Any]型でなく、型パラメータで指定されたToyBox[Toy]となる。