How to Use the Scala ClassTag
Learn more about using the Scala ClassTag to make runtime a lot easier.
Join the DZone community and get the full member experience.
Join For FreeAccording to Scala Standard Library documentation, ClassTag
is defined as below.
A ClassTag[T]
stores the erased class of a given type T
, accessible via the runtimeClass
field. This is particularly useful for instantiating Array
s whose element types are unknown at compile time.
So, whenever we want to access the type information during runtime, then we can use ClassTag
.
Without further ado, let us dive into our use case.
Use Case
Given a Map[String, Any]
, we want to check whether the Map
contains a value of required type for a given key. If such a value exists, then return the value and handle it gracefully. Sounds simple, right? Let us see how we approach this. Let us not dive into the discussion of whether using the Any
type in the Map
is correct. Let us assume that such a situation exists in an application and proceed with our task at hand to understand ClassTag
.
First Attempt
The following is our first attempt to meet our use case.
def main(args: Array[String]): Unit = {
class Animal
val myMap:collection.Map[String, Any] = Map("Number" -> 1, "Greeting"->"Hello World",
"Animal" -> new Animal)
/* the following will not work because of compilation error
* we are trying to assining Any to Int
*/
//val number:Int = myMap("Number")
//println("number is " + number)
// Now this works because we are casting the value into Int
val number:Int = myMap("Number").asInstanceOf[Int]
println("number is " + number)
//the following will throw ClassCastException
val greeting:String = myMap("Number").asInstanceOf[String]
}
Obviously, there are few problems in the above code.
First, the compiler will complain about seeing the following line because we are trying to assign the value of "Any" type to an "Int" type:
// this will not compile
val number:Int = myMap("Number")
Yes, the compiler is correct, but the point is that we are not able to use the type we know that we will be getting out of our Map. In other words, we are not leveraging rich type system provided by Scala if we just assign the value to "Any" type. We need some way to fix this.
So, to make the compiler happy, we resort to casting. We are now able to assign the value from Map to an Int variable without any compiler error.
val number:Int = myMap("Number").asInstanceOf[Int]
This works, but the asInstanceOf
will throw a ClassCastException
when we try to do casting between incompatible types (say because of unintentional user mistake).
So, the following is a problem at runtime because we are trying to cast Int value to String.
val greeting:String = myMap("Number").asInstanceOf[String]
We are introducing a new problem by fixing one.
Even if we go with the get()
method of Map, we will not be able to assign Option[Any]
to Option[Int]
because the compiler won't be happy again and we are back to square one.
// The following will not compile assigning Option[Any] to Option[Int]
val number:Option[Int] = myMap.get("Number")
Now, you might be wondering why there a Map[String, Any]
exists in any application. Though it is a code smell, let us mute our design expertise for a while and assume there is a scenario where Map[String, Any]
exists in an application. Let us go back to our problem.
Obviously, using asInstanceOf
is not a good practice, and as we see above, there is a possibility of the ClassCastException
if we try to do cast between incompatible types (ex: String to Int).
One way to handle the ClassCastException
is to surround it with try/catch, and if we take that approach, it is not really going to be a scalable and elegant solution. So, let us not attempt that.
Second Attempt
// getValueFromMap for the Int, String and Animal
def getValueFromMapForInt(key:String, dataMap: collection.Map[String, Any]): Option[Int] =
dataMap.get(key) match {
case Some(value:Int) => Some(value)
case _ => None
}
def getValueFromMapForString(key:String, dataMap: collection.Map[String, Any]): Option[String] =
dataMap.get(key) match {
case Some(value:String) => Some(value)
case _ => None
}
def getValueFromMapForAnimal(key:String, dataMap: collection.Map[String, Any]): Option[Animal] =
dataMap.get(key) match {
case Some(value:Animal) => Some(value)
case _ => None
}
def main(args: Array[String]): Unit = {
class Animal {
override def toString = "I am Animal"
}
val myMap:collection.Map[String, Any] = Map("Number" -> 1, "Greeting"->"Hello World",
"Animal" -> new Animal)
// returns Some(1)
val number1:Option[Int] = getValueFromMapForInt("Number", myMap)
println("number is " + number1)
// returns None
val numberNotExists:Option[Int] = getValueFromMapForInt("Number2", myMap)
println("number is " + numberNotExists)
println
// returns Some(Hello World)
val greeting:Option[String] = getValueFromMapForString("Greeting", myMap)
println("greeting is " + greeting)
// returns None
val greetingDoesNotExists:Option[String] = getValueFromMapForString("Greeting1", myMap)
println("greeting is " + greetingDoesNotExists)
println()
// returns Some[Animal]
val animal:Option[Animal] = getValueFromMapForAnimal("Animal", myMap)
println("Animal is " + animal)
// returns None
val animalDoesNotExist:Option[Animal] = getValueFromMapForAnimal("Animal1", myMap)
println("Animal is " + animalDoesNotExist)
}
The output of the above code is given below:
number is Some(1)
number is None
greeting is Some(Hello World)
greeting is None
Animal is Some(I am Animal)
Animal is None
Now, we have a solution to avoid the ClassCastException
. We have a getValueFromMapForXXX
method where XXX takes the type of value we are expecting from the map.
The problems we saw in the previous version are gone now. We don't have ClassCastException
because we are not doing any casting at all, and the compiler also catches us if we try to assign the wrong type without the use of asInstanceOf
.
But still, the solution is not good because it is not a scalable solution. Why? It is because now we have to introduce getValueFromMapForXXX
for every possible type (XXX) of values in the Map.
Third Attempt
Let us try to solve the scalability problem in our previous attempt by using the type parameter in our getValueFromMapForXXX
method.
def getValueFromMap[T](key:String, dataMap: collection.Map[String, Any]): Option[T] =
dataMap.get(key) match {
case Some(value:T) => Some(value)
case _ => None
}
Now, we have a single getValueFromMap[T]
method whose signature is the same as its previous version, but now, it takes the type parameter T.
// getValueFromMap which takes type parameter T
// Now this single method will give the values for Int, String and Animal
// from our Map
def getValueFromMap[T](key:String, dataMap: collection.Map[String, Any]): Option[T] =
dataMap.get(key) match {
case Some(value:T) => Some(value)
case _ => None
}
def main(args: Array[String]): Unit = {
class Animal {
override def toString = "I am Animal"
}
val myMap:collection.Map[String, Any] = Map("Number" -> 1, "Greeting"->"Hello World",
"Animal" -> new Animal)
// returns Some(1)
val number1:Option[Int] = getValueFromMap[Int]("Number", myMap)
println("number is " + number1)
// returns None
val numberNotExists:Option[Int] = getValueFromMap[Int]("Number2", myMap)
println("number is " + numberNotExists)
println
// returns Some(Hello World)
val greeting:Option[String] = getValueFromMap[String]("Greeting", myMap)
println("greeting is " + greeting)
// returns None
val greetingDoesNotExists:Option[String] = getValueFromMap[String]("Greeting1", myMap)
println("greeting is " + greetingDoesNotExists)
println()
// returns Some[Animal]
val animal:Option[Animal] = getValueFromMap[Animal]("Animal", myMap)
println("Animal is " + animal)
// returns None
val animalDoesNotExist:Option[Animal] = getValueFromMap[Animal]("Animal1", myMap)
println("Animal is " + animalDoesNotExist)
println
// PROBLEM STARTS HERE
// The following is really a problem and
// now there is nothing in the control of compiler.
// Even if the return from getValueFromMap is Option[String]
// the compiler does not complain because it is all happening at runtime.
// The compiler does not have any control over the runtime behavior now.
// The consequence is that it
// gives a problem when we take this Option value and go for
// some integer tranformation
//Wow, assigning Option[String] to Option[Int]
//Still works. No one complains
val greetingInt:Option[Int] = getValueFromMap[Int]("Greeting", myMap)
// prints Some(Hello World)
println("greetingInt is " + greetingInt)
// The problem comes here and now it will throw the ClassCastException
val somevalue = greetingInt.map((x) => x + 5)
// The following will not get printed at all
println(somevalue)
}
The output of the above code is given below:
number is Some(1)
number is None
greeting is Some(Hello World)
greeting is None
Animal is Some(I am Animal)
Animal is None
greetingInt is Some(Hello World)
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at scala.runtime.BoxesRunTime.unboxToInt(BoxesRunTime.java:101)
at scala.runtime.java8.JFunction1$mcII$sp.apply(JFunction1$mcII$sp.java:12)
at scala.Option.map(Option.scala:146)
Now, all we have to do is just call the method by passing the type we are expecting, the Key, and the Map to get the value from our Map. So, to get the Int value from our Map for key "Number," all we have to do is call the method with this information.
val number1:Option[Int] = getValueFromMap[Int]("Number", myMap)
So now, we have a scalable solution because we have only a single getValueFromMap
method and we don't have any other issues like compiler error for incompatible types or runtime ClassCastException
due to performing asInstanceOf
between incompatible types.
In our third attempt, we addressed the following:
- The user is more aware of the types she is dealing with now, which means there won't be any unnecessary runtime errors because of user ignorance. In other words, the user is now more knowledgeable about the types she is dealing with instead of assigning a value to supertype "Any" and later cast it to the right type.
- No
asInstanceOf
casting is required. If the key is found, then we will getSome[value]
, otherwise we getNone
. - It is scalable
So far, so good. But the nasty error happens, as seen above, when the user mistakenly uses the wrong type and the value exists.
For instance, the following works fine, but it should not.
//Wow, assigning Option[String] to Option[Int]
val greetingInt:Option[Int] = getValueFromMap[Int]("Greeting", myMap)
We know that the "Greeting" key in our map has a "String" value, but we are assigning it to Option[Int]
. The surprising part is that it works. It even prints the value.
// prints Some(Hello World)
println("greetingInt is " + greetingInt)
This will lead to surprising results when we take this value and do some monadic transformations.
The following will give ClassCastException
.
// The problem comes here and now it will throw the ClassCastException
val somevalue = greetingInt.map((x) => x + 5)
// The following will not get printed at all because of above exception
println(somevalue)
Is it not our method getValueFromMap[T]
supposed to capture this?
Is there a problem in that method? Let us see the method again.
def getValueFromMap[T](key:String, dataMap: collection.Map[String, Any]): Option[T] =
dataMap.get(key) match {
case Some(value:T) => Some(value)
case _ => None
}
The method looks good. All we are doing is call the get()
method on the key passed and check whether there is a "value" of the passed "T" exists. Basically, we are looking to see whether the Some(value:T)
exists, and if it exists, then we are just returning it. Otherwise, it should return None
.
So, in our case, when we pass the key "Greeting" and type "Int," it should check whether Some(value:Int)
exists for the key "Greeting" and return the Some(value)
if the value exists in the map and, otherwise, None. Obviously, in this case, it should return "None" because the "Greeting" key has the value "Hello World," but it is a String and not Int. But what we asked for is Int value while calling the method by passing the key "Greeting."
The question is why did not the following case statement inside the method did not catch this.
case Some(value:T) => Some(value)
The problem is that the runtime only looks to see whether it has some value for the key and did not check for whether the value belongs to the passed type "T." Why? The runtime does not have any information about the type we passed at all. During execution time, the "T" (in our case "Int") is erased and does not exist. This is called Type Erasure in the JVM world. That is why the method fails to capture this mistake.
So now, we need to make sure that the type "T" we pass also exists in the runtime and aids the runtime in some logic. Welcome to the world of ClassTag
.
All we have to do is pass the ClassTag[T]
implicitly. So, if we want to get the value from the map of type Int, then we need to pass the ClassTag[Int]
object implicitly. Then, when calling the method, we don't need to provide implicit ClassTag,
and the compiler will automatically provide one for us.
So now, the getValueFromMap
will be as shown below:
def getValueFromMap[T](key:String, dataMap: collection.Map[String, Any])(implicit t:ClassTag[T]): Option[T] = {
dataMap.get(key) match {
case Some(value: T) => Some(value)
case _ => None
}
}
The above method can also be written as below. Both the versions are same, but the below one is elegant.
def getValueFromMap[T : ClassTag](key:String, dataMap: collection.Map[String, Any]): Option[T] = {
dataMap.get(key) match {
case Some(value: T) => Some(value)
case _ => None
}
The caller does not change at all because the compiler provides the implicit ClassTag[T]
parameter.
Final Attempt
// getValueFromMap
def getValueFromMap[T : ClassTag](key:String, dataMap: collection.Map[String, Any]): Option[T] = {
dataMap.get(key) match {
case Some(value: T) => Some(value)
case _ => None
}
}
def main(args: Array[String]): Unit = {
class Animal {
override def toString = "I am Animal"
}
val myMap:collection.Map[String, Any] = Map("Number" -> 1, "Greeting"->"Hello World",
"Animal" -> new Animal)
// returns Some(1)
val number1:Option[Int] = getValueFromMap[Int]("Number", myMap)
println("number is " + number1)
// returns None
val numberNotExists:Option[Int] = getValueFromMap[Int]("Number2", myMap)
println("number is " + numberNotExists)
println
// returns Some(Hello World)
val greeting:Option[String] = getValueFromMap[String]("Greeting", myMap)
println("greeting is " + greeting)
// returns None
val greetingDoesNotExists:Option[String] = getValueFromMap[String]("Greeting1", myMap)
println("greeting is " + greetingDoesNotExists)
println()
// returns Some[Animal]
val animal:Option[Animal] = getValueFromMap[Animal]("Animal", myMap)
println("Animal is " + animal)
// returns None
val animalDoesNotExist:Option[Animal] = getValueFromMap[Animal]("Animal1", myMap)
println("Animal is " + animalDoesNotExist)
println
// NOW THERE IS NO PROBLEM BECAUSE WE WILL GET None
val greetingInt:Option[Int] = getValueFromMap[Int]("Greeting", myMap)
// prints None
println("greetingInt is " + greetingInt)
// No ClassCastException because it will give None
val somevalue = greetingInt.map((x) => x + 5)
// The following will print None
println(somevalue)
println
// other map with list
val someMap = Map("Number" -> 1, "Greeting"->"Hello World",
"Animal" -> new Animal, "goodList" -> List("good", "better", "best"))
// gets the list from map
val goodList:Option[List[String]] = getValueFromMap[List[String]]("goodList", someMap)
// prints the list
println(goodList)
println
println("Now let us try to get bad list")
// tries to get bad list from the map
val badListNotExists:Option[List[String]] = getValueFromMap[List[String]]("badList", someMap)
// prints None
println(badListNotExists)
}
The output produced by the above code is as follows:
number is Some(1)
number is None
greeting is Some(Hello World)
greeting is None
Animal is Some(I am Animal)
Animal is None
greetingInt is None
None
Some(List(good, better, best))
Now let us try to get bad list
None
Now, all our problems are gone, thanks to ClassTag
, which helps the runtime get information about the type parameter that we passed.
Conclusion
ClassTag
is useful in passing the information about the type parameter T to the runtime. It would be handy in many situations in our applications. However, it has its own limitation. We can only get the higher type information from the ClassTag
and not the argument type of the higher type passed.
For instance, in the above code, if we had used the following:
val goodList:Option[List[Int]] = getValueFromMap[List[Int]]("goodList", someMap)
// prints the list
println(goodList)
Instead of:
val goodList:Option[List[String]] = getValueFromMap[List[String]]("goodList", someMap)
// prints the list
println(goodList)
Still, it will work because the ClassTag
only provides the higher type information during runtime (in our case it is "List") and not the argument type information (in our case it is Int or String). That is why, even if we pass getValueFromMap[List[Int]]
for the key goodList
, the code will work. In most situations, we just want to need the higher kind and not the argument type during runtime. In case we need the argument type information along with the higher kind during runtime, then we have to go for the TypeTag
.
Opinions expressed by DZone contributors are their own.
Comments