Closed ljharb closed 2 years ago
Obviously it’s hard to know until people are actually using this feature, but if people regularly return object literals from match statements, this syntax has a lot of noise:
when (pattern) { ({ object: 'literal'}); };
Ideally it would have at most two layers of bracket nesting.
I feel pretty strongly that any solution which requires {({...})}
for directly returning an object literal is a no-go; this is a common thing to return and I suspect it'll be common in match() too. This rules out 1, 2, and 5 right away imo.
Of the remaining, I'm happiest with 3 or 4 (expr-only, without or with separator). I don't understand what the difference is between 6 and 3 (if you allow expressions only, a do-expr is an expression, so that's implicitly allowed). 7 (expr with separator, or do-expr without separator) feels a little messy but I'm not opposed if necessary.
I felt more strongly in favor of 4 (expr-only, with separator) back when the matcher was bare, for readability reasons; now that it's paren-wrapped, like if(), I'm fine with no-separator, so 3. This avoids the need to stack up two prefixes for do blocks (when(...) -> do {...}
).
So my preferences is 3, with 4 slightly behind it, and 7 as a moderately distant third place.
One of the challenges with using do explicitly, is that in an async match, either:
all RHS's with do would have to implicitly change into async do semantics (this seems bad)
I don't understand why this is a problem. async match
doesn't turn the branches into async do
, it wraps the match()
in an async do
. That's what allows await
in the matchable or in pinned matchers.
You can certainly use a do-expr inside an async-do, and it will have well-defined semantics, and those will apply just as well in here. I don't see any issue or confusion.
@tabatkins yes, but by wrapping the match in an async do
, the semantics of all do
expressions inside the match change, because they can no longer return
/break
/continue
.
Yes, but I don't understand how that's an issue, or what confusion you think will occur. You can't use return
/etc inside of an async block, full stop, and an async match()
is an async block.
I suppose it's quite fair that any such confusion would occur with any do
expression that gets wrapped in an async do
. It's not just an async block, however - inside an async function, a do
expression has full use of continue/return/break.
My thoughts on this are evolving to fairly closely meet Tab's. My initial proposal of using implicit do-expressions as the RHS was mostly to allow for "block" semantics without requiring the developer to understand do-expressions. I do still worry that the cognitive load on a newcomer of having to learn the semantics of two distinct constructs and how they compose is a bit high, but I think this is something we can study.
I support expression without separator because I think it's very common and it's a waste to type ->
so that excludes 1,2,4,7.
I don't want an implicit do expression syntax unless #173 is resolved.
Let's discuss #173 separately; either way, that's an issue with do expressions themselves, and i'm very skeptical it's something that pattern matching should solve.
Code is read much more often than it's written - if it makes it more readable, it should be irrelevant that it takes more typing, because explicit is better than implicit. I don't think "it's hard to write" is a strong argument unless it's deciding between two equally readable alternatives.
Code is read much more often than it's written
Yeah, I totally agree, but it doesn't get benefits in the ->
separator in this specific case. They're both easy to read.
when (pattern) as { bindings } if (condition) { object: 'literal'};
when (pattern) as { bindings } if (condition) expression;
vs
when (pattern) as { bindings } if (condition) -> { object: 'literal'};
when (pattern) as { bindings } if (condition) -> expression;
i'm not so sure; the second seems clearer to me since the LHS can potentially have a lot of curly braces and square brackets and parens. It seems clearer to me to have an explicit separation between the LHS and RHS.
Oh, it seems like a separator does help if the pattern is complicated...
since this would be the first language-feature using do-expressions, I feel like it's useful to indicate that it's not a function or a block but something that implicitly returns, therefore I'm in favour of a proposal that starts with do
. This would also avoid the confusions as in #173 regarding semantics of the return value.
What I prefer most is (option 6 apparently):
when (pattern) expression;
when (pattern) { object: 'value'};
when (pattern) do { somethingComplex };
I don't fully understand why it's
when (pattern) as { bindings } if (condition) { object: 'literal'};
when (pattern) as { bindings } if (condition) expression;
and not:
when ((pattern) as { bindings } if (condition)) { object: 'literal'};
when ((pattern) as { bindings } if (condition)) expression;
though
Because that adds a separate set of parens for imo minimal grouping benefit (altho, tbh it never occurred to us). I don’t think that makes the “no separator” case sufficiently distinct between LHS and RHS to warrant considering it, but maybe others will have different thoughts.
I don’t think that makes the “no separator” case sufficiently distinct between LHS and RHS to warrant considering it
I concur with this, to me it looks more noisy but not any clearer. Given that we're already seeing a desire to cut down on existing parens / grouping sigils (#182), I'm wary of adding yet more.
To throw some more feedback in, I like options 2 and 6 (in that order).
Reasoning:
Option 2 It's explicit. It provides clear separation with minimal additional syntax (compared to an implicit expression). There's less to learn ("This is how you do it and it's the only way"). And the ease-of-refactoring is nice. Going from a one line to a multi-line RHS is as simple as adding a line and typing; as opposed to needing to do the song and dance that we do when refactoring arrow functions with implicit returns to explicit.
Option 6 JS devs are already used to this way of doing things thanks to arrow functions. So understanding arrow functions (which most JS devs probably know) gives you a good analogy to use when learning this syntax.
In general, I think there's a good bit to learn with pattern matching so we should try to keep it explicit and/or not surprising/different so as not to overwhelm people learning/using it. Which is why I'm against the ->
separator being thrown in there. I think do
would just help with verbal communication as well as it's actually a word that we can read and say: "match
this when
this, then do
what's in this block of code". Using the ->
instead could make it less clear how to communicate when talking about pattern matching.
In addition, using do
also just simplifies how to learn about pattern matching. "It's a do
expression, let me look up what that is" vs the cryptic ->
which doesn't give you a good starting point to figure out what's going on.
I think a separator between lhs and rhs is critical, because the examples on the front page right now read like a Katamari ball of braces and parentheses to me. It's really hard to parse. The lack of extraneous parentheses around everything is one of the things I love about languages like Rust and Go because they're so much cleaner to read and write. I loved the old match {foo, bar} -> baz
form, but the newest version of the proposal feels like a huge regression to me.
I like the ->
operator for the separator, because it's distinct, easy to spot, and would be immediately familiar to anyone who's used similar constructs in other languages. I really don't anticipate any serious confusion between ->
and =>
— they're visually distinct at a glance, despite having the same general shape (no one complains that -
and =
are confusing, do they?). And further, I think having similar semantics to arrow functions (e.g., -> expr
produces the expression, -> { ... }
executes a block of statements) would make it much more intuitive due to the consistency and familiarity.
I don't particularly like the idea of using a different token separator for expression vs statement bodies, and I don't like the idea of using a keyword in that position whatever it might be. There's already a bit of keyword hell going on here — match
, when
, if
, else
, as
... throw do
in there and you've got enough control flow keywords to populate a pretty expressive language all on their own, just to cover a single construct. It's a lot.
I would be okay with limiting match
/when
to just expressions and letting switch
/case
hold down the statement fort (clunky as it is), but I definitely would not be okay with dropping expressions. My single biggest pain point right now transitioning from my Rust side projects to my JS day job is the fact that there are so few expressions. I've occasionally resorted to ugly shenanigans like this, which absolutely no one wants to see:
const foo = ((arg) => {
switch (arg) {
case "lorem" : return lorem;
case "ipsum" : return ipsum;
case "dolor" : return dolor;
case "sit" : return sit;
case "amet" : return amet;
}
})(param);
(EDIT: I tend to come off more abrasive/sarcastic than I mean to, so just to clarify, I'm really excited and optimistic about this proposal. When it lands I think it'll be about as significant an improvement to the language as there ever has been, so thanks for all y'all's work on this. 🙂)
EDIT 2: I'm also just realizing after reading some more that I don't actually understand the semantics, so apologies for the drive-by ignorance lol. I don't really understand why we're introducing two new language concepts at once (pattern matching + do expressions) when existing semantics seem sufficient to cover the use cases of the latter, but it's not really a hill I'm eager to die on and I'm sure there's plenty of context and history that I'm missing.
Note that we're not really "introducing do expressions" as part of the current proposed grammar, so much as we're "assuming that do expressions will exist and we can rely on them".
(I agree with the rest of your comments, tho.)
At this juncture, the champions are split between options 3 and 4, with 7 trailing. There was also very brief discussion of using :
as a separator.
We plan to punt on the spelling of a separator (or lack thereof) and come back to this issue after another round of committee feedback.
There are many use cases for the RHS to be "just an expression", and there are many use cases for the RHS to be "a list of statements", and there are use cases for the RHS to evaluate to "an object literal".
For the latter case, the ambiguity between "a block" and "an object literal" is a problem that we're all likely familiar with from concise arrow function bodies that attempt to return an object literal. This challenge is quite relevant here.
Some options we're currently considering:
no separator
mandatory separator
conditional separator (
do
can take the place of the separator)Nixed options
Options 1, 2, and 5 were removed from consideration because of the syntax mess required to return an object literal. Option 6 was removed because it's equivalent to option 3. 1. statements only, no separator/implicit `do` expression semantics ```jsx when (pattern) { expression; } // evaluates to "expression" when (pattern) { ({object: 'literal'}); }; // evaluates to `{ object: 'literal' }` ``` 2. statements only, explicit `do` expression semantics ```jsx when (pattern) do { expression; } // evaluates to "expression" when (pattern) do { ({ object: 'literal' }); } // evaluates to `{ object: 'literal' }` ``` 5. statements (no separator/implicit `do` expression semantics) **and** expressions, no separator ```jsx when (pattern) expression; // evaluates to "expression" when (pattern) { expression; } // evaluates to "expression" when (pattern) { ({ object: 'literal'}); }; // evaluates to `{ object: 'literal' }` ``` 6. statements (explicit `do` expression semantics) **and** expressions, no separator ```jsx when (pattern) expression; // evaluates to "expression" when (pattern) do { expression; } // evaluates to "expression" when (pattern) do { ({ object: 'literal'}); }; // evaluates to `{ object: 'literal' }` when (pattern) { object: 'literal'}; // evaluates to `{ object: 'literal' }` ```One of the challenges with using
do
explicitly, is that in anasync match
, either:do
would have to implicitly change intoasync do
semantics (this seems bad)do
would become syntax errors, and users would have to change them toasync do
(this seems bad also)This is why we leaned towards not using
do
explicitly, but you can see above that if there is no separator for the bare expression form, the "object literal" case gets very unergonomic.Do you have any thoughts about how to spell the expression separator, or on these options? Option 7 seems particularly nice if we can all agree on a separator.