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.