Effect cases in switch
forax at univ-mlv.fr
forax at univ-mlv.fr
Wed Dec 13 12:49:22 UTC 2023
> From: "Brian Goetz" <brian.goetz at oracle.com>
> To: "Remi Forax" <forax at univ-mlv.fr>
> Cc: "amber-spec-experts" <amber-spec-experts at openjdk.java.net>
> Sent: Wednesday, December 13, 2023 12:04:09 AM
> Subject: Re: Effect cases in switch
>>> Exception cases can be used in all forms of `switch`: expression and statement
>>> switches, switches that use traditional (colon) or single-consequence (arrow)
>>> case labels. Exception cases can have guards like any other pattern case.
>> I think I would prefer "case throws" to be spell "catch" even if we have to have
>> a discussion about catch(Throwable t) vs catch Throwable t.
> Ah, you've stepped in the trap. You know the rule: if you make a syntax comment,
> you've implicitly signed up for all the semantics. Glad to know you're on board
> :)
> We knew that someone (everyone?) would ask about this, because its kind of the
> obvious choice. But having thought about it for quite a while, I'm firmly
> convinced that this is in the "obvious but wrong" department. PLEASE, let's not
> have a back-and-forth on this, because I don't want a substantive discussion to
> be derailed by a syntax triviality (we can return to this topic after the
> substantitive discussions have played out), but I will summarize some of the
> reasons why this is not what we want, because I think they are relevant to the
> goals of the feature:
> - The semantics would be extremely confusing, because a `case throws` matches
> only exceptions thrown _in evaluating the selector_, just like other cases
> match the result of a successful evaluation of the selector. But if we
> expressed this as `switch ... catch`, users would forever be assuming,
> incorrectly, that they mean to be catching all errors from the switch block,
> including both those from the selector and those from the body. That is *not*
> what this construct is about.
> - The *whole point* of this feature is allowing evaluation failures to be
> handled consistently and uniformly with successful evaluation. Having a
> different syntax for handling failures does not help and does not highlight the
> uniformity. Again, that is not what this constructs is about.
> The keyword `catch` is familiar but that is a very short-lived benefit. The
> purpose of the feature is to allow uniform handling of results and effects; the
> syntax should reflect that.
I do not like case throws. I see why you like it, you want to do a case but not on the value of the expression but on the exception raised from the exception.
It has also the advantage of being clears that a "case throw" can not catch exception like MatchingException that are. throws while executing a matching.
But syntactically, if a "case throws" throws an exception, we have throws and throw on the same line, which is just awful to read
case throws Exception e -> throw new AppException(e);
Also it does not convey the right semantics, a "case throws" is not a "case", you can not mix it with the other cases and the merging of exceptions does not works like the merging of values,
Some like case throws RuntimeException | Error e -> ... should be valid.
For now, i think that __rescue is a better keyword, because unlike case and catch it does not have any existing semantics attached.
>>> Exception cases have the obvious dominance order with other exception cases (the
>>> same one used to validate order of `catch` clauses in `try-catch`), and do not
>>> participate in dominance ordering with non-exceptional cases. It is a
>>> compile-time error if an exception case specifies an exception type that cannot
>>> be thrown by the selector expression, or a type that does not extend
>>> `Throwable`. For clarity, exception cases should probably come after all other
>>> non-exceptional cases.
>>> When evaluating a `switch` statement or expression, the selector expression is
>>> evaluated. If evaluation of the selector expression throws an exception, and
>>> one of the exception cases in the `switch` matches the exception, then control
>>> is transferred to the first exception case matching the exception. If no
>>> exception case matches the exception, then the switch completes abruptly with
>>> that same exception.
>> I don't want to be the guy implementing this :)
> Good news, Jan has volunteered to be that guy :)
Are you sure you do not want a VM guy too ?
>> I understand the appeal of such construct, it's a way to switch (eheh) from a
>> world with exceptions to a more functional world where errors are values.
> And even more so, to allow existing effectful APIs (like Future::get) to be
> consumed as uniformly as they are implemented.
Future::get as several issues, for me, the main one is that Callable/Future does not tracks the exception type so the cause of an ExecutionException is a Throwable so you are required to do a pattern-matching on the cause to repropagate at least the Error correctly.
That said, exceptions are a good candidates for a deconstructor, i'm sure people will want to write
switch (future.get()) {
...
__rescue ExecutionException(Error error) -> throw error;
}
---
Also, I believe we are in trouble if the expression of the switch is typed as AutoCloseable,
var paths = switch(Files.list(path)) {
Stream<Path> stream -> yield stream.toList();
__rescue IOException _ -> List.of();
};
because users will want the switch to call close() on an AutoCloseable but it's not a backward compatible change.
regards,
Rémi
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-spec-observers/attachments/20231213/c32d1f8c/attachment-0001.htm>
More information about the amber-spec-observers
mailing list