This enjoyable reality has been on my thoughts for some time, and a current reddit thread about “Smuggling Checked Exceptions with Sealed Interfaces” made me write this put up right here. Specifically, Java had union sorts earlier than it was cool! (In the event you squint laborious).
What are union sorts?
Ceylon is an underrated JVM language that by no means actually took off, which is just too dangerous, as a result of the ideas it launched are very elegant (see e.g. how they carried out nullable sorts as syntax sugar on prime of union sorts, which IMO is a lot better than something monadic utilizing Choice sorts or kotlin’s ad-hoc sort system extension).
So, a type of ideas are union sorts. Probably the most common language that helps them, presently, is TypeScript, although C++, PHP, and Python even have one thing related. (The very fact whether or not the union sort is tagged or not isn’t related to this put up).
In the event you perceive Java’s intersection sorts A & B
(which means that one thing is each a subtype of A and of B), then it’s straightforward to grasp union sorts A | B (which means that one thing is a subtype of any of A or B). TypeScript reveals a easy instance of this
perform printId(id: quantity | string) {
console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
printId({ myID: 22342 });
Structural vs nominal typing
Such a union sort (or intersection sort) is a structural sort, versus what we’ve been doing in Java by way of nominal sorts, the place it’s important to declare a named sort for this union each time you need to use it. E.g. in jOOQ, we have now issues like:
interface FieldOrRow {}
interface Discipline<T> extends FieldOrRow {}
interface Row extends FieldOrRow {}
Very quickly, the Java 17 distribution will seal the above sort hierarchy as follows (incomplete, subtypes of Discipline<T>
and Row
are omitted for brevity):
sealed interface FieldOrRow permits Discipline<T>, Row {}
sealed interface Discipline<T> extends FieldOrRow permits ... {}
sealed interface Row extends FieldOrRow permits ... {}
There are professionals and cons for doing this stuff structurally or nominally:
Structural typing:
- Professional: You may create any ad-hoc union of any set of sorts anyplace
- Professional: You don’t have to alter the prevailing sort hierarchy, which is crucial whenever you don’t have entry to it, e.g. whenever you need to do one thing just like the above
quantity | string
sort. This kinda works like JSR 308 sort annotations that had been launched in Java 8.
Nominal typing:
- Professional: You may connect documentation to the kind, and reuse it formally (somewhat than structurally). TypeScript and lots of different languages provide sort aliases for this type of stuff, so you possibly can have a little bit of each worlds, although the alias is erased, which means you retain the complicated structural sort resulting in curious error messages.
- Professional: You may seal the kind hierarchy to permit for exhaustiveness checking amongst subtypes (e.g. above, there can solely be
Discipline<T>
orRow
subtypes ofFieldOrRow
. A structurally typed union sort is implicitly “sealed” ad-hoc by the union sort description (unsure if that’s the way it’s referred to as), however with nominal sorts, you can also make certain nobody else can prolong the kind hierarchy, (besides the place you allow it explicitly utilizing thenon-sealed
key phrase)
In the end, as ever so typically, issues like structural and nominal typing are two sides of the identical coin, professionals and cons largely relying on style and on how a lot you management a code base.
So, how are checked exceptions union sorts?
While you declare a technique that throws checked exceptions, the return sort of the tactic is basically such a union sort. Have a look at this instance in Java:
public String getTitle(int id) throws SQLException;
The decision-site now has to “examine” the results of this technique name utilizing try-catch, or declare re-throwing the checked exception(s):
strive {
String title = getTitle(1);
doSomethingWith(title);
}
catch (SQLException e) {
deal with(e);
}
If early Java had union sorts somewhat than checked exceptions, we’d have declared this as follows, as an alternative:
public String|SQLException getTitle(int id);
Likewise, a caller of this technique must “examine” the results of this technique name. There’s no easy means of re-throwing it, so if we do need to re-throw, we’d want some syntax sugar, or repeat the identical code on a regular basis, Go-style:
// Hypothetical Java syntax:
String|SQLException end result = getTitle(1);
change (end result) {
case String title -> doSomethingWith(title);
case SQLException e -> deal with(e);
}
It might be apparent how such a JEP 406 type change sample matching assertion or expression may implement an exhaustiveness examine, identical to with the prevailing JEP 409 sealed courses method, the one distinction, once more, being that all the pieces is now structurally typed, somewhat than nominally typed.
In truth, when you declare a number of checked exceptions, such because the JDK’s reflection API:
public Object invoke(Object obj, Object... args)
throws
IllegalAccessException,
IllegalArgumentException,
InvocationTargetException
With union sorts, this might simply be this, as an alternative:
// Hypothetical Java syntax:
public Object
| IllegalAccessException
| IllegalArgumentException
| InvocationTargetException invoke(Object obj, Object... args)
And the union sort syntax from the catch block, which checks for exhaustiveness (sure, we have now union sorts in catch
!)…
strive {
Object returnValue = technique.invoke(obj);
doSomethingWith(returnValue);
}
catch (IllegalAccessException | IllegalArgumentException e) {
handle1(e);
}
catch (InvocationTargetException e) {
handle2(e);
}
May nonetheless examine for exhaustiveness with the change sample matching method:
// Hypothetical Java syntax:
Object
| IllegalAccessException
| IllegalArgumentException
| InvocationTargetException end result = technique.invoke(obj);
change (end result) {
case IllegalAccessException,
IllegalArgumentException e -> handle1(e);
case InvocationTargetException e -> handle2(e);
case Object returnValue = doSomethingWith(returnValue);
}
A refined caveat right here is that exceptions are subtypes of Object
, so we should put that case on the finish, because it “dominates” the others (see JEP 406 for a dialogue about dominance). Once more, we are able to show exhaustiveness, as a result of all kinds which might be concerned within the union sort have a change case.
Can we emulate union sorts with checked exceptions?
You recognize what Jeff Goldblum would say
However this weblog is understood to do it anyway. Assuming that for each potential sort, we had an artificial (code generated?) checked exception that wraps it (as a result of in Java, exceptions will not be allowed to be generic):
// Use some "protecting" base class, so nobody can introduce
// RuntimeExceptions to the kind hierarchy
class E extends Exception {
// Simply in case you are doing this in efficiency delicate code...
@Override
public Throwable fillInStackTrace() {
return this;
}
}
// Now create a wrapper exception for each sort you need to characterize
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 advantage of that is we don’t have to attend for Valhalla to help primitive sorts in generics, nor to reify them. We’ve already emulated that as you possibly can see above.
Subsequent, we’d like a change emulation for arbitrary levels (22 will most likely be sufficient?). Right here’s one for diploma 2:
// Create an arbitrary variety of change utilities for every arity up
// to, say 22 as is greatest apply
class Switch2<E1 extends E, E2 extends E> {
E1 e1;
E2 e2;
non-public 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 examine() throws E1, E2 {
if (e1 != null)
throw e1;
else
throw e2;
}
}
And at last, right here’s how we are able to emulate our exhaustiveness checking change with catch blocks!
// "Union sort" emulating String|int
Switch2<EString, Eint> s = Switch2.of1(new EString("good day"));
// Would not compile, Eint is not caught (catches aren't exhaustive)
strive {
s.examine();
}
catch (EString e) {}
// Compiles effective
strive {
s.examine();
}
catch (EString e) {}
catch (Eint e) {}
// Additionally compiles effective
strive {
s.examine();
}
catch (EString | Eint e) {}
// Would not compile as a result of Eint "partially dominates" EString | Eint
strive {
s.examine();
}
catch (Eint e) {}
catch (EString | Eint e) {}
“Neat”, huh? We may even think about destructuring throughout the catch block, such that we are able to mechanically unwrap the worth from the auxiliary “E” sort.
Since we have already got “union sorts” in Java (in catch blocks), and since checked exception declarations may very well be retrofitted to type a union sort with the tactic’s precise return sort, my hopes are nonetheless that in some distant future, a extra highly effective Java will probably be obtainable the place these “union sorts” (and in addition intersection sorts) will probably be made first-class. APIs like jOOQ would drastically revenue from this!