Scala suggests using the covariance solution [+T] for the variance problem in generic class, and when I dig into it I could produce two solutions, the thing is that each one of them has its own disadvantage.
Solution 1 - using upper type boundary both in the class header and in the method, for example.
class Animal(val name: String)
class Dog(override val name: String, val property: String) extends Animal(name)
class Puppy(override val name: String, override val property: String, val age: Int) extends Dog(name, property)
abstract class AnimalCage[+A <: Animal] {
def head: A
def tail: AnimalCage[A]
def add[A <: Animal](animal: A): AnimalCage[Animal]
}
class RealCage[+A <: Animal](h: A, t: AnimalCage[A]) extends AnimalCage[A] {
override def head: A = h
override def tail: AnimalCage[A] = t
override def add[A <: Animal](animal: A): AnimalCage[Animal] = new RealCage(animal, this)
}
object EmptyCage extends AnimalCage[Nothing] {
override def head: Nothing = throw new NoSuchElementException()
override def tail: AnimalCage[Nothing] = throw new NoSuchElementException()
override def add[A <: Animal](animal: A): AnimalCage[Animal] = new RealCage(animal, this)
}
val dog1: Dog = new Dog("dog_1", "bark bark")
val dogCage: RealCage[Dog] = new RealCage(dog1, EmptyCage)
val puppy1: Puppy = new Puppy("puppy_1", "puppy bark", 5)
val x: AnimalCage[Dog] = dogCage.add(puppy1) //error! must be Animal, could not be Dog!
pros - [Animal] is the higher super-class Cage can holds
cons - When I'm adding [Puppy] (sub-type of [Dog]) to a Cage of [Dog] I can't have Cage of [Dog] but rather Cage of [Animal].
solution 2 - using boundary only in method signature
abstract class AnyCage[+A] {
def head: A
def tail: AnyCage[A]
def add[B >: A](animal: B): AnyCage[B]
}
class RealCage[+A](h: A, t: AnyCage[A]) extends AnyCage[A] {
override def head: A = h
override def tail: AnyCage[A] = t
override def add[B >: A](animal: B): AnyCage[B] = new RealCage(animal, this)
}
object EmptyCage extends AnyCage[Nothing] {
override def head: Nothing = throw new NoSuchElementException()
override def tail: AnyCage[Nothing] = throw new NoSuchElementException()
override def add[B >: Nothing](animal: B): AnyCage[B] = new RealCage(animal, this)
}
val dog1: Dog = new Dog("dog_2", "bark bark")
val dogCage: AnyCage[Dog] = new RealCage(dog1, EmptyCage)
val bug: LivingCreature = new LivingCreature("bug")
val generalCage: AnyCage[Any] = dogCage.add(bug) // compiled but we lose all properties due to the fact the [Any] is lowest super-type.
pros - When I'm adding [Puppy] (sub-type of [Dog]) to a Cage of [Dog], I get Cage of [Dog] and not [Animal] like the previous solution.
cons - we don't have restrictions on AnyCage[+A] and we can potentially add any type, even if it's not related to Animal, and then the lower super-type would be [Any] which will cause us to lose all the properties of [Animal].
Therefore, the question is, if there is any way to combine the above two solution and do something like -
abstract class AnimalCage[+A <: Animal] {
def head: A
def tail: AnimalCage[A]
override def add[A <: B <: Animal](animal: B): AnimalCage[B] = new RealCage(animal, this)
}
i.e, when we both can have restrictions on Cage (<: Animal) and also receive the lowest super-type (not [Animal] but [Dog]) in a case we want to add a [Puppy] to a Cage of [Dog].
Thanks