Proposal-pattern-matching: Bikeshed issue: RHS syntax/bare expression form

Created on 21 Apr 2021  Â·  16Comments  Â·  Source: tc39/proposal-pattern-matching

(Currently, the RHS is option 1 in the below list)

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 aware of:

  1. statements only, no separator/implicit do expression semantics
when (pattern) { expression; } // evaluates to "expression"
when (pattern) { ({object: 'literal'}); }; // evaluates to `{ object: 'literal' }`
  1. statements only, explicit do expression semantics
when (pattern) do { expression; } // evaluates to "expression"
when (pattern) do { ({ object: 'literal' }); } // evaluates to `{ object: 'literal' }`
  1. expressions only, no separator
when (pattern) expression; // evaluates to "expression"
when (pattern) { object: 'literal' }; // evaluates to `{ object: 'literal' }`
  1. expressions only, with a separator
when (pattern) -> expression; // evaluates to "expression"
  1. statements (no separator/implicit do expression semantics) and expressions, no separator
when (pattern) expression; // evaluates to "expression"
when (pattern) { expression; } // evaluates to "expression"
when (pattern) { ({ object: 'literal'}); }; // evaluates to `{ object: 'literal' }`
  1. statements (explicit do expression semantics) and expressions, no separator
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' }`
  1. statements (explicit do expression semantics) and expressions, with a separator
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.

champion group discussion design idea help wanted syntax discussion

Most helpful comment

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.

All 16 comments

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.

Was this page helpful?
0 / 5 - 0 ratings