This seems like a nice landing.
The unification of guards and AND patterns was clever, and clearly
compositional, but exposing AND patterns just to get to guards can seem
a daunting step.
I agree that the priority, language design-wise is to get the combo
switch/case and if/instanceof on roughly the same expressive footing.
Without _some_ kind of guard-like capabilities, doing patterns in
switches is gonna be very limited, and some code will necessarily fall
off the expressiveness cliff and be rewritten as an if/else chain.
I think the `when(expr)` syntax is a minor detour; it's easy on the eye
and, as you show, has a gentle progression as to where we are headed.
Personally, given how common the basic guard use case is, I don't mind a
little bit of sugar sprinkled here and there, even though yes, it does
create two ways to do the same thing.
Cheers
Maurizio
On 05/03/2021 19:14, Brian Goetz wrote:
Let me try and summarize all that has been said on the Guards topic.
#### Background and requirements
For `instanceof`, we don't need any sort of guard right now (with the
patterns we have); we can already conjoin arbitrary boolean
expressions with `&&` in all the contexts we can use `instanceof`,
because it's a boolean expression. (This may change in the future as
patterns get richer.) So we can already express our canonical guarded
Point example with
if (p instanceof Point(var x, var y) && x > y) { ... }
with code that no one will find confusing.
For switch, we can't do this, because case labels are not boolean
expressions, they're some ad-hoc sub-language. When the sub-language
was so limited that it could only express int and string constants,
this wasn't a problem; there was little refinement needed on `case
"Foo"`.
As we make switch more powerful, we face a problem: if the user drifts
out of the territory of what can be expressed as case labels, they
fall off the cliff and have to refactor their 50-way switch into an
if-else chain. This will be a really bad user experience. Some sort
of escape hatch to boolean logic buys us insurance against this bad
experience -- as long as you can express your non-pattern criteria
with a boolean expression (which is pretty rich), you don't have to
leave switch-land.
So we took as our requirement:
Some sort of guard construct that is usable in switch is a forced
move.
#### Expressing guards in switch
There are several ways to envision guards:
- As patterns that refine other patterns (e.g., a "true" pattern)
- As an additional feature of "case" in switch (e.g., a "when" clause)
- As an imperative control-flow statement usable in "switch" (e.g.,
"continue")
We've largely rejected the third (even though it is more primitive
than the others), because we think the resulting code will be much
harder to read and more error-prone. We've bounced back and forth
between "let's nail something on the side of switch" and "let's let
the rising pattern tide lift all conditional constructs."
Other languages have demonstrated that guards in switch-like
constructs are viable.
The argument in favor of nailing something on the side of switch is
that it is pragmatic; it is immediately understandable, it raises the
expressivity of `switch` to where `if` already is, and it solves the
immediate requirement we have in adding patterns to switch.
The argument against is that it is not a primitive; it is dominated by
the option of making patterns richer (by adding boolean patterns), it
is weak and non-compositional, and overly specific to switch. (It is
possible to make worse-is-better arguments here that we should do this
anyway, but it's not really possible to seriously claim better,
despite attempts to the contrary.)
#### Interpreting the feedback
The JEP proposes a powerful and compositional approach:
- true/false patterns that accept arbitrary boolean expressions (and
which ignore their target);
- combining patterns with a pattern-AND combinator
On the one hand, this is a principled, orthogonal, compositional,
expressive, broadly applicable approach, based on sensible primitives,
which will be usable in other contexts, and which anticipate future
requirements and directions.
On the other hand, there has been a pretty powerful emotional
reaction, which could be summarized as "sorry, we're not ready for
this degree of generality yet with respect to patterns." This
emotional reaction seems to have two primary components:
- A "who moved my cheese" reaction to the overloading of `true` in
this way -- that `true` seems to be, in everyone's mind, a constant,
and seeing it as a pattern is at least temporarily jarring. (This may
be a temporary reaction, but there's still a cost of burning through it.)
- A reaction to "borrowing & from the future" -- because the other
use cases for &-composition are not obvious or comfortable yet, the
use of &-composition seems foreign and forced, and accordingly
engenders a strong reaction.
The former (which I think is felt more acutely) could be addressed by
taking a conditional keyword such as `when` here; ad-hoc "focus"
research suggests the negative reaction here is lower, but still there.
The latter is, I think, the more linguistically significant of the
two; even though there is a strong motivation for & coming down the
pike, this is not the gentle introduction to pattern combination that
we'd like, and developer's mental models of patterns may not be
ready. Patterns are still new, and we'd like for the initial
experience to make people want more, rather than scare them with too
much up front.
#### Options
I suspect that we'd get a lot of mileage out of just renaming true to
something like "when"; it avoids the "but that's not what true is"
reaction, and is readable enough:
case Foo(var x) & when(x > 0):
but I think it will still be perceived as "glass half empty", with
lots of "why do I need the &" reactions. And, in the trivial (but
likely quite common, at least initially) case of one pattern and one
guard, the answers are not likely to be very satisfying, no matter how
solidly grounded in reality, because the generality of the
compositional approach is not yet obvious enough to those seeing
patterns for the first time.
I am not compelled by the direction of "just add guards to switch and
be done with it", because that's a job we're going to have to re-do
later. But I think there's a small tweak which may help a lot: do
that job now, with only a small shadow of lasting damage:
- Expose `grobble(expr)` clauses as an option on pattern switch cases;
- When we introduce & combination (which can be deferred if we have a
switch guard now), plan for a `grobble(e)` pattern. At that point,
case Foo(var x) grobble(x > 0):
is revealed to be sugar for
case Foo(var x) & grobble(x > 0):
As as bonus, we can use grobble by itself in pattern switches to
incorporate non-target criteria:
case grobble(e):
which is later revealed to be sugar for:
case Foo(var _) & grobble(e):
The downside here is that in the long run, we have something like the
C-style array declarations; in the trivial case of a single pattern
with a guard, you can leave in the & or leave it out, not unlike
declaring `int[] x` vs `int x[]`. Like the "transitional" (but in
fact permanent) sop of C-style declarations, the "optional &" will
surely become an impediment ("why can I leave it out here, but not
there, that's inconsistent").
All that said, this is probably an acceptable worse-is-better
direction, where in the short term users are not forced to confront a
model that they don't yet understand (or borrow concepts from the
future), with a path to sort-of-almost-unification in the future that
is probably acceptable.