懸念を整理したので、自分なりの理解を記録しておきます。
おもちゃ箱、ToyBox
におもちゃToy
のサブクラス、MiniCar
とCharacterCard
インスタンスを格納するという実装で考える。
初期実装
型パラメータを使わない各初期実装は以下の通り。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の変更
ToyBox
はToy
抽象に依存した実装なので、これをインスタンス生成時に型を確定する型パラメータとして指定できるよう変更を行う。
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) }
これは、B
はA
のスーパークラスである性質を意味する。つまり上記で、引数に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 o2
はToyBox[Any]
型でなく、型パラメータで指定されたToyBox[Toy]
となる。