post-photo

How are Scala Implicits more explicit than Java Generics?

This blogpost will show you how implicits can solve problems arising from/because of type erasure, proving that Scala implicits sometimes are more explicit than Java generics :D

All of these code snippets can be copy-pasted to the REPL. Some of the require-s will fail, those are just to demonstrate the expected work without the need of a testing framework.

text

Easy, class based ranking

So we need to create a problem for better understanding. Let’s say we have an email app, where we have Mail, and ImportantMail classes, and an Unread trait.

I want to list the mails on the UI by importance, and always want the unread emails higher than the read ones. So the desired sequence would be ImpotantMail with Unread, ImportantMail, Mail with Unread and last but not least Mail.

Our toycode would look like something like this:

class Mail()
class ImportantMail() extends Mail
trait Unread
 
def rank(mail: Mail): Int = {
  var rank = 0
  if(mail.isInstanceOf[Unread]) rank += 1
  if(mail.isInstanceOf[ImportantMail]) rank += 10
  rank
}
 
require(rank(new Mail())==0)
require(rank(new Mail() with Unread)==1)
require(rank(new ImportantMail())==10)
require(rank(new ImportantMail() with Unread)==11)

It’s not so pretty, I use var, and isInstanceOf, but it does the things I want to, so we will use it as a starting point.

(Ofc this can be achived if the Mail, ImportantMail etc have a rank function. However if the implementation of the data-types can not be modified, you need to come up with something like this.)

So I need to add this functionality to more places

I want to rank my todos or tasks or whatevers with this super ranking algo :) So I will modify it a bit. With some generics, it could be used for any classes or traits.

class Mail()
class ImportantMail() extends Mail
trait Unread
 
def rank[A,B](thing: AnyRef): Int = {
  var rank = 0
  if(thing.isInstanceOf[A]) rank += 1
  if(thing.isInstanceOf[B]) rank += 10
  rank
}
 
require(rank[Unread,ImportantMail](new Mail())==0)
require(rank[Unread,ImportantMail](new Mail() with Unread)==1)
require(rank[Unread,ImportantMail](new ImportantMail())==10)
require(rank[Unread,ImportantMail](new ImportantMail() with Unread)==11)

If you run this in REPL, the last require will be ok, but the others drop the IllegalArgumentException. We get a warning as well: “abstract type A is unchecked since it is eliminated by erasure”. What the hell is erasure? And whydid my rank function start to rank everything to 11? Let’s read the java docs about generics. It says that the generated bytecode will use the upper bounds of the generics, in cases where these are not specified (like there) it will use Object. The generated bytecode will be something unexpected at first, like:

def rank(thing: Object): Int = {
  var rank = 0
  if(thing.isInstanceOf[Object]) rank += 1
  if(thing.isInstanceOf[Object]) rank += 10
  rank
}

That’s bad. Those isInstanceOf checks will be degraded to if(true)-s. How can I match to a generic type?

Scala ClassTags and match-case

So as I said above, the isInstanceOf is evil, and if you want to match classes, you should use match-case instead. The problem with match-case in this particular problem is; it won’t let both cases run, so we need to match twice :( (If you know C/C++/C#/Java, all the switch cases can run on multiple branches if you left the break out. Most of the time it’s annoying. You need to type the breaks all the time, and if you forget, you get crazy-hard-to-reproduce bugs.) Our code with match-case:

def rank[A, B](thing: AnyRef): Int = {
  var rank = 0
  thing match {
    case _: A => rank += 1
    case _ =>
  }
  thing match {
    case _: B => rank += 10
    case _ =>
  }
  rank
}

Still unchecked warnings, and still not working :( But we have a special kind of weapon, the ClassTags. Here is the working code:

def rank[A, B](thing: AnyRef)(implicit tagA: ClassTag[A], tagB: ClassTag[B]): Int = {
  var rank = 0
  thing match {
    case _: A => rank += 1
    case _ =>
  }
  thing match {
    case _: B => rank += 10
    case _ =>
  }
  rank
}

And yes! Those two little implicits magically solved our problem, and the match started matching again. Before some explanation, we could add a little more refactor:

import scala.reflect.ClassTag
 
class Mail()
class ImportantMail() extends Mail
trait Unread
 
def isItMatch[A](thing: AnyRef, retValIfMatch: Int)(implicit tagA: ClassTag[A]): Int = {
  thing match {
    case _: A => retValIfMatch
    case _ => 0
  }
}
 
def rank[A, B](thing: AnyRef)(implicit tagA: ClassTag[A], tagB: ClassTag[B]): Int = {
  isItMatch[A](thing,1) + isItMatch[B](thing,10)
}
 
require(rank[Unread, ImportantMail](new Mail()) == 0)
require(rank[Unread, ImportantMail](new Mail() with Unread) == 1)
require(rank[Unread, ImportantMail](new ImportantMail()) == 10)
require(rank[Unread, ImportantMail](new ImportantMail() with Unread) == 11)

Much cleaner code, no more var, no more copypasted code, and the requires are still OK. So we can talk about the implicit magic here.

Most of the important points are in the official doc. The ClassTag is a partial type descriptior of the runtime scala type. These tags are generated by the compiler, and can be obtained as implicits. With these you can hijack the java compiler and its type erasure.

The docs mention a less verbose but shorter version, instead of adding those implicit parameters, you can include them in the type parameter list like below.

It’s worth mentioning that ClassTags can only show you the erased class information, so you will know it’s a List, but you will not know whather it’s a List[String] or a List[Int]. If you want class matching this deep, you need to go deeper in the docs too :)

def isItMatch[A: ClassTag](thing: AnyRef, retValIfMatch: Int): Int = {
  thing match {
    case _: A => retValIfMatch
    case _ => 0
  }
}
 
def rank[A: ClassTag, B: ClassTag](thing: AnyRef): Int = {
  isItMatch[A](thing,1) + isItMatch[B](thing,10)
}

All in all: Java generics v. Scala implicits

If you have never heard of the java type erasure, probably you are amazed by how could you use generics without this information, and if you as curious as I am you want to play with it a bit.

If you have never heard about the ClassTags, the above linked scala docs page could be a good read to see just how powerful and magical this tool can be.

If you already knew all the things above, you can maybe leave a comment with your suggestion as to how this can be more helpful to others :)

member photo

His precision is only coupled by his attention to detail. (Really.) He is passionate about becoming more and more effective in software development and loves experimenting with new technologies.

Latest post by Gergő Törcsvári

Learning by Doing – BlockChain & Akka Tutorial