Generic function output for covariance type is wrong, is it a type system bug or expected?

I am sorry to repost it here as I did not get a satisfying answer on Stackoverflow

Consider

trait Fruit { 
  val name: String
}
case class Apple(override val name: String) extends Fruit
case class Banana(override val name: String) extends Fruit

When I define a function to collect only specific types

def onlyT[T](list: List[Fruit]): List[T] = list.collect { { case x: T => x } }
def onlyT2[T: ClassTag](list: List[Fruit]): List[T] = list.collect { { case x: T => x } }
val fruitList: List[Fruit] = List(Apple("app1"), Apple("app2"), Apple("app3"), Apple("app4"), Banana("ban1"), Banana("ban2"), Banana("ban3"))

And below val a is of type List[Banana] but contains entries of type Apple.

val a: List[Banana] = onlyT[Banana](fruitList)
val b: List[Banana] = fruitList.collect { case x: Banana => x }
val c: List[Banana] = onlyT2[Banana](fruitList)
println(a) // List(Apple(app1), Apple(app2), Apple(app3), Apple(app4), Banana(ban1), Banana(ban2), Banana(ban3))
println(b) // List(Banana(ban1), Banana(ban2), Banana(ban3))
println(c) // List(Banana(ban1), Banana(ban2), Banana(ban3))

Is this expected? Or is it a typesystem bug? I know at runtime type is erased, but not sure why it works for b but not a? I think the compiler should not allow a if it will be like this. Anything I am missing?

FYI, this is not a question to solve the issue I can resolve it by using ClassTag. The question is: Why is scala compiler allowing the type declaration val a: List[Banana] and we end up with a list of apples and bananas of type List[Banana]

I believe this breaks the type system, it acts as if this is a dynamically typed language.

Is that an expected problem with type erasure? The problem is I don’t even have a compiler warning for the line val a: List[Banana] ... only warning for onlyT definition

I get a warning

the type test for T cannot be checked at runtime because it refers to an abstract type member or type parameter

Maybe compiler people can explain.

Shouldn’t we see a warning for the line (or error)

val a: List[Banana] = onlyT[Banana](fruitList)

as a ends up with the values:
List(Apple(app1), Apple(app2), Apple(app3), Apple(app4), Banana(ban1), Banana(ban2), Banana(ban3))

No need to apologize, and welcome by the way! (Forgot to say it before.) :wave:

Also try asking on Discord

1 Like

You already got a warning when you compiled onlyT. Warnings at definition sites aren’t viral, they’d don’t propagate to call sites.

In my opinion, the warning you got when you compiled onlyT should be a hard error, not merely a warning. The type check you requested in the body of onlyT (namely : T) literally cannot be performed on the JVM, because of type erasure.

4 Likes

Yes, due to type erasure.

No, because you are essentially bypassing the type system by doing a class check : T.
You will see that informally people call it a “type check” but that name is wrong. You can’t check types at runtime, only classes; this is why I recommend staying away from it.

Because for b you are checking for the class Banana. Whereas for a you are checking for whatever T erased, most probably Object.

And the reason why c fixes it is because you are explicitly preserving the right class. Just like with b but in a generic way.

As @SethTisue said. a probably warned about it.
If it should be an error is a long discussion, I would say it should, further I would say : Foo checks should be disabled by default. But not everyone agrees.

1 Like

The answer is that you can’t check at run-time if a value matches a generic type because the generic type has been erased.

In the code below, the Scala 3 compiler issues a warning on the partial function passed to the collect method call, as it should.

  trait Fruit
  def onlyT[T](list: List[Fruit]): List[T] = list.collect { case x: T => x }

Warning: the type test for T cannot be checked at runtime because it refers to an abstract type member or type parameter

In your code, you can’t match against the generic type T (well, you can, but it will always match), but you can match against the Banana type because it is not a generic type.

Thanks a lot for the detailed response, didn’t know about the warnings not propagating. I imagine onlyT is being part of a third party library, let’s say, a badly implemented third party jar, this would make my program inconsistent without any warnings. I guess need to be more careful when using libraries, “signature is most you need to look for in scala when functions are pure” is not much that true when the function is generic (and prone to type erasure) then

No one would implement a library function that would compile with this obvious Scala compiler warning, unless the library implementer has no clue what is type erasure on the JVM. I find that the Scala compilers are really good at spotting type errors at compile time. I had a number of subtle type errors in my code in the past, and the compilers, either 2 or 3, were always correct in their diagnostic. Best regards.

2 Likes

Uhm no,
A generic pure function will be fine and you probably only need to look at the signature most of the time.

But, doing a runtime class check is not pure.
Well, it kind of depends on how you define pure, but the point here is that they are breaking the rules and as such assumptions can’t be held.

Anyways, yeah, I agree that you will never find this bug in the wild unless someone is both completely unaware of type erasure and actively silence / ignore the warning.
If that happens, then that pretty much tells you not to use that library.

2 Likes