This fun fact has been on my mind for a while, and a recent reddit thread about “Smuggling Checked Exceptions with Sealed Interfaces” made me write this post here. Namely, Java had union types before it was cool! (If you squint hard).
What are union types?
Ceylon is an underrated JVM language that never really took off, which is too bad, because the concepts it introduced are very elegant (see e.g. how they implemented nullable types as syntax sugar on top of union types, which IMO is much better than anything monadic using Option types or kotlin’s ad-hoc type system extension).
So, one of those concepts are union types. One of the most popular language that supports them, currently, is TypeScript, though C++, PHP, and Python also have something similar. (The fact whether the union type is tagged or not isn’t relevant to this post).
If you understand Java’s intersection types A & B
(meaning that something is both a subtype of A and of B), then it’s easy to understand union types A | B (meaning that something is a subtype of any of A or B). TypeScript shows a simple example of this
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
printId({ myID: 22342 });
Structural vs nominal typing
Such a union type (or intersection type) is a structural type, as opposed to what we’ve been doing in Java via nominal types, where you have to declare a named type for this union every time you want to use it. E.g. in jOOQ, we have things like:
interface FieldOrRow {}
interface Field<T> extends FieldOrRow {}
interface Row extends FieldOrRow {}
Very soon, the Java 17 distribution will seal the above type hierarchy as follows (incomplete, subtypes of Field<T>
and Row
are omitted for brevity):
sealed interface FieldOrRow permits Field<T>, Row {}
sealed interface Field<T> extends FieldOrRow permits ... {}
sealed interface Row extends FieldOrRow permits ... {}
There are pros and cons for doing these things structurally or nominally:
Structural typing:
- Pro: You can create any ad-hoc union of any set of types anywhere
- Pro: You don’t have to change the existing type hierarchy, which is essential when you don’t have access to it, e.g. when you want to do something like the above
number | string
type. This kinda works like JSR 308 type annotations that were introduced in Java 8.
Nominal typing:
- Pro: You can attach documentation to the type, and reuse it formally (rather than structurally). TypeScript and many other languages offer type aliases for this kind of stuff, so you can have a bit of both worlds.
- Pro: You can seal the type hierarchy to allow for exhaustiveness checking among subtypes (e.g. above, there can only be
Field<T>
orRow
subtypes ofFieldOrRow
. A structurally typed union type is implicitly “sealed” ad-hoc by the union type description (not sure if that’s how it’s called), but with nominal types, you can make sure no one else can extend the type hierarchy, (except where you permit it explicitly using thenon-sealed
keyword)
Ultimately, as ever so often, things like structural and nominal typing are two sides of the same coin, pros and cons mostly depending on taste and on how much you control a code base.
So, how are checked exceptions union types?
When you declare a method that throws checked exceptions, the return type of the method is really such a union type. Look at this example in Java:
public String getTitle(int id) throws SQLException;
The call-site now has to “check” the result of this method call using try-catch, or declare re-throwing the checked exception(s):
try {
String title = getTitle(1);
doSomethingWith(title);
}
catch (SQLException e) {
handle(e);
}
If early Java had union types rather than checked exceptions, we might have declared this as follows, instead:
public String|SQLException getTitle(int id);
Likewise, a caller of this method will have to “check” the result of this method call. There’s no simple way of re-throwing it, so if we do want to re-throw, we’d need some syntax sugar, or repeat the same code all the time, Go-style:
// Hypothetical Java syntax:
String|SQLException result = getTitle(1);
switch (result) {
case String title -> doSomethingWith(title);
case SQLException e -> handle(e);
}
It would be obvious how such a JEP 406 style switch pattern matching statement or expression could implement an exhaustiveness check, just like with the existing JEP 409 sealed classes approach, the only difference, again, being that everything is now structurally typed, rather than nominally typed.
In fact, if you declare multiple checked exceptions, such as the JDK’s reflection API:
public Object invoke(Object obj, Object... args)
throws
IllegalAccessException,
IllegalArgumentException,
InvocationTargetException
With union types, this would just be this, instead:
public Object
| IllegalAccessException
| IllegalArgumentException
| InvocationTargetException invoke(Object obj, Object... args)
And the union type syntax from the catch block, which checks for exhaustiveness (yes, we have union types in catch
!)…
try {
Object returnValue = method.invoke(obj);
doSomethingWith(returnValue);
}
catch (IllegalAccessException | IllegalArgumentException e) {
handle1(e);
}
catch (InvocationTargetException e) {
handle2(e);
}
Could still check for exhaustiveness with the switch pattern matching approach:
// Hypothetical Java syntax:
Object
| IllegalAccessException
| IllegalArgumentException
| InvocationTargetException result = method.invoke(obj);
switch (result) {
case IllegalAccessException,
IllegalArgumentException e -> handle1(e);
case InvocationTargetException e -> handle2(e);
case Object returnValue = doSomethingWith(returnValue);
}
A subtle caveat here is that exceptions are subtypes of Object
, so we must put that case at the end, as it “dominates” the others (see JEP 406 for a discussion about dominance). Again, we can prove exhaustiveness, because all types that are involved in the union type have a switch case.
Can we emulate union types with checked exceptions?
You know what Jeff Goldblum would say
But this blog is known to do it anyway. Assuming that for every possible type, we had a synthetic (code generated?) checked exception that wraps it (because in Java, exceptions are not allowed to be generic):
// Use some "protective" base class, so no one can introduce
// RuntimeExceptions to the type hierarchy
class E extends Exception {
// Just in case you're doing this in performance sensitive code...
@Override
public Throwable fillInStackTrace() {
return this;
}
}
// Now create a wrapper exception for every type you want to represent
class EString extends E {
String s;
EString(String s) {
this.s = s;
}
}
class Eint extends E {
int i;
Eint(int i) {
this.i = i;
}
}
The benefit of this is we don’t have to wait for Valhalla to support primitive types in generics, nor to reify them. We’ve already emulated that as you can see above.
Next, we need a switch emulation for arbitrary degrees (22 will probably be enough?). Here’s one for degree 2:
// Create an arbitrary number of switch utilities for each arity up
// to, say 22 as is best practice
class Switch2<E1 extends E, E2 extends E> {
E1 e1;
E2 e2;
private Switch2(E1 e1, E2 e2) {
this.e1 = e1;
this.e2 = e2;
}
static <E1 extends E, E2 extends E> Switch2<E1, E2> of1(E1 e1) {
return new Switch2<>(e1, null);
}
static <E1 extends E, E2 extends E> Switch2<E1, E2> of2(E2 e2) {
return new Switch2<>(null, e2);
}
void check() throws E1, E2 {
if (e1 != null)
throw e1;
else
throw e2;
}
}
And finally, here’s how we can emulate our exhaustiveness checking switch with catch blocks!
// "Union type" emulating String|int
Switch2<EString, Eint> s = Switch2.of1(new EString("hello"));
// Doesn't compile, Eint isn't caught (catches aren't exhaustive)
try {
s.check();
}
catch (EString e) {}
// Compiles fine
try {
s.check();
}
catch (EString e) {}
catch (Eint e) {}
// Also compiles fine
try {
s.check();
}
catch (EString | Eint e) {}
// Doesn't compile
try {
s.check();
}
catch (Eint e) {}
catch (EString | Eint e) {}
“Neat”, huh? We could even imagine destructuring within the catch block, such that we can automatically unwrap the value from the auxiliary “E” type.
Since we already have “union types” in Java (in catch blocks), and since checked exception declarations could be retrofitted to form a union type with the method’s actual return type, my hopes are still that in some distant future, a more powerful Java will be available where these “union types” (and also intersection types) will be made first class. APIs like jOOQ would greatly profit from this!
from Java, SQL and jOOQ. https://ift.tt/3CAssjc
via IFTTT
No comments:
Post a Comment