An Ingenious Workaround to Emulate Sum Types in Java
Before I move on with the actual article, I’d like to give credit to Daniel Dietrich, author of the awesome Javaslang library, who has had the idea before me:
@lukaseder try with a static method <T, T1 extends T, … Tn extends T> Seq<T> toSeq(T1 t1, …, Tn tn) { … } (from my mobile phone…)
— Daniel Dietrich (@danieldietrich) February 16, 2016
Contravariant Generic Bounds
It all started with a tweet:
Didn't you ever wish to write <T super T1 & T2 & … & TN> in Java?
— Lukas Eder (@lukaseder) February 16, 2016
I wanted to do something like pattern-matching a common super type of a set of types, along the lines of:
<T super T1 | T2 | ... | TN>
Note that what I really wanted is support for union types, not intersection types as I originally claimed.
Why did I want to do that? Because it would be a nice addition to the jOOλ library, which features typesafe tuples for Java:
class Tuple3<T1, T2, T3> { final T1 v1; final T2 v2; final T3 v3; // Lots of useful stuff here }
What would be nice in a tuple is something like a forEach()
method that iterates over all attributes:
tuple(1, "a", null).forEach(System.out::println);
The above would simply yield:
1 "a" null
Now, what would this forEach()
method’s argument type be? It would look like this:
class Tuple3<T1, T2, T3> { void forEach(Consumer<? super T1 | T2 | T3> c) {} }
The consumer would receive an object that is of type T1 or T2 or T3. But a consumer that accepts a common super type of the previous three types is OK as well. For example, if we have:
Tuple2<Integer, Long> tuple = tuple(1, 2L); tuple.forEach(v->System.out.println(v.doubleValue()));
The above would compile, because Number
is a common super type of Integer
and Long
, and it contains a doubleValue()
method.
Unfortunately, this is not possible in Java
Java currently supports union / sum types (see also algebraic data types) only for exception catch blocks, where you can write things like:
class X extends RuntimeException { void print() {} } class X1 extends X {} class X2 extends X {} // With the above try { ... } catch (X1 | X2 e) { // This compiles for the same reasons! e.print(); }
But unfortunately, catch blocks are the only place in Java that allows for using sum types.
This is where Daniel’s clever and cunning workaround comes into play. We can write a static method that performs some “pattern-matching” (if you squint) using generics, the other way round:
static < T, T1 extends T, T2 extends T, T3 extends T > void forEach( Tuple3<T1, T2, T3> tuple, Consumer<? super T> consumer ) { consumer.accept(tuple.v1); consumer.accept(tuple.v2); consumer.accept(tuple.v3); }
The above can now be used typesafely to infer the common super type(s) of T1, T2, and T3:
Tuple2<Integer, Long> t = tuple(1, 2L); forEach(t, c -> { System.out.println(c.doubleValue()); });
yielding, as expected:
1.0 2.0
It makes sense, because the generic type constraints are simply specified “the other way round”, i.e. when T1 extends T
, forcibly, T super T1
…
If you squint really hard ;-)
This technique is supposedly used by Daniel in Javaslang’s upcoming pattern matching API. We’re looking forward to seeing that in action!
Reference: | An Ingenious Workaround to Emulate Sum Types in Java from our JCG partner Lukas Eder at the JAVA, SQL, AND JOOQ blog. |
Sooo, what’s the difference with this good old pattern ?
static void forEach(Tuple3 tuple, Consumer consumer) {
consumer.accept(tuple.v1);
consumer.accept(tuple.v2);
consumer.accept(tuple.v3);
}
Tuple3 t = new Tuple3(42, 0L, (byte) 1);
forEvery(t, e-> System.out.println(e.doubleValue()));
Not sure what you mean by “pattern”. Also, with only rawtypes (or without generics), how would you be able to dispatch to the doubleValue() method? The compiler doesn’t have any type info regarding the Tuple attributes being of a Number subtype…
I didn’t put raw types ; the comment system ate my angled brackets…
See this gist : https://gist.github.com/anonymous/1d1b6d4ac0b09b8429e4
As for “pattern”, I just wanted to say that merely deporting the (non-reused) type variable’s contraints to their declaration spot and using funky formatting is neither “pattern matching” nor “ingenious”. It’s just a confusing way to write a piece of code that can be expressed in a much simpler way (see gist).
Oh, I see. Yes, that’s an equivalent solution. Indeed, it is not really necessary to capture the types of T1-T3 in the
forEach()
method. Nice observation!