Type variables are a powerful piece of type-level programming. They can appear in a variety of forms and factors; phantoms and existential types being two cases. In this article we will be exploring existential types via a specific use case you may encounter in production code.
But before we can talk about existential types, we must talk about type variables in general.
Normally, this is the form in which we see type variables:
A is said to be a “type variable”. Note that
A appears on both sides of the
F and on
SomeClass. This means that
F is fully dependent on
the type of the data for
SomeClass. For example, all of these are valid:
While the following isn’t:
Now let’s expand this into something more concrete (with traits and classes):
This is pretty straight forward and you probably aren’t impressed, but it is important to see the basic example since existential types (and phantom ones) are a variation on it.
Existential types are like the normal type variables we saw above, except the variable only shows up on the right:
A only appears on the right side, which means that the final type,
will not change regardless of what
A is. For example:
Side note: if
A were to only appear on the left side, then
A would be a
Phantom type. We aren’t covering those in this article though, so let’s ignore
Like before, let’s now expand it. Beware, this expansion isn’t as clean as the previous one:
Here we’re using path dependent types in order to create a better interface for
Existential trait. We could just have an empty trait but that would mean
that we would need to case match
MkEx whenever we wanted to access its
fields. However, the concept still holds and shares the properties we described
We could think of
MkEx as a type eraser: it doesn’t matter what type of data
we choose to put into
MkEx, it will erase the type and always return
Time for a game, let’s say you have some function that, when called,
Existential. What is the type of the inner
The answer is
ex.Inner of course. But what is
ex.Inner? The answer to
that is that we can’t know. The original type defined in
MkEx has been erased
from compilation, never to be seen again. Now that’s not to say that we have
lost all information about it. We know that it exists (we have a reference to
ex.Inner), hence why it is called “existential”, but sadly that’s
pretty much all the information we know about it.
This means that, in its current form,
MkEx are useless. We
ex.value anywhere expect where
ex.Inner is required, but even
ex.Inner is pretty bare bones with no properties for us to use: so once we
accept it in a function, what do we do with it? Well nothing right now, but we
could add restrictions to the type, which in terms would allow us to do
something with it without knowing what it is.
And here is where Existential types shine: they have the interesting property of unifying different types into a single one with shared restrictions. These restrictions could be anything: from upper bounds to type classes or even a combination. To show this we will be creating a type-safe wrapper around an unsafe Java library.
Let’s say you have a Java library that contains the following signature:
This signature isn’t special, many SQL Java libraries have a method similar to
it. Normally you would define some string with many question marks (?) followed
by a call to a
bind method that would bind the passed objects to the question
marks (hopefully sanitizing the input in the process). Like so:
The problem though is that
Object is a very general thing. Obviously, if we
user is some class we defined, that wouldn’t work.
However, it would compile and crash at runtime. Additionally, a lot of these
Java libraries are, well, for Java, so certain Scala types won’t work either
(eg: BigInt, Int, List). In fact, you could think of
Object as an
The problem is that it is too wide, it encompasses ALL types. This all means
that we need to somehow “restrict” the number of types that are allowed to go
bind. This new bind, we shall call it
- Convert types to their Java counterpart (BigInt -> Long, Int -> Integer)
- Fail to compile any nonsensical types (User, Profile)
We aren’t going to be making the length of parameters passed safe (that would require a lot more than existentials). Nor are we checking that the types passed match the expected types in the SQL query.
safeBind will be defined something like so:
safeBind doesn’t look that much different that
bind except for
AnyAllowedType. But before we can talk about
AnyAllowedType, we need to
define what types are allowed. For this, we will use a typeclass, called
AllowedType, since they lend themselves well for defining a set of types
unrelated types that have a specific functionality.
This typeclass is defined as:
The stuff in the companion object are just helper functions we will use later. Now let’s define some types that will be allowed through:
This gives us our filter and converter: we can only call
A implements our typeclass.
Great, now we need to define
AnyAllowedType. As we saw above, we want
AnyAllowedType to behave somewhat like
Object, but for our small set of
types. We can achieve this using an Existential type but with an evidence for
This existential is similar to the
Existential we defined above, except that
we are now asking for an evidence for
AllowedType[A] and capturing it as part
of the trait. This means that, unlike before, we can’t pass any random type
Great! Now we have our
AnyAllowedType defined. Let’s see it in action by
Note that we still don’t know what the type of
ex.value is (just like before).
However, we do know that it is the same type as the evidence
That means that all functions, that are part of the evidence (ie: typeclass),
match the type of
ex.value! So our knowledge of
ex.value has expanded from,
“all we know is that it exists” to “we know it exists AND that it implements
the typeclass AllowedType”.
Finally, when we go to use
safeBind, we do as such:
And it works! But the following will not:
We are essentially done now, we just need to wrap our values in
MkAnyAllowedType and the compiler will do the rest (or yell).
However, there are some extra tweaks we can make to make our interface better.
Making an AllowedType instance for AnyAllowedType
You many have noticed that it is awkward to call functions in
We can make this better by creating an instance for
Using implicit conversions to avoid wrapping
Having to do this manual wrapping can become old fast:
We can actually avoid it by using an implicit conversion:
Now we can simply call:
user will fail, like before.
When we created
AnyAllowedType, we made it for the typeclass
Does this mean that we need to make a new
AnyX for every
X typeclass we
have? Nope, we do not. We can generalize
AnyAllowedType to work for ANY
typeclass. This would require a simple modification:
Now, instead of hard-coding it to
AllowedType, we take the typeclass as a
TC[_]. We still take a
TC[A] implicitly in
MkTCBox along with the
value. Note though that
TC[_] isn’t existential, nor phantom, it is just a
common type variable. A
TCBox[TC] is, essentially, a “TypeClass Box” for the
Our implicit conversion can also be translated:
Finally, a simple type alias
type AnyAllowedType = TCBox[AllowedType] would
make everything we have written before keep working.
Existential types don’t seem that useful when first encountered, but they can be quite powerful when mixed with the correct restrictions. The use case we presented is one of the simpler uses but they can be as complex or as simple as your use case requires them to be.
PS: you can find all the code we just wrote for TCBox here: https://gist.github.com/pjrt/269ddd1d8036374c648dbf6d52fb388f