Home > Enterprise >  Scala upper bounds
Scala upper bounds

Time:09-17

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`

Scastie link

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.

Scastie link

  • Related