tc39 / proposal-pattern-matching

Pattern matching syntax for ECMAScript
https://tc39.es/proposal-pattern-matching/
MIT License
5.46k stars 89 forks source link

Bikeshed issue: RHS syntax/bare expression form #181

Closed ljharb closed 2 years ago

ljharb commented 3 years ago

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:

  1. no separator

    when (pattern) expression // evaluates to "expression"
    when (pattern) { object: 'literal' } // evaluates to `{ object: 'literal' }`
  2. mandatory separator

    when (pattern) -> expression // evaluates to "expression"
    when (pattern) -> { object: 'literal' } // evaluates to `{ object: 'literal' }`
  3. conditional separator (do can take the place of the separator)

    when (pattern) -> expression; // evaluates to "expression"
    when (pattern) -> { object: 'literal'}; // evaluates to `{ object: 'literal' }`
    when (pattern) do { expression; } // evaluates to "expression"
    when (pattern) do { ({ object: 'literal'}); }; // evaluates to `{ object: 'literal' }`
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 an async match, either:

  1. all RHS's with do would have to implicitly change into async do semantics (this seems bad)
  2. all RHS's with do would become syntax errors, and users would have to change them to async 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.

j-f1 commented 3 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.

tabatkins commented 3 years ago

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.

tabatkins commented 3 years ago

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.

ljharb commented 3 years ago

@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.

tabatkins commented 3 years ago

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.

ljharb commented 3 years ago

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.

mpcsh commented 3 years ago

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.

Jack-Works commented 3 years ago

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.

ljharb commented 3 years ago

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.

Jack-Works commented 3 years ago

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.

ljharb commented 3 years ago
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.

Jack-Works commented 3 years ago

Oh, it seems like a separator does help if the pattern is complicated...

Haroenv commented 3 years ago

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

ljharb commented 3 years ago

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.

mpcsh commented 3 years ago

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.

kee-oth commented 3 years ago

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.

dannymcgee commented 3 years ago

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.

tabatkins commented 3 years ago

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.)

mpcsh commented 2 years ago

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.