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