1. Overview

In Scala, the compiler removes all generic type information at compile-time, leaving this information missing at runtime. TypeTag and its counterparts like ClassTag and WeakTypeTag are solutions to obtaining the type information of the erased type at runtime.

In this tutorial, we’re going to look at the type erasure and how to obtain the type details at runtime.

2. Type Erasure

At runtime, every type parameter has been removed. Therefore, List[Int] and List[String] are considered the same.

Let’s define a List[Int] and a List[String]:

val intList: List[Int] = List(1, 2, 3)
val strList: List[String] = List("foo", "bar")

Let’s also define a function that checks the type of a parameter:

def checkType[A](xs: List[A]) = xs match {
  case _: List[String] => "List of Strings"
  case _: List[Int] => "List of Ints"
}

In this case, the compiler will warn us about the type erasure in the function above because, as was mentioned before, the type of List and the type parameter of List are erased. The result will be the same whether we call checkType() with a List[Int] or a List[String] parameter.

The compiler warns that the type patterns in our pattern matching are unchecked:

On line 2: warning: non-variable type argument String in type pattern List[String] (the underlying of List[String]) is unchecked since it is eliminated by erasure
           case _: List[Int] => "List of Ints"
                   ^
On line 3: warning: non-variable type argument Int in type pattern List[Int] (the underlying of List[Int]) is unchecked since it is eliminated by erasure
           case _: List[Int] => "List of Ints"

In this case, the checkType method will return on the first case value, case _: List[String]:

assert(checkType(intList) == "List of Strings")
assert(checkType(strList) == "List of Strings")

Next, we’re going to discuss methods to overcome problems like this.

3. Runtime Type Information

So, how can we obtain type information of variables at runtime? The Scala compiler has three ways to produce type information at runtime:

  1. TypeTag contains all type information at runtime.
  2. ClassTag obtains the runtime class of the type but doesn’t inform us about the type parameters.
  3. WeakTypeTag is a weaker version of TypeTag that obtains abstract type information.

Let’s take a look at each of these methods.

3.1. TypeTag

TypeTag is the strongest and most detailed way to obtain type information at runtime. If we instruct the compiler to find an implicit of TypeTag[T], we can easily obtain the type information:

def obtainTypeTag[T](implicit tt: TypeTag[T]) = tt

Now we can find the implicit TypeTag at runtime by calling this function on any type:

assert(obtainTypeTag[Int].tpe == Int)

Or, we can get information from nested types:

obtainTypeTag[List[Map[Int, (Long, String)]]]

Additionally, we can call the typeTag method, in the scala.reflect.runtime.universe package, to get a TypeTag[T].

TypeTag[T] has a tpe method that contains the reflective representation of type T. typeOf[T] is a shortcut method for typeTag[T].tpe.

Let’s update our checkType method to use TypeTag:

def checkType[T: TypeTag](v: T) = typeOf[T] match {
  case t if t =:= typeOf[List[String]] => "List of Strings"
  case t if t =:= typeOf[List[Int]] => "List of Ints"
}

assert(checkType(List("foo", "bar") == "List of Strings")
assert(checkType(Seq(1, 2, 3)) == "List of Ints")

After obtaining the type, we can use the =:=, <:<, and weak_<:< predicates to determine more information about that type.

3.2. ClassTag

Assume we want to write a function that creates a List of elements that have the same type:

def makeListFrom[T](elems: T*): List[T] = List[T](elems: _*)

We can instantiate a List from arbitrary elements by calling the makeListFrom(1, 2, 3) function.

How about creating an Array from a list of elements? It seems we can write it in the same way:

def makeArrayFrom[T](elems: T*): Array[T] = Array[T](elems: _*)

However, in this case, we’ll encounter an error:

No ClassTag available for T
def makeArrayFrom[T](elems: T*): Array[T] = Array[T](elems: _*)
                                                    ^
Compilation Failed

What is the difference between creating List and Array? Why does the compiler need ClassTag for type parameter T in the Array version? The short answer is that this is caused by how Array is represented in the JVM.

As we mentioned before, the compiler erases the type parameter of List[T] and converts it to a List. Therefore, type erasure will cause them to have the same class at runtime.

Arrays, on the other hand, have different classes in the JVM. The compiler type erasure doesn’t erase the type parameters of Arrays and distinguishes between Array[String] and Array[Int]. Therefore, they have different classes at runtime. We need the concrete type of Arrays at runtime to instantiate them. This is why we need ClassTag to preserve the type parameter T at runtime.

Let’s define a method that uses a ClassTag as a context-bound of type parameter:

def makeArrayFrom[T: ClassTag](elems: T*): Array[T] = Array[T](elems: _*)

Now, we can call the makeArrayFrom() function:

assert(makeArrayFrom(1, 2, 3) == Array(1, 2, 3))

3.3. WeakTypeTag

Until now, all of the types have been concrete. If the type is abstract, we can’t use TypeTag. This is where we need WeakTypeTag.

Let’s see an example method:

trait Foo {

  type Bar

  def barType = typeTag[Bar].tpe
}

Trait Foo has an abstract type, Bar. We have written barType, which exposes the reflective representation of type Bar. But this code doesn’t compile because the compiler cannot instantiate the proper TypeTag for abstract type Bar:

error: No TypeTag available for Foo.this.Bar

This is where WeakTypeTag comes into play. Let’s rewrite the barType method using WeakTypeTag:

trait Foo {

  type Bar

  def barType = weakTypeTag[Bar].tpe
}

Now, we can call the barType method:

assert(new Foo {
  override type Bar = Int
}.barType.toString == "Foo.this.Bar")

As our Bar type is abstract, the compiler can produce an implicit WeakTypeTag for that type, and it will be available at runtime.

4. Conclusion

In this article, we’ve discussed Scala type erasure and how to obtain type information at runtime using TypeTag, ClassTag, and WeakTypeTag. 

As usual, the full source code can be found over on GitHub.

guest
0 Comments
Inline Feedbacks
View all comments