In a typical Scala upperbound example
abstract class Animal {
def name: String
}
abstract class Pet extends Animal {}
class Cat extends Pet {
override def name: String = "Cat"
}
class Dog extends Pet {
override def name: String = "Dog"
}
class Lion extends Animal {
override def name: String = "Lion"
}
What is the difference between this
class PetContainer[P <: Pet](p: P) {
def pet: P = p
}
val dogContainer = new PetContainer[Dog](new Dog)
val catContainer = new PetContainer[Cat](new Cat)
and this?
class PetContainer1(p: Pet) {
def pet: Pet = p
}
val dogContainer1 = new PetContainer1(new Dog)
val catContainer1 = new PetContainer1(new Cat)
What is the advantage of using an upper type bound vs using the abstract class/trait directly?
CodePudding user response:
With upper bound you can have a collection of specific subtype - so limiting to only cats or dogs and you can get a specific subtype back from def pet
. It's not true for PetContainer1
.
Losing more accurate type info example:
val doggo: Dog = new Dog
val dogContainer1 = new PetContainer1(doggo)
// the following won't compile
// val getDoggoBack: Dog = dogContainer1.pet
val dogContainer2 = new PetContainer[Dog](doggo)
// that works
val getDoggoBack: Dog = dogContainer2.pet
You can't put a cat into dog container:
// this will not compile
new PetContainer[Dog](new Cat)
It would get more significant if you would be dealing with collections of multiple elements.
CodePudding user response:
The main difference between the two is actually the difference between these two types:
def pet: P = p
// and
def pet: Pet = p
So, in the first example, the type of pet
is P
, and in the second example, the type of pet
is Pet
. In other words: in the first example, the type is more precise, since it is a specific subtype of Pet
.
If I put a Dog
in the PetContainer[Dog]
, I get a Dog
back out. Whereas, if I put a Dog
in the PetContainer1
, I get back a Pet
, which could be either a Cat
or a Dog
, or something else entirely. (You haven't made your classes sealed
or final
, so someone else could come along, and create their own subclass of Pet
.)
If you use an editor or IDE which shows you the types of expressions, or you try your code in the REPL or in Scastie, you actually see the difference:
dogContainer.pet // has type `Dog`
catContainer.pet // has type `Cat`
dogContainer1.pet // has type `Pet`
catContainer1.pet // has type `Pet`
See Scastie link for an example:
sealed trait Animal:
val name: String
sealed trait Pet extends Animal
case object Cat extends Pet:
override val name = "Cat"
case object Dog extends Pet:
override val name = "Dog"
case object Lion extends Animal:
override val name = "Lion"
final case class PetContainer[ P <: Pet](pet: P)
val dogContainer = PetContainer(Dog)
val catContainer = PetContainer(Cat)
final case class PetContainer1(pet: Pet)
val dogContainer1 = PetContainer1(Dog)
val catContainer1 = PetContainer1(Cat)
dogContainer.pet //=> Dog: Dog
catContainer.pet //=> Cat: Cat
dogContainer1.pet //=> Dog: Pet
catContainer1.pet //=> Cat: Pet
This gets more interesting if, say, you have a kennel that can hold two pets:
sealed trait Pet:
val name: String
case object Cat extends Pet:
override val name = "Cat"
case object Mouse extends Pet:
override val name = "Mouse"
final case class UnsafePetKennel(pet1: Pet, pet2: Pet)
val unsafeCatKennel = UnsafePetKennel(Cat, Cat)
val unsafeMouseKennel = UnsafePetKennel(Mouse, Mouse)
val oopsSomeoneAteMyMouse = UnsafePetKennel(Cat, Mouse)
final case class SafePetKennel[ P <: Pet](pet1: P, pet2: P)
val safeCatKennel = SafePetKennel[Cat.type](Cat, Cat)
val safeMouseKennel = SafePetKennel[Mouse.type](Mouse, Mouse)
val mouseIsSafe = SafePetKennel[Cat.type](Cat, Mouse)
// Type error: found `Mouse`, required `Cat`
val mouseStillSafe = SafePetKennel[Mouse.type](Cat, Mouse)
// Type error: found `Cat`, required `Mouse`
Unfortunately, things can still go wrong if we rely on Scala's type parameter inference:
val inferredKennel = SafePetKennel(Cat, Mouse)
works, because Scala infers P
to be the least upper bound of Cat
and Mouse
, which is Pet
, and thus inferredKennel
has type SafePetKennel[Pet]
, and since both Cat
and Mouse
are subtypes of Pet
, this is allowed.
We can fix that with a slight modification to our SafePetKennel
:
final case class SaferKennel[ P <: Pet, Q <: Pet](
pet1: P, pet2: Q)(implicit ev: P =:= Q)
val inferredKennel = SaferKennel(Cat, Mouse)
// Cannot prove that Cat =:= Mouse.