Curycu
Curycu

Reputation: 1605

Scala type lowerbound bug?

case class Level[B](b: B){
  def printCovariant[A<:B](a: A): Unit = println(a)
  def printInvariant(b: B): Unit = println(b)
  def printContravariant[C>:B](c: C): Unit = println(c)
}

class First
class Second extends First
class Third extends Second

//First >: Second >: Third

object Test extends App {

  val second = Level(new Second) //set B as Second

  //second.printCovariant(new First) //error and reasonable
  second.printCovariant(new Second) 
  second.printCovariant(new Third) 

  //second.printInvariant(new First) //error and reasonable
  second.printInvariant(new Second) 
  second.printInvariant(new Third) //why no error?

  second.printContravariant(new First) 
  second.printContravariant(new Second)
  second.printContravariant(new Third) //why no error?
}

It seems scala's lowerbound type checking has bugs... for invariant case and contravariant case.

I wonder above code are have bugs or not.

Upvotes: 2

Views: 64

Answers (1)

slouc
slouc

Reputation: 9698

Always keep in mind that if Third extends Second then whenever a Second is wanted, a Third can be provided. This is called subtype polymorhpism.

Having that in mind, it's natural that second.printInvariant(new Third) compiles. You provided a Third which is a subtype of Second, so it checks out. It's like providing an Apple to a method which takes a Fruit.

This means that your method

def printCovariant[A<:B](a: A): Unit = println(a)

can be written as:

def printCovariant(a: B): Unit = println(a)

without losing any information. Due to subtype polymorphism, the second one accepts B and all its subclasses, which is the same as the first one.

Same goes for your second error case - it's another case of subtype polymorphism. You can pass the new Third because Third is actually a Second (note that I'm using the "is-a" relationship between subclass and superclass taken from object-oriented notation).

In case you're wondering why do we even need upper bounds (isn't subtype polymorphism enough?), observe this example:

def foo1[A <: AnyRef](xs: A) = xs
def foo2(xs: AnyRef) = xs
val res1 = foo1("something") // res1 is a String
val res2 = foo2("something") // res2 is an Anyref

Now we do observe the difference. Even though subtype polymorphism will allow us to pass in a String in both cases, only method foo1 can reference the type of its argument (in our case a String). Method foo2 will happily take a String, but will not really know that it's a String. So, upper bounds can come in handy when you want to preserve the type (in your case you just print out the value so you don't really care about the type - all types have a toString method).

EDIT:
(extra details, you may already know this but I'll put it for completeness)

There are more uses of upper bounds then what I described here, but when parameterizing a method this is the most common scenario. When parameterizing a class, then you can use upper bounds to describe covariance and lower bounds to describe contravariance. For example,

class SomeClass[U] {

  def someMethod(foo: Foo[_ <: U]) = ???

}

says that parameter foo of method someMethod is covariant in its type. How's that? Well, normally (that is, without tweaking variance), subtype polymorphism wouldn't allow us to pass a Foo parameterized with a subtype of its type parameter. If T <: U, that doesn't mean that Foo[T] <: Foo[U]. We say that Foo is invariant in its type. But we just tweaked the method to accept Foo parameterized with U or any of its subtypes. Now that is effectively covariance. So, as long as someMethod is concerned - if some type T is a subtype of U, then Foo[T] is a subtype of Foo[U]. Great, we achieved covariance. But note that I said "as long as someMethod is concerned". Foo is covariant in its type in this method, but in others it may be invariant or contravariant.

This kind of variance declaration is called use-site variance because we declare the variance of a type at the point of its usage (here it's used as a method parameter type of someMethod). This is the only kind of variance declaration in, say, Java. When using use-site variance, you have watch out for the get-put principle (google it). Basically this principle says that we can only get stuff from covariant classes (we can't put) and vice versa for contravariant classes (we can put but can't get). In our case, we can demonstrate it like this:

class Foo[T] { def put(t: T): Unit = println("I put some T") }

def someMethod(foo: Foo[_ <: String]) = foo.put("asd") // won't compile
def someMethod2(foo: Foo[_ >: String]) = foo.put("asd")

More generally, we can only use covariant types as return types and contravariant types as parameter types.

Now, use-site declaration is nice, but in Scala it's much more common to take advantage of declaration-site variance (something Java doesn't have). This means that we would describe the variance of Foo's generic type at the point of defining Foo. We would simply say class Foo[+T]. Now we don't need to use bounds when writing methods that work with Foo; we proclaimed Foo to be permanently covariant in its type, in every use case and every scenario.

For more details about variance in Scala feel free to check out my blog post on this topic.

Upvotes: 4

Related Questions