3
votes

Today I spent a few hours to understand the logic behind Lower Bounds in Scala but the more I read the more confusing it gets. Would you please shed some light on this?

Here is the simple class hierarchy for our workshop:

class Animal
class Pet extends Animal
class Wild extends Animal

class Dog extends Pet
class Cat extends Pet

class Lion extends Wild
class Tiger extends Wild

So the hierarchy would be something like:

        Animal
        /    \
      Pet    Wild
      / \    /  \
    Dog Cat Lion Tiger

And here is the client code:

 object Main extends App {
  //I expect the compilation of passing any type above Pet to fail
  def upperBound[T <: Pet](t: T) = {println(t.getClass.getName)}

  //I expect the compilation of passing any type below Pet to fail
  def lowerBound[T >: Pet](t: T) = {println(t.getClass.getName)}

  upperBound(new Dog)//Ok, As expected
  upperBound(new Cat)//Ok, As expected
  upperBound(new Pet)//Ok, As expected
  //Won't compile (as expected) because Animal is not a sub-type of Pet
  upperBound(new Animal)

  lowerBound(new Pet)//Ok, As expected
  lowerBound(new Animal)//Ok, As expected
  //I expected this to fail because Dog is not a super type of Pet
  lowerBound(new Dog)
  //I expected this to fail because Lion is not a super type of Pet either
  lowerBound(new Lion)
  lowerBound(100)//Jesus! What's happening here?!
  lowerBound(Nil)// Ok! I am out!!! :O
}

Well... The last four lines of the code does not make any sense to me! From what I understand, Lower Bound does not impose any constraints on the type parameter at all. Is there an implicit Bound to Any or AnyRef somewhere which I am missing?

3

3 Answers

3
votes

Let me explain the unexpected behaviour of bounded type inference

  1. Upper Bound(T <: Pet): This means T is applicable for all the classes which has inherited at least the Pet class or any of Pet's subclasses.
  2. Lower Bound(T >: Pet): This means T is applicable for all the classes which has inherited at least one of the parent class of Pet class.

So as you correctly guessed, AnyRef is a super type of all object/reference types. So when we say

lowerBound(new Dog())

Dog is under the class AnyRef. So by lower bound, since AnyRef is a parent of Pet, the compiler throws no warning.

You can see similar behaviour for the :: method of scala List class. With List you can do the following without any compile errors.

val list = List(1, 2, 3)

val newList = "Hello" :: list

For further reading, please take a look at these stack overflow answers:

  1. https://stackoverflow.com/a/19217523/4046067
  2. https://stackoverflow.com/a/19821995/4046067
1
votes

Lets see why some of these work. You can inspect the typed inferred by scalac by using the -Xprint:typer flag. This is what we see:

NewTest.this.lowerBound[tests.NewTest.Pet](new NewTest.this.Dog());
NewTest.this.lowerBound[tests.NewTest.Animal](new NewTest.this.Lion());
NewTest.this.lowerBound[Any](100)

For the case of Dog, the compiler looks for an ancestor that will match the lower bound requirement, which Pet satisfies since Pet >: Pet. For Lion, the ancestor matching the requirement is Animal, which works since Animal >: Pet. For the last one, the compiler infers Any, which is the highest in the Scala type hierarchy, which also works because Any >: Pet.

All of these work since the definition of a lower bound is that any type which is higher up the type hierarchy may be a candidate. This is why, if we take the most contrived example, passing an Int works, since the only common ancestor of Int and Pet is Any.

0
votes

You can get your expected results by changing the definition to

def lowerBound[T](t: T)(implicit ev: Pet <:< T) = ...

This tells the compiler to infer T (so it will be Lion, Int, and Nil.type in the last three cases) and then check if Pet is a subtype of T, instead of inferring T such that Pet is a subtype.