proposal-pattern-matching: The "no `LineTerminator` here" restriction before the brace in `match` is inconsistent with existing control flow operators

All existing control flow operators allow a line break before their block bodies, including with switch, and there’s even an ESLint rule that can enforce it as a preference.

// Example taken from there
if (foo)
{
  bar();
}
else
{
  baz();
}

This proposal blocks it for clear ASI concerns*, but for some teams, this could prove to be a problem (not all JS users use 1TBS or Stroustrup style**), and I don’t believe we should be enforcing this opinion now after not having enforced it previously.

I have previously proposed (in multiple places, although with no specific bug) that we use case instead of match, although @ljharb has voiced some opposition in one of them.


* In a potentially statement-terminating context, match could end up interpreted as a call rather than an expression when a newline is present.

** It’s not uncommon among C# or Windows devs who happen to write JS on occasion, since Allman style is the default VS style for C/C++/C# there. (I’ve also seen it a few times with Java.)

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Comments: 56 (14 by maintainers)

Commits related to this issue

Most upvoted comments

That bothers me less, but bothers me still - i want no overlap with switch in any way, so i can continue to teach that switch statements are abomination (and hopefully teach that this proposal is the solution)

Another possible alternative with a similar syntax to do...while

const value = do {
	case {status: 200} => `status is ${status}`
	case {status: 404} => 'JSON not found'
} match (res)

I’d be pretty against repurposing anything; I’m not interested in making it harder to learn JavaScript than it already is.

I can’t believe I didn’t remember this, but…

Both Erlang and Elixir, two of the most prominent languages that use pattern matching, use case as the keyword for the analogous construct.

Combine this with the fact that using that keyword would solve pretty much all grammar-related issues… I think I’m on board with renaming this to case

I continue to feel very very strongly that the word “switch” and “case” must be avoided, and I don’t see the NLT requirement as problematic. Additionally, the NLT requirement is something that a number of existing proposals will need in order to be viable, so citing that as a reason to change this proposal will set an unfortunate precedent that I think is best avoided.

I’m totally onboard with super switch (...) {}. It could be the new smoosh()!

I mean case as the actual block name, like:

let x = case(foo) {
  {y} => 1;
};

That bothers you too? (I can understand not wanting a case prefix for each clause, to avoid switch similarities.)

since switch will always exist, teaching the difference is most easy when the visual/syntactic difference is widest (while still intuitive for the new thing, ofc).

Piggybacking a bit on #77, if this proposal was to use switch, the case keyword could be replaced by match to make the difference more visible.

const val = switch (res) {
  match {status: 200, headers: {'Content-Length': s}} => `size is ${s}`,
  match {status: 404} => 'JSON not found',
  match {status} if (status >= 400) => throw new RequestError(res)
};

Not necessarily a blocker, but I believe using case would require special-casing to either explicitly disallow it inside existing case clauses, or potentially a cover grammar if that would work. e.g.

switch (true) {
  case true:
    case (value) {
      pattern => {}
    }
}

Since we’re inside a switch, the case keyword could either be the start of a pattern match, or the start of another clause in the switch statement. The : makes it specifically one or the other, but you can’t know which is which up front.

This is the longest issue thread in this entire repo.

I would like to propose going back to @isiahmeadows’s proposal of using case for this. I believe it fixes all the most important concerns around syntax right now, and is an acceptable name for this sort of construct:

Pros:

  • It removes the need for overly-clever syntax backflips.
    • no need for noLineTerminator thing
    • no need for special keywords or syntax for every internal clause
    • gives full flexibility for internal matcher syntax going forward
  • It is the closest s/x/y/ replacement for the current syntax (so we can preserve all the other current work).
  • It has significant precedent in existing languages. Particularly, the two most prominent dynamic languages that have this construct (Erlang and Elixir).
  • It uses a reserved token in a backwards-compatible format and so is guaranteed to not break the web.
  • It saves a whole character (case vs match). 😏

Cons:

  • Reminiscent of case clauses inside switch, but not easy to actually confuse in practice.
  • Potential documentation search issues from the overlap. (unclear about actual impact of this once case is released and matures).
  • match as the word “match” in it, and it’s a pattern matching thing.

I would appreciate it if y’all would consider this again, as I still think it’s the most elegant solution to a bikeshed that continues to get longer.

@borela The term match comes from pattern matching. switch is like a railway switch, where you basically say “take me to this route”, given a set of named routes. match is subtly different: you’re going “here’s what I have, match this against your routes and take me to the first one it matches”. It’s fuzzier than just “here’s the route I want to take, take me to it”, but it’s similar to the difference between string === equality and regexp matching, where your operand is the string, and your pattern is the regexp.

This is why match is preferred over switch for naming this - it just makes more sense. It occupies a bit of a middle ground between the flexible-yet-verbose if/else and the highly rigid switch, with on one end, F#'s highly flexible, Turing-complete “active patterns” and Scala’s magic apply/unapply methods, and on the other, Swift’s almost-C-style switch statement (up to and including it supporting explicit fallthrough and early breaking) and C#'s recent switch updates that bring it barely within the realm of pattern matching (this appears to be only part of what’s coming, though.

@ljharb @zkat @tabatkins

The more I think about it, it’s not as impossible as I once thought about avoiding ambiguity and conflict with existing syntax. If we choose to use =>, there’s only one case where it’d conflict:

const val = match (res)
{
    foo => bar
}

This is currently valid (it’d be parsed as a function call, then a block statement with an arrow function expression), but I’m pretty sure we’d break nobody if we just usurped it ourselves, disallowing arrow function expressions with bare heads as statements within blocks (it’s nonsensical, anyways). Note that this ambiguity does not exist with the foo if cond => bar variant.

Of course, this would require parsing an entire pattern to disambiguate a match block from an expression, but here’s how we can draw the distinction without a no LineTerminator here:

  1. If the match call includes a rest parameter, it can’t be a match expression head.
    • Commas would convert the arguments list to a sequence expression.
  2. If we don’t hit a block, it can’t be a match expression.
  3. Arrow functions already require parentheses for anything other than a single bare argument. It’s impossible to conflict with them apart from the aforementioned case.
  4. If we hit a { in a block after a potential call expression to a match, we can speculatively parse it as an object pattern. If we hit any of these scenarios, we can bail out and reinterpret it as a block statement:
    1. We detect something that isn’t a label, literal, identifier, array, or potential block expression.
      • This is the only case where reparsing could be necessary/easier. The rest can just be reinterpreted post-parsing by just returning a different node type.
    2. The next token after parsing the block statement is anything other than a =>.
    3. If there’s a subsequent if, the next character is anything other than a (.
    4. If there’s a subsequent if with a parenthesized condition, the next token after parsing it is a =>.
  5. If we hit a [ in a potential match block, we can speculatively parse it as an assignment pattern or expression. If we hit any of these, we can reinterpret it as a match pattern (or error if nothing works):
    1. A => character after the literal.
    2. An if after the literal, where the next character is anything but a (
    3. An if after the literal, with a parenthesized condition and where the next token after parsing it is a =>.
  6. If we hit a literal in a potential match block, a subsequent => could trivially disambiguate.
  7. If we hit an identifier in a potential match block, we could use the same rules that disambiguate arrays if bare-argument arrow function expressions are disallowed.

So as far as I can tell, the best way to resolve this is to just:

  1. Require at least one case. (What’s the point of a match expression with zero cases?)
  2. Within block statements, ban expression statements consisting only of arrow function expressions with bare heads, by adding a [lookahead ∉ `=>`] for identifier-only expression statements. (No engine with any sanity is even going to persist that kind of absurdity unless it’s used as a completion value for eval.)

If you make these two changes, there is literally no longer any need for any [no LineTerminator here] productions anywhere in the proposal.

@ljharb BTW, I meant case as in what @tabatkins was talking about, and was basically suggesting that very syntax change (s/match/case/g). (Just dropping by to clarify, in case it wasn’t clear from context.)

@isiahmeadows Regarding item 3, that’s true today, but it would stop being true if do expressions happen.

Brain dump of possible varients:

match (value) for {
	case "1" => expression
	case "2" case "3" => expression
}

// easiest on the tongue.
match (value) with {
	case "1" => expression
	case "2" case "3" => expression
}

// explicitly implies an expression is evaluated.
match (value) return {
	case "1" => expression
	case "2" case "3" => expression
}

One alternative is to require a prefix keyword that would allow parsers to disambiguate it with a look-ahead. For example, the case keyword cannot appear in the following context.

function match () {}

match(...)
{
case ...
}

This would mean that a look ahead for the first keyword case would disambiguate the match from the above, this works especially well since match requires at-least one case.

const val = match (res) {
  case {status: 200} =>
  	`size is ${s}`
  case {status: 404} =>
  	'JSON not found'
}

That said the => syntax is somewhat of a confusing point/restriction, 1. because it assumes the above is invalid because of the newline after =>, 2. It looks confusingly like arrow function but diverges from the semantics of arrow functions and always assumes an implicit return instead of an explicit return that unlike the former(implicit) an explicit return also allows you to exit early.

For example consider the following, one might assume val would be undefined instead of the length of the array.

const val = match (input) {
  {x} => {
    array.push(x)
  }
}

Using an explicit return might look like:

const val = match (res) {
  case {status: 200}:
  	return `size is ${s}`
  case {status: 404}:
  	return 'JSON not found'
  case {status}:
  	if (status >= 400)
  		return new RequestError(res)
}

and avoids the afore-mentioned issues.

@ljharb is it possible to add a new match keyword and require something like "use strict v2";?

At least using a different keyword (any keyword) to kick off the brain to visually parse it differently from a procedural C-style switch will keep people from making that mistake.

Sure, but it’s not free: we have to pay with cover grammars and newline restrictions if we use a contextual keyword, or mental tax if we overload an existing keyword.

In any case, I’m not particularly arguing for switch at this point; just testing the idea!

@zenparsing Reusing switch for both would need more than that to disambiguate. Consider this as a statement:

switch (expr) { }

Currently, this doesn’t throw. But the current match (expr) { } does. If you were to require at least one case (which would make sense either way - what’s the point of a zero-case match?), that would fix this ambiguity.

@zenparsing since switch will always exist, teaching the difference is most easy when the visual/syntactic difference is widest (while still intuitive for the new thing, ofc).

I agree with the general sentiment that we should avoid cover grammars and excessive line terminator rules if possible.

@ljharb If this construct effectively replaces and eliminates the need for the switch statement, why not just reuse switch? The first token after { would disambiguate.

and b) the style that’s unintentionally forced is, at least, widely preferred in the community (which isn’t a justification but certainly mitigates the turbulence that might otherwise be caused).

That is true, and 1TBS/Stroustrup style is so prevalent tools have been known to forget about Allman style on occasion when handling JS code. Also, we do kind of force it some with return, due to ASI rules (if that counts as precedent). So I’m not 100% against leaving it this way.

I filed this more as a thing that should be addressed explicitly, so we are at least aware of the potential confusion. (Syntax errors from erroneously using Allman style are going to be misleading unless engines/parsers specifically check for match(expr) preceding block statements and rethrow with the misplaced brace’s line number, so they should know that this may require such a check.)

Essentially it boils down to, either we use an existing reserved word, or we have a NLT restriction. Existing words that have other meanings - including case - are likely to garner objections because of various forms of confusion, and the unused reserved words are unlikely to be applicable.

I don’t think there’s any other option here besides NLT - while it’s unfortunate if JS forces a specific style, a) NLT has other precedent in the language, and b) the style that’s unintentionally forced is, at least, widely preferred in the community (which isn’t a justification but certainly mitigates the turbulence that might otherwise be caused).