wangzaixiang
wangzaixiang

Reputation: 43

Why does type inference fails in this case?

The following code works well:

object InfDemo {    
  class Tag[T]
  case object IntegerTag extends Tag[Int]
  case object StringTag extends Tag[String]
  val TagOfInteger: Tag[Int] = IntegerTag

  def defaultValue[T](typ: Tag[T]): T = typ match {
    case IntegerTag => 0
    case StringTag => ""
    // case TagOfInteger => 0 // this not works
  }     
}

but the following code will report type inference error:

object InfDemo2 {
    val ClassOfInteger: Class[Integer] = classOf[Integer]
    val ClassOfString : Class[String] = classOf[String]
    def defaultValue[T](typ: Class[T]): T = typ match {
      case ClassOfInteger => 0
      case ClassOfString => ""
   }
}

So what is the difference between these code, and how scala does the type inference here?

Upvotes: 2

Views: 230

Answers (1)

Régis Jean-Gilles
Régis Jean-Gilles

Reputation: 32739

The problem has nothing to do with using Class over Tag, and all to do with matching against a case object (such as IntegerTag and StringTag) over matching against a mere value (such as TagOfInteger, ClassOfInteger and ClassOfString).

Let's try to compile 4 variants of your first example:

Version 1:

class Tag[T]
case object IntegerTag extends Tag[Int]
case object StringTag extends Tag[String]
def defaultValue[T](typ: Tag[T]): T = typ match {
  case IntegerTag => 0
  case StringTag => ""
}     

Version 2:

class Tag[T]
case class IntegerTag() extends Tag[Int]
case class StringTag() extends Tag[String]
def defaultValue[T](typ: Tag[T]): T = typ match {
  case IntegerTag() => 0
  case StringTag() => ""
}     

Version 3:

class Tag[T]
class IntegerTag extends Tag[Int]
class StringTag extends Tag[String]

def defaultValue[T](typ: Tag[T]): T = typ match {
  case _: IntegerTag => 0
  case _: StringTag => ""
}     

Version 4:

class Tag[T]
val IntegerTag: Tag[Int] = new Tag[Int]
val StringTag: Tag[String] = new Tag[String]
def defaultValue[T](typ: Tag[T]): T = typ match {
  case IntegerTag => 0 // error: type mismatch
  case StringTag => "" // error: type mismatch
} 

If you try to compile them you'll see that version 1, 2 and 3 compile fine, while version 4 does not. The reason is that in version 1, 2 and 3, the pattern matching allows the compiler to know for sure which type is T:

  • In version 1 we do case IntegerTag =>. Because IntegerTag is a case object, we know for sure that there cannot be any instance that is equal to IntegerTag (except for IntegerTag itself). So if there is a match here, the runtime type of IntegerTag can only be IntegerTag, which extends Tag[Int]. Thus we can safely infer that T = Int.

  • In version 2 we do case IntegerTag() =>. Here IntegerTag is a case class, and as such we know that there can only bea match here if typ is an instance of IntegerTag, which extends Tag[Int]. Thus we can safely infer that T = Int.

  • In version 3 we do case _: IntegerTag =>. In other words, we explictly match against the IntegerTag type. So once again we know that typ is of type IntegerTag, which extends Tag[Int], and we can safely infer that T = Int.

Now, the problem with version 4 is that we have no guarantee about the runtime type of typ. This is because in this version we just do case IntegerTag =>, where IntegerTag is a val. In other words, there will be a match if and only if typ == IntegerTag. The problem is that the fact that typ is equal to IntegerTag (or in other words that typ.==(IntegerTag) returns true) tells us nothing about the runtime type of typ. Indeed, one can very well redefine equality in such a way that it can be equal to instance of unrelated classes (or simply be equal to instances of the same generic class but with different type arguments). By example, consider:

val StringTag: Tag[String] = new Tag[String]
val IntegerTag: Tag[Int] = new Tag[Int] { 
  override def equals( obj: Any ) = {
    (obj.asInstanceOf[AnyRef] eq this) || (obj.asInstanceOf[AnyRef] eq StringTag)
  }
}
println(StringTag == StringTag) // prints true
println(StringTag == IntegerTag) // prints false
println(IntegerTag == IntegerTag) // prints true
println(IntegerTag == StringTag) // prints true

IntegerTag == StringTag returns true, which means that if we passed StringTag to method defaultValue, there would be a match with case IntegerTag =>, even though StringTagactuall is an instance of Tag[String] rather than of Tag[Int]. This shows that indeed the fact the there is a match for case IntegerTag => tells us nothing regarding the runtime type of typ. And so the compiler cannot assume anything about the exact type of typ: we only know from its declared static type that it is a Tag[T] but T is still unknown.

Upvotes: 6

Related Questions