Problem with bounded type parameterised case class and default args in Scala

Kristian Domagala picture Kristian Domagala · May 26, 2011 · Viewed 7.1k times · Source

Consider the following (tested with Scala 2.8.1 and 2.9.0):

trait Animal
class Dog extends Animal

case class AnimalsList[A <: Animal](list:List[A] = List())
case class AnimalsMap[A <: Animal](map:Map[String,A] = Map())

val dogList = AnimalsList[Dog]()  // Compiles
val dogMap = AnimalsMap[Dog]()    // Does not compile

The last line fails with:

error: type mismatch;
 found   : scala.collection.immutable.Map[Nothing,Nothing]
 required: Map[String,Main.Dog]
Note: Nothing <: String, but trait Map is invariant in type A.
You may wish to investigate a wildcard type such as `_ <: String`. (SLS 3.2.10)
Error occurred in an application involving default arguments.
    val dogMap = AnimalsMap[Dog]()    // Does not compile
                       ^
one error found

Changing it to val dogMap = AnimalsMap[Dog](Map()) fixes it, but no longer takes advantage of the default argument value.

Why is the default value being inferred as Map[Nothing,Nothing], given that List counterpart works as expected? Is there a way to create an AnimalsMap instance that uses the default value for the map arg?


Edit: I've accepted an answer to my more-pressing second question, but I'd still be interested to know why the key type to Map() is inferred differently between these two cases:

case class AnimalsMap1(map:Map[String,Animal] = Map())
val dogs1 = AnimalsMap1() // Compiles

case class AnimalsMap2[A <: Animal](map:Map[String,A] = Map())
val dogs2 = AnimalsMap2[Dog]() // Does not compile

Edit 2: Seems that type bounds are irrelevant - any parametric type to the case class causes the problem:

case class Map3[A](map:Map[String,A] = Map())
val dogs3 = Map3[Dog]() // Does not compile

Answer

IttayD picture IttayD · May 26, 2011

Scala has a feature where you can define a class to be covariant / contravariant in its generic parameters.

As an example to covariance: it is natural to think that if class Student extends Person then List[Student] "extends" List[Person]. This is because every method that accepts a List[Person] should have no problem working with an object List[Student]. This is not possible in Java (without making the method also generic).

Contravariance is the opposite and is a bit trickier to explain. It is required when the type is supposed to be pushed to the generic class, not read (in List[Person] you read the elements of the list). The general example is a function. The types of the arguments to a function are put into it, so if a method expects a function Person => String it can't be called with a function Student => String (it will call the argument with a person, but it expects a student)

Scala also defines Nothing to implicitly extend everything. It is the bottom type. So List[Nothing] always "extends" List[X] for any X. List() creates List[Nothing] and the covariance is why you can write val x: List[Person] = List().

Anyways, Map is invariant in its key types. The reason is that a Map[A, B] is like a function A => B so it can only be contravariant in A. Another way is to think what happens if you pass a Map[Student, String] to a method expecting Map[Person, String], obviously it may try to put Person objects in there which is not good, the other way is OK. On the other hand, a Map can be viewed as Iterable[(A, B)], here it should be covariant in A. So it is invariant in its value.

The result is you cannot assign a Map[Nothing, Nothing] to a variable of type Map[String, Animal]. Map() creates a Map[Nothing, Nothing]

The compiler tells you this:

scala> val dogs3 = Map3[Dog]()
<console>:13: error: type mismatch;
 found   : scala.collection.immutable.Map[Nothing,Nothing]
 required: Map[String,Dog]
Note: Nothing <: String, but trait Map is invariant in type A.
You may wish to investigate a wildcard type such as `_ <: String`. (SLS 3.2.10)
Error occurred in an application involving default arguments.
       val dogs3 = Map3[Dog]()
                       ^