> From: "Brian Goetz" <brian.go...@oracle.com>
> To: "Remi Forax" <fo...@univ-mlv.fr>
> Cc: "amber-spec-experts" <amber-spec-experts@openjdk.java.net>
> Sent: Thursday, February 17, 2022 3:43:08 PM
> Subject: Re: [External] : Re: Record patterns (and beyond): exceptions

>>> As we look ahead to record patterns, there is a new kind of remainder: the
>>> "spine" of nested record patterns. This includes things like Box(null),
>>> Box(novel), Box(Bag(null)), Box(Mapping(null, novel)), etc. It should be 
>>> clear
>>> that there is no clean extrapolation from what we currently do, to what we
>>> should do in these cases. But that's OK; both of the existing
>>> remainder-rejection cases derive from "what does the context think" -- 
>>> switch
>>> hates null (so, NPE), and enum switches are a thing (which makes ICCE on an
>>> enum switch reasonable.) But in the general case, we'll want some sort of
>>> MatchRemainderException.
>> Nope, it can not be a runtime exception because people will write code to 
>> catch
>> it and we will have a boat load of subtle bugs because exception are side
>> effect so you can see in which order the de-constructors or the pattern 
>> methods
>> are called. ICCE is fine.

> But this clearly does not fall into ICCE. ICCE means, basically, "your 
> classpath
> is borked"; that things that were known to be true at compile time are not 
> true
> at runtime. (Inconsistent separate compilation is the most common cause.) But
> Box(Bag(null)) is not an artifact of inconsistent separate compilation.
I think i've not understood the problem correctly, i was thinking the error was 
due to the erasure, Box<Bag> being erased to Box, the problem with erasure is 
that you see the problem late, in case of the switch after the phase that does 
instanceofs, so we end up with ClassCastException instead of ICCE. 

> In any case, I am not getting your point about "but people can catch it." So
> what? People can catch OOME too, and try to parse the output of toString() 
> when
> we tell them not to. But that's no reason to make all exceptions 
> "OpaqueError".
> So what is your point here?
You can catch OOME if you write the code by hand. People are using IDEs and 
when the IDE is lost or the user have click on the wrong button, 
catch(Exception) appears. 
That the reason why we have both IOError and UncheckedIOException in the JDK. 

>>> Note that throwing an exception from remainder is delayed until the last
>>> possible moment. We could have:

>>> case Box(Bag(var x)): ...
>>> case Box(var x) when x == null: ...

>>> and the reasonable treatment is to treat Box(Bag(var x)) as not matching
>>> Box(null), even though it is exhuastive on Box<Bag<?>>), and therefore fall
>>> into the second case on Box(null). Only when we reach the end of the switch,
>>> and we haven't matched any cases, do we throw MatchRemainderException.
>> I really dislike that idea, it will be a burden in the future each time we 
>> want
>> to change the implementation.
>> I would like the semantics to make no promise about when the error will be
>> thrown, the semantics should not be defined if a deconstructors/pattern 
>> method
>> does a side effect, the same way the stream semantics is not defined if the
>> lambda taken as parameter of Stream.map() does a side effect.
>> I think the parallel between the pattern matching and a stream in term of
>> execution semantics is important here. From the outside, those things are
>> monads, they should work the same way.

> I think this stems from the same misunderstanding you have about the boundary
> between the pattern semantics and the construct semantics. I'm going to
> test-drive some adjusted language here.

> A total pattern is just that -- it matches everything.

> Some patterns are considered exhaustive, but not total. A deconstruction 
> pattern
> D(E(total)) is one such example; it is exhaustive on D, but does not match
> D(null), because matching the nested E(total) requires invoking a 
> deconstructor
> in E, and you can't invoke an instance member on a null receiver. Still, we
> consider D(E(total)) exhaustive on D<E>, which means it is enough to satisfy
> the type checker that you've covered everything. Remainder is just the gap
> between exhaustiveness and totality.
The gap is due to E(...) not matching null, for me it's a NPE with an error 
message saying exactly that. 

> If we have the following switch:

> case D(E(Object o)):
> case D(var x) when x == null:

> the semantics of D(E(Object)) are that *it matches all non-null D, except
> D(null)*. So throwing when we evaluate the case would be incorrect; switch 
> asks
> the pattern "do you match", and the pattern says "no, I do not." And the
> semantics of switch, then, say "then I will keep trying the rest of the 
> cases."
What you are saying is that at runtime you need to know if a pattern is total 
or not, exactly you need to know if was decided to be total at compile, so at 
runtime you can decide to throw a NPE or not. 
Furthermore, if at runtime you detect that the total pattern is not total 
anymore, a ICCE should be raised. 

> So *when* the error is thrown derives from the semantics of the construct;
> switch tries matching with each pattern, until it finds a match or runs out of
> patterns. When it runs out of patterns is when it needs to insert a catch-all
> to deal with remainder (as we do with enum switch expressions.)
The semantics of switch at runtime need to be feed with whenever a pattern was 
considered as total or not at compile time. 

RĂ©mi 

Reply via email to