Before I move on with the actual article, I’d like to give credit to
Daniel Dietrich, author of the awesome
vavr library, who has had the idea before me:
Contravariant Generic Bounds
It all started with a tweet:
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 have to 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
(or, more formally,
Number & Comparable<?>
is a common super type of
Integer
and
Long
, and it contains a
doubleValue()
method.
As soon as we’re adding e.g. a
String
to the tuple, the following will no longer compile:
Tuple3<Integer, Long, String> tuple =
tuple(1, 2L, "A");
// Doesn't compile
tuple.forEach((Number v) ->
System.out.println(v.doubleValue()));
Unfortunately, this is not possible in Java
Java currently supports union types (
see also algebraic data types) only for exception catch blocks, where you can write things like:
interface X {
default void print() {}
}
class X1 extends RuntimeException implements X {}
class X2 extends RuntimeException implements 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 properties of union 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
vavr’s upcoming pattern matching API. We’re looking forward to seeing that in action!
Did you like this article?
Read also the following ones:
Like this:
Like Loading...
Nifty! You might also be interested in this post by Benji Weber: http://benjiweber.co.uk/blog/2015/08/07/anonymous-types-in-java in which he discusses the converse case, namely anonymous intersection types in Java.
Indeed, I remember having read that
Great write up. I had to try and see myself to believe it was actually working:
https://gist.github.com/noamik/d85f949d6cb83bce7e53
Yep, I know. It appears reasonable when you think about it. A lot in Java 8’s lambda type inference depend on this, even if these features were available already since Java 5. But it’s not very usual to think of generics this way in Java, which is why it’s so surprising.
Apache Commons ftw ?
http://commons.apache.org/proper/commons-lang/apidocs/index.html?org/apache/commons/lang3/tuple/package-summary.html
Autoboxing ftw ? (perhaps, depends on your actual final use case)
https://docs.oracle.com/javase/tutorial/java/data/autoboxing.html
Pair and triple, that’s impressive … :)
Now, how does your comment relate to the article, though? I mean, the article was about union types…
Why sould T1 T2 and T3 extend T? Why you can’t have two disjoint class heirarchies T1 and T2 (other than extending Object)
I’m not sure if I understand your question. The goal of the article is to emulate a union type and match the union’s common super type, if available. Of course, Object is always a common super type, but there might be a more useful common super type, as in the example:
java.lang.Number
.The example will let the compiler infer that
T extends Number
fromT1 extends Integer
andT2 extends Long
To be clear, this technique does not emulate union types, instead it demonstrates Java’s ability to infer the least upper bound. E.g., Number is the least upper bound of the union (Integer | Long).
You’re right, the title should read “… to Emulate an Application of Union Types …”
You may want to update the link to javaslang as it points to some ?Chinese? page unrelated to javaslang – appropriate link: https://github.com/vavr-io/vavr
Fixed, thanks!
How rude of me: loved the article btw