Accumulator
Accumulator

Reputation: 43

Disfunctionality of type parameter

I’m new to using Scala and am trying to see if a list contains any objects of a certain type.

When I make a method to do this, I get the following results:

var l = List("Some string", 3)
def containsType[T] = l.exists(_.isInstanceOf[T])
containsType[Boolean]   // val res0: Boolean = true
l.exists(_.isInstanceOf[Boolean])   // val res1: Boolean = false

Could someone please help me understand why my method doesn’t return the same results as the expression on the last line?

Thank you, Johan

Upvotes: 2

Views: 122

Answers (2)

Alin Gabriel Arhip
Alin Gabriel Arhip

Reputation: 2638

Scala uses the type erasure model of generics. This means that no information about type arguments is kept at runtime, so there's no way to determine at runtime the specific type arguments of the given List object. All the system can do is determine that a value is a List of some arbitrary type parameters.

You can verify this behavior by trying any List concrete type:

  val l = List("Some string", 3)

  println(l.isInstanceOf[List[Int]])       // true
  println(l.isInstanceOf[List[String]])    // true
  println(l.isInstanceOf[List[Boolean]])   // also true
  println(l.isInstanceOf[List[Unit]])      // also true

Now regarding your example:

  def containsType[T] = l.exists(_.isInstanceOf[T])
 
  println(containsType[Int])        // true
  println(containsType[Boolean])    // also true
  println(containsType[Unit])       // also true
  println(containsType[Double])     // also true

isInstanceOf is a synthetic function (a function generated by the Scala compiler at compile-time, usually to work around the underlying JVM limitations) and does not work the way you would expect with generic type arguments like T, because after compilation, this would normally be equivalent in Java to instanceof T which, by the way - is illegal in Java.

Why is illegal? Because of type erasure. Type erasure means all your generic code (generic classes, generic methods, etc.) is converted to non-generic code. This usually means 3 things:

  1. all type parameters in generic types are replaced with their bounds or Object if they are unbounded;
  2. wherever necessary the compiler inserts type casts to preserve type-safety;
  3. bridge methods are generated if needed to preserve polymorphism of all generic methods.

However, in the case of instanceof T, the JVM cannot differentiate between types of T at execution time, so this makes no sense. The type used with instanceof has to be reifiable, meaning that all information about the type needs to be available at runtime. This property does not apply to generic types.

So if Java forbids this because it can't work, why does Scala even allows it? The Scala compiler is indeed more permissive here, but for one good reason; because it treats it differently. Like the Java compiler, the Scala compiler also erases all generic code at compile-time, but since isInstanceOf is a synthetic function in Scala, calls to it using generic type arguments such as isInstanceOf[T] are replaced during compilation with instanceof Object.

Here's a sample of your code decompiled:

public <T> boolean containsType() {
    return this.l().exists(x$1 -> BoxesRunTime.boxToBoolean(x$1 instanceof Object));
}

Main$.l = (List<Object>)package$.MODULE$.List().apply((Seq)ScalaRunTime$.MODULE$.wrapIntArray(new int[] { 1, 2, 3 }));
Predef$.MODULE$.println((Object)BoxesRunTime.boxToBoolean(this.containsType()));
Predef$.MODULE$.println((Object)BoxesRunTime.boxToBoolean(this.containsType()));

This is why no matter what type you give to the polymorphic function containsType, it will always result in true. Basically, containsType[T] is equivalent to containsType[_] from Scala's perspective - which actually makes sense because a generic type T, without any upper bounds, is just a placeholder for type Any in Scala. Because Scala cannot have raw types, you cannot for example, create a List without providing a type parameter, so every List must be a List of "something", and that "something" is at least an Any, if not given a more specific type.

Therefore, isInstanceOf can only be called with specific (concrete) type arguments like Boolean, Double, String, etc. That is why, this works as expected:

  println(l.exists(_.isInstanceOf[Boolean]))   // false

We said that Scala is more permissive, but that does not mean you get away without a warning. To alert you of the possibly non-intuitive runtime behavior, the Scala compiler does usually emit unchecked warnings. For example, if you had run your code in the Scala interpreter (or compile it using scalac), you would have received this:

enter image description here

Upvotes: 2

Silvio Mayolo
Silvio Mayolo

Reputation: 70257

Alin's answer details perfectly why the generic isn't available at runtime. You can get a bit closer to what you want with the magic of ClassTag, but you still have to be conscious of some issues with Java generics.

import scala.reflect.ClassTag

var l = List("Some string", 3)

def containsType[T](implicit cls: ClassTag[T]): Boolean = {
  l.exists(cls.runtimeClass.isInstance(_))
}

Now, whenever you call containsType, a hidden extra argument of type ClassTag[T] gets passed it. So when you write, for instance, println(containsType[String]), then this gets compiled to

scala.this.Predef.println($anon.this.containsType[String](ClassTag.apply[String](classOf[java.lang.String])))

An extra argument gets passed to containsType, namely ClassTag.apply[String](classOf[java.lang.String]). That's a really long winded way of explicitly passing a Class<String>, which is what you'd have to do in Java manually. And java.lang.Class has an isInstance function.

Now, this will mostly work, but there are still major caveats. Generics arguments are completely erased at runtime, so this won't help you distinguish between an Option[Int] and an Option[String] in your list, for instance. As far as the JVM is concerned, they're both Option.

Second, Java has an unfortunate history with primitive types, so containsType[Int] will actually be false in your case, despite the fact that the 3 in your list is actually an Int. This is because, in Java, generics can only be class types, not primitives, so a generic List can never contain int (note the lowercase 'i', this is considered a fundamentally different thing in Java than a class).

Scala paints over a lot of these low-level details, but the cracks show through in situations like this. Scala sees that you're constructing a list of Strings and Ints, so it wants to construct a list of the common supertype of the two, which is Any (strings and ints have no common supertype more specific than Any). At runtime, Scala Int can translate to either int (the primitive) or Integer (the object). Scala will favor the former for efficiency, but when storing in generic containers, it can't use a primitive type. So while Scala thinks that your list l contains a String and an Int, Java thinks that it contains a String and a java.lang.Integer. And to make things even crazier, both int and java.lang.Integer have distinct Class instances.

So summon[ClassTag[Int]] in Scala is java.lang.Integer.TYPE, which is a Class<Integer> instance representing the primitive type int (yes, the non-class type int has a Class instance representing it). While summon[ClassTag[java.lang.Integer]] is java.lang.Integer::class, a distinct Class<Integer> representing the non-primitive type Integer. And at runtime, your list contains the latter.

In summary, generics in Java are a hot mess. Scala does its best to work with what it has, but when you start playing with reflection (which ClassTag does), you have to start thinking about these problems.

println(containsType[Boolean]) // false
println(containsType[Double])  // false
println(containsType[Int])     // false (list can't contain primitive type)
println(containsType[Integer]) // true  (3 is converted to an Integer)
println(containsType[String])  // true  (class type so it works the way you expect)
println(containsType[Unit])    // false
println(containsType[Long])    // false

Upvotes: 3

Related Questions