Updated patterns-in-switch doc
Brian Goetz
brian.goetz at oracle.com
Tue Sep 22 17:22:11 UTC 2020
>> with the possible temporary exception of the three primitive types not currently permitted
> I believe, there are four of them: boolean, long, double, and float
RIght.
> #2 Disallowing switch expressions inside guards
>
>> And worse if we allow switch expressions inside guards, which we shouldn't do.
> Hm... sounds like this may heavily complicate the grammar if we really
> want to prohibit switch expressions anywhere inside the guard. E.g.:
> switch (obj) {
> case Foo(int x)
> __where process(switch(x) {case 1 -> 10;case 2 -> 20;default -> 0;}) -> ...
> }
> Should this be allowed? If yes, then there's no point to disallow
> top-level switch expressions inside guards, as nested ones are
> confusing to the same level.
> If yes, then the grammar should be updated to include tons of
> productions like ExpressionNoSwitch, MethodInvocationNoSwitch,
> ArgumentListNoSwitch, and so on.
>
> To me, adding any restrictions to expressions inside guards looks an
> arbitrary decision. If users really want to write confusing code, let
> them allow using expression switches in guards.
Let's separate out the grammar from the goal.
Embedding switch expressions has two problems here; one is the obvious
syntactic confusion (what switch is that case a part of?), and the other
is side-effects; switch expressions are the only expressions that can
embed statements. Both are a bad match for expressing guard
conditions. (Note that when we did switch expressions, we omitted the
possibility that a switch expression could be a constant expression, to
avoid their use in, say, case labels in a constant switch:
case switch (x) { case 1 -> 3; case 2 -> 4; } -> 5;
I think that was the right call :)
As you pointed out yesterday on Twitter regarding old-style array
declarations in record components, there are two ways to address it;
constrain the grammar, or use a deliberately coarse grammar and then
perform a post-parse check. So if the answer is to constrain what you
can put in guards, that doesn't mean we have to constrain the grammar.
The concern about side-effects (which I know we can't fully contain)
comes from a bigger goal, one that may not have been explicitly stated:
I want that the computation inherent in a pattern switch effectively be
"constant", for a number of reasons. Having side-effects in case labels
or guards effectively undermines that. One benefit (but not the only) of
doing this is that it is a necessary condition for encoding the entire
switch logic in an `indy` bootstrap, so that we can dynamically
construct a decision tree based on the characteristics of the case
labels. The more imperative a switch looks, the harder it is to do
that. (So, for example, guards should probably be restricted to
capturing effectively-final locals.)
THe remaining constraining of side-effects will come from making only
vague promises about when, and how often, to execute pattern bodies. if
we have a switch with:
case Foo(var x) where x == 0:
case Foo(var x) where x > 0:
case Foo(var x):
we should be free to execute the deconstructor body early or
just-in-time, once or three times (or more!)
> #3 Total patterns in instanceof
>
>> It is sensible because x instanceof <total pattern> is in some sense a silly question, in that it will always be true and there's a simpler way (local variable assignment) to express the same thing.
> It should be noted that local variable assignment requires the
> variable declaration which cannot be done inside the expression.
Yes, I realize that one can use pattern matching as a form of implicit
assignment. But if we think that is important, maybe it is better to
try to get there more directly, rather than relying on a "trick". The
idiom you describe -- pulling a DU local into a broader scope -- works,
but is less than ideal, because you don't get the perfect scoping.
> Well, we may split declaration and assignment and put the assignment
> inside the condition:
>
> Foo foo;
> if (x != null && (foo = x.doExpensiveCalculation()) != null &&
> foo.isValidResult()) {
> use(foo);
> }
>
> But this has at least two drawbacks: necessity to specify explicit Foo
> type (var doesn't work anymore) and broadening the scope of `foo` more
> than necessary (it pollutes the namespace after `if`). In general, it
> looks asymmetrical to me that the condition can introduce variables
> only if we can express the condition on that variable with a pattern,
> but we cannot do this if we have a guard-like condition.
Here's how I would rather write that (if the status quo isn't good enough):
if (x != null && (var foo = x.doExpensiveCalculation()) != null &&
foo.isValidResult()) { use(foo); }
That is, treat `var x = e` as a pattern match that always succeeds and
generates one binding.
More information about the amber-spec-experts
mailing list