After a few minutes more thought, this approach can be simulated pretty
well by a library solution.
Suppose you wrote the following class (once):
sealed class Result<T, E extends Exception> {
record Succ<T, E extends Exception>(T t) extends Result<T,E> { }
record Fail<T, E extends Exception>(E e) extends Result<T,E> { }
interface ExceptionalSupplier<T, E extends Exception> {
T get() throws E;
}
static<T, E extends Exception> Result<T,E>
of(ExceptionalSupplier<T,E> s) {
try {
return new Succ(s.get());
}
catch (Exception e) {
return new Fail(e);
}
}
}
Note that no library needs to directly return Result; you can wrap an
exceptional computation with Result.of(() -> expr).
Now, you can switch using ordinary pattern matching:
switch (Result.of(() -> possiblyExceptionalExpression()) {
case Succ(T t) -> ...
case Fail(E e) -> ...
}
And note that nested patterns work just fine here:
switch (Result.of(() -> possiblyExceptionalExpression()) {
case Succ(T t) -> ...
case Fail(FooException e) -> ...
case Fail(BarException e) -> ...
}
(Critics will say "but, that's more ceremony", because you have to wrap
with a Result and then distinguish between Succ and Fail. Supporters
would say that this is a small price for saying what you mean.)
On 7/20/2021 9:15 AM, Brian Goetz wrote:
The following idea was received on amber-spec-comments.
Essentially, the OP is asking that, if we're extending switch to cover
more different kinds of cases, why not further extend it to treat an
exception in evaluating the target as another kind of matchable
result. It is a little like the current treatment of null, where, if
there is a `null` case, it is treated as a switchable value, and if
not, the switch completes abruptly with NPE.
While the questions are fair questions to ask, I see several
challenges with this approach:
- The motivations are mostly syntactic; `case X -> S` is more compact
than `catch (E e) { S }`. If these two had similar syntactic weight,
it is less likely such suggestions would be made.
- It only reduces the syntactic weight of try-catch in switches, but
not other constructs. This will surely be a "tomorrow's problems come
from today's 'solutions'" thing.
- People were uncomfortable about the "action at a distance" problem
with other aspects of switch; this creates new ones (you have to scan
all the cases to see which exceptions in evaluating the target are
handled.)
- The `catch` case labels are not being matched against the target of
the switch, making the basic model for the body of a switch more
complicated.
- There are two possible interpretations of the approach; the
desugaring suggested in the note seems the less sensible of the two to
me. They are:
- The switch target is evaluated. If evaluation completes
normally, the non-catch cases are tried, otherwise the switch
completes abruptly. If evaluation completes exceptionally, the catch
cases are tried, and if none of those match, the switch completes
abruptly. In this version, only exceptions thrown by evaluation of
the target can match.
- The entire switch is considered to be wrapped by a big try-catch
(this is what the OP suggests.) This means exceptions thrown from the
body of a previous case can also be caught by a later catch case.
If the motivation is to be more like a traditional Result<T,E>, the
former interpretation makes more sense, because it more directly
models switching over a Result<T,E>, with sugar to deconstruct the
Result wrapper.
-------- Forwarded Message --------
Subject: switch-case-catch
Date: Tue, 20 Jul 2021 09:01:14 +0300
From: Omar Aloraini <aloraini.o...@gmail.com>
To: amber-spec-comme...@openjdk.java.net
Hello,
With the introduction of sealed hierarchies and the enhancement of switch
expressions, I find myself wondering about the way I model errors.
Previously using a class `Result<T>` to model success or failure of an
operation was something that I used to do, but I quickly reverted as
all my
code base methods returned the `Result` type, sometime I introduce another
type to model the error type such as Result<T,E>, where E is typically a
subtype of Exception. But again things get cumbersome, fortunately we now
have sealed hierarchies, which allows us to model multiple possible
outcomes of an operation, or a closed set of error conditions. Client code
is not ugly anymore as switch expressions and pattern matching amend it to
become more concise and pleasant to read and write, or as someone whom I
respected would say ‘less ceremony’. Now I find myself wondering why not
use Exceptions? For some method A that returns a value of type *T* or
throws an exception of two possible types, something like `T A() throws
E1,E2`, obviously you will lose that pleasant client code with switch and
pattern matching. But what if we had switch-case-catch where the catch
clauses must cover all exceptions in the throws clause for it to compile.
Something like the following:
Object r = switch (o.A()){
case T t -> ...
catch E1 e -> ...
catch E1 e -> ...
}
Semantically it would equivalent to:
Object r;
try {
r = switch (o.A()) {
case T t -> ...
} catch (E1 e) {
r = ...
} catch (E2 e) {
r = ...
}
}
You will also get the benefit of pattern matching for the catch clause,
something like `catch E1 | E2`.
I don't think it's necessary to use the `catch` keyword, `case` might just
work.
Apologies if this was suggested before.