How I discovered the DummyImplicit (in Scala)

Antoine Doeraene
Dec 9, 2019 · 4 min read

Recently we stumbled on a problem that at first glance looks particularly simple: overload a method for a `List[String]` and a `List[Int]`. One could expect that simply writing

def foo(ls: List[String]): Unit = ???
def foo(ls: List[Int]): Unit = ???

would do the trick, the same way as we would do

def foo(s: String): Unit = ???
def foo(j: Int): Unit = ???

As much as the latter will work like a charm, the former will gratify you with the following compile-time error:

[error] both methods
[error] def foo(ls: List[Int]): Unit at line 11 and
[error] def foo(ls: List[String]): Unit at line 12
[error] have same type after erasure: (ls: List)Unit
[error] def foo(ls: List[String]): Unit = ???
[error] ^

In the following, we’re going to explain why this happens, give the solution and explain why the solution works.

Why it fails

Actually, the compile error message says it all, given that we understand the words it uses. It says that the two functions will have the same type after “erasure”, namely a function from `List` to `Unit`. Where are the types `Int` and `String`, then? Well, that is exactly what type erasure is. At compile type, all type parameters are removed and they are no more available at runtime.

That means that the program will not know which method to call with the objects it receives, and that is what it is telling us.

The exact same phenomenon appears when you try to pattern match on a type parameter, like so:

val ls: List[Int] = ???
ls match {
case ls: List[Int] => ???
case ls: List[String] => ???
}

It’s a little “worse” in this case since you only have warnings, like so:

fruitless type test: a value of type List[Int] cannot also be a List[String] (the underlying of List[String]) (but still might match its erasure)
[warn] case ls: List[String] => ???
[warn] ^
[warn] /[...]/src/main/scala/main/Main.scala:15:30: unreachable code
[warn] case ls: List[String] => ???
[warn] ^

The second warning says that you’ll never enter that piece of code since type erasure will always make the `case ls: List[Int]` to match before. The first warning is irrelevant to our situation, and you would have something different by defining `val ls: List[Any] = ???`.

How to solve this

A way to solve this problem is to give the runtime a way to distinguish between the two versions. A basic idea would be to add a “dummy” parameter to one of the two. That way, it is easily possible to know whether to use the `String` or the `Int` version. For example, we could do

def foo(ls: List[String]): Unit = ???
def foo(ls: List[Int], u: Unit): Unit = ???

Calling `foo` with a list of integers would simply require to write `foo(ls, ())`.

This is a bit cumbersome, though, and may I dare to say particularly ugly. Moreover, intuitively, the compiler should be able to do such thing for us — it suffices for it to follow the types! This points us to another solution. Indeed, in Scala, whenever we want the compiler to do something for us, it suggests that *implicit* stuff could be involved in the picture.

Let’s tweak the previous solution like this

def foo(ls: List[String]): Unit = ???
def foo(ls: List[Int])(implicit u: Unit): Unit = ???

and in order to use it, all we need is to have a “dummy” implicit unit in scope, e.g. by doing somewhere

object MyDummyImplicitUnit {
implicit val $: Unit = ()
}

and importing it whenever we want to use `foo` with a list of integers. This will work because the implicit parameters are filled at compile time (where the type parameters are still known) and then at runtime there will be a concrete difference between the two methods (one will have an extra `Unit` parameter).

The `DummyImplicit`

Credit: World of Warcraft, Blizzard

By doing the previous trick, we’re approaching a nice solution because when we code, we can actually write `foo(ls)` with both a list of strings and a list of ints. But it is still not as convenient as it should be (we need to import our dummy Unit everywhere). One could also argue that it moreover forbids us to have other implicit methods that return Unit, although I think this is something that you should never do anyway.

But there’s actually a simpler (and nicer!) solution provided to us by a built-in nicety: the `DummyImplicit`. Let’s look at the source code:

package scala/** A type for which there is always an implicit value. */
final class DummyImplicit private ()
object DummyImplicit {
/** An implicit value yielding a `DummyImplicit`. */
implicit val dummyImplicit: DummyImplicit = new DummyImplicit
}

This is a simple class that has only one instance, which is conveniently implicit. Moreover, both the class and the instance `dummyImplicit` are always in the (implicit) scope! That’s great, because it’s all that we need. We can finally have a nice solution to overloaded methods with changing type parameter arguments:

def foo(ls: List[String]): Unit = ???
def foo(ls: List[Int])(implicit d: DummyImplicit): Unit = ???

It has all the advantages that we wanted (nothing to import, being able to effectively call `foo` with the correct overload) without any of the disadvantages.

Finally, what about overloading `foo` three times? Well, we can simply add more dummy implicit arguments:

def foo(ls: List[String]): Unit = ???
def foo(ls: List[Int])(implicit d: DummyImplicit): Unit = ???
def foo(ls: List[Double])(implicit d1: DummyImplicit, d2: DummyImplicit): Unit = ???

Conclusion

As we see, sometimes a simple problem can lead into a quite involved solution. As a mathematician, I think that these are actually the most beautiful kind of problems!

The price to pay in order to solve it is rather low, and the reward, be it that you need it, is worth it.

Antoine Doeraene

Written by

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade