Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dropping when keyword from match clauses? #304

Open
tabatkins opened this issue Oct 3, 2023 · 17 comments
Open

Dropping when keyword from match clauses? #304

tabatkins opened this issue Oct 3, 2023 · 17 comments

Comments

@tabatkins
Copy link
Collaborator

When we took over this proposal, we changed the original grammar of the match arms from <pattern> => <expr>; to when(<pattern>): <expr>;, in part to allow if() guards to be placed alongside the when(), like when(...) if(...): <expr>. We no longer have if() guards (rather, if() is just a pattern syntax now), and so we removed the required parentheses from when() - the arms are now defined as when <pattern>: <expr>;.

I think the when keyword has become vestigial here, tho. There's no longer a grammatical requirement for it, and it makes some types of simple patterns, like when if(...): ...;, read somewhat silly.

Further, most languages with pattern-matching features don't have a per-clause introductory keyword (or other syntax) either: in particular, Rust, Haskell, and C# (in some cases) don't. (Python and Dart do, as does C# in some cases - they all happen to use case - so the ecosystem is mixed on the matter.)

We should just drop the when keyword and make the syntax slightly lighter. This would resolve @rkirsling's objection about when if(...) looking silly. However, @ljharb appears to have an objection to this, so let's discuss.

@ljharb
Copy link
Member

ljharb commented Oct 4, 2023

I'm not stuck on how when is spelled, but I definitely think it's critical for readability, teachability, googleability, and learnability to have some kind of clear syntactic marker for the beginning of a clause.

@tabatkins
Copy link
Collaborator Author

I think readability/learnability is already completely handled by the fact that the only reasonable way to format a match is with each arm on a line. If someone does anything else, that's them shooting their own foot. We're not providing a footgun, they're just pointing downwards and pulling the trigger, and that's on them. It's the same as literally any other sequences of statements - if you're putting multiple statements on a line, you're either hurting yourself or you have a very good personal reason for formatting it that way.

In all other cases, your match clause starts on a new line. You see a new line, it's a new match clause.

For googleability, we already have the match name itself. I know you wanted to avoid case and switch to avoid crossed wires, but do we really need two keywords?

@ljharb
Copy link
Member

ljharb commented Oct 5, 2023

In all other cases, your match clause starts on a new line.

Given that the expression on the RHS will be multiline, and that's perfectly reasonable, I don't think this holds.

@tabatkins
Copy link
Collaborator Author

It's an expression, tho. If it's multiline, you indent; that's how everyone writes their code already (or, again, they're just hurting themselves for no reason and we can't stop them). This is true if we have do-exprs too - the obvious formatting for do-exprs will indent their contents.

Basically, do you think JS made a mistake in its general syntax, having statements not require an introductory keyword? Because that's the exact same situation, as far as I can tell. There's no difference between:

if(...) {
  statement;
  statement;
  long statement
    .thatWraps();
  statement;
}

and

match(...) {
  pattern: expr;
  pattern: expr;
  pattern: long expr
    .thatWraps();
  pattern: expr;
}

At least, as far I can tell. If you think there is a difference in these two cases, could you elaborate on that? Or if my reasoning is off-base at a different point?

@ljharb
Copy link
Member

ljharb commented Oct 5, 2023

Those two cases are very different; the former is a series of top-down instructions, and the latter is a pattern to expression map.

@rkirsling
Copy link
Member

Am I remembering correctly that you'd object to respelling when as case, @ljharb?

@ljharb
Copy link
Member

ljharb commented Oct 5, 2023

@rkirsling yes, but i'm open to almost any other respelling :-) I agree when if reads weirdly.

@haltcase
Copy link

haltcase commented Oct 5, 2023

Those two cases are very different; the former is a series of top-down instructions, and the latter is a pattern to expression map.

So what about a plain object, which would have the same structure lacking prefix keywords? I think it would be reasonable to mirror that considering it's also a map of sorts.

@ljharb
Copy link
Member

ljharb commented Oct 5, 2023

@haltcase keying into a plain object is a map of sorts, sure - an object by itself isn't inherently like a match construct.

@tabatkins
Copy link
Collaborator Author

Those two cases are very different; the former is a series of top-down instructions, and the latter is a pattern to expression map.

Yes, I know they're different syntax constructs. My question is: for the purpose of reading, writing, and understanding code, how are they different? What makes the one acceptable without a prefix, while the other needs one?

If you write if(...) { foo; bar; baz } it's just as potentially difficult to read as match(...) { f: foo; b: bar; b2: baz; }. And in both cases, if you write them properly, one statement/arm per line, and use indentation to visually indicate that you're breaking a long statement/arm across multiple lines, it's perfectly readable and understandable without a prefix.

So I'm asking you to elaborate on what, exactly, you feel is the problem being solved by requiring a prefix on arms, and why that same problem doesn't apply in other cases like this.

@ljharb
Copy link
Member

ljharb commented Oct 16, 2023

I’m saying that they’re conceptually unrelated, despite syntactic similarity.

I think that “what’s proper” is not an objective thing - it’s something the community discovers over time. Additionally, even long time practitioners of the language can have preferences that violate the community’s preferences. It’s not for us to deem whether separate lines is “proper”, that’s paternalistic - it’s certainly how I’d hope to write it. Our job is to design things that we think will be used and written best, and to ensure that other patterns either are impossible, or are minimally problematic. I personally believe that prefixes will help with readability, syntax highlighting, parsing, linting, googling/education/learnability, and, yes, starting each clause on its own line.

@zeel01
Copy link

zeel01 commented Oct 4, 2024

I like the when in general as it better differentiates this from other types of block-constructs and keeps it parallel to switch.

My concern is this example from the main document, when written without when:

match (res) {
  { status: 200, let body, ...let rest }: handleData(body, rest);
  { const status, destination: let url } and if (300 <= status && status < 400):
    handleRedirect(url);
  { status: 500 } and if (!this.hasRetried): do {
    retry(req);
    this.hasRetried = true;
  };
  default: throwSomething();
}

This example is hard to parse visually, despite being formatted pretty well. I don't like that we have lines starting with braces for one thing, normally that would only happen inside of an object literal. Additionally, it feels really awkward for the colon separator to just sit between two things that kind of look like expressions or objects or... who knows what. It looks like ternary soup with missing question marks. Furthermore, there is no clear indication in the syntax that these lines are special, someone unfamiliar with this new syntax would likely expect that each line runs in order as statements would, not that it would terminate at one of them.

Then there is the dangling default:, do you still use that key word without the when? Do you just put one last statement with nothing preceding it? Do you do : expression;? Seems really weird.

The original exaple:

match (res) {
  when { status: 200, let body, ...let rest }: handleData(body, rest);
  when { const status, destination: let url } and if (300 <= status && status < 400):
    handleRedirect(url);
  when { status: 500 } and if (!this.hasRetried): do {
    retry(req);
    this.hasRetried = true;
  };
  default: throwSomething();
}

This is more clear, the when keyword clearly indicates something special is going on, and it looks suspiciously similar to the familiar switch statement. Furthermore, even if I've never seen this construct, I can generally get the idea of what it does. Without the keyword, I would be scratching my head wonder what those lines did, and how someone defined a function called match without the function keyword. Even assuming my IDE colors it differently and I know it must be a special language construct, it's still very confusing exactly what it does.

Regarding when if(...): ...;, is there a reason that the when needs to be used in that special case?

match (subject) {
  when { a }: ...;
  if (condition == value): ...;
  when { b }: ...;
}

Basically, why not allow the lines to start with if while still requiring when in all other cases? Essentially the match statement can have either matchers or conditions, and these would both occupy the same syntactic region.

Though that brings up a question I'm having a hard time finding an answer to in the proposal: Why not just support boolean expressions in the patterns region?

match (subject) {
  when (value === somethingElse): ...;
  when { has: "pattern" } and (value === anotherThing): ...;
}

I assume I'm missing a reason why this can't be allowed?

@ljharb
Copy link
Member

ljharb commented Oct 4, 2024

it would be confusing to conflate pattern space and JS expression space, imo.

@tabatkins
Copy link
Collaborator Author

Yeah, patterns are a very distinct syntax from expressions, and can't be arbitrarily mixed. The one place we currently allow them (inside the if() pattern) has a reliable indicator (the if) and boundary (the parens) that let us handle it. Your suggestion just uses parens, which isn't a strong indicator; parens can also be wrapped around ordinary patterns, so we can't tell how we're supposed to parse it ahead of time.

This example is hard to parse visually [snip]

As far as I can tell, every problem you raise here except the first is exactly as present if you use when at the start. We still use a : between the pattern and the expression, it looks exactly as much like ternary, and someone unfamiliar with the syntax still wouldn't know what they were looking at (but I expect that, in either syntax, they could figure out the gist pretty easily, as many languages have pattern-matching).

As for the first, we do have lines starting with {} elsewhere in the language: destructuring patterns. Pattern-matching patterns are, intentionally, closely aping destructuring patterns, as they do very similar things. The intuition should carry over.

Then there is the dangling default:, do you still use that key word without the when?

Yes, you still write default: in either syntax variant.

Regarding when if(...): ...;, is there a reason that the when needs to be used in that special case?

Syntax special cases are generally a bad design. It means that, if you wanted to later edit that condition to add an or {foo}, or just swap the if() for a different condition, you have to remember to go back and add a when to the beginning, too. They can be justified if it's particularly common, or the result without the special case is particularly egregious, but we avoid it when we can. (But, luckily, going when-less avoids the problem entirely.)

@zeel01
Copy link

zeel01 commented Oct 4, 2024

I think you misunderstand half my issues with it. My point is that with the when there is a clear signal that something special is going on, which contextualizes the rest of the syntax. Furthermore, with the when the entire statement look very similar to a switch, the over all structure is almost identical:

switch (<subject>) {
    case <value1>: <expressions1>;
    case <value2>: <expressions2>;
    case <value3>: <expressions3>;
    default: <defaultExpression>;
}

match (<subject>) {
    when <pattern1>: <expressions1>;
    when <pattern2>: <expressions2>;
    when <pattern3>: <expressions3>;
    default: <defaultExpression>;
}

This results in something familiar, even if the shape of a pattern is complicated and confusing. Also, one could just as easily argue that switch should work like this:

switch (<subject>) {
    <value1>: <expressions1>;
    <value2>: <expressions2>;
    <value3>: <expressions3>;
    default: <defaultExpression>;
}

Is there really a reason why there must be a keyword case there? But there is, so if for no other reason than symmetry with switch I would prefer match include the when keyword.

As for the first, we do have lines starting with {} elsewhere in the language: destructuring patterns. Pattern-matching patterns are, intentionally, closely aping destructuring patterns, as they do very similar things. The intuition should carry over.

Yes, we have { a, b, c } = object; but typically you would do it let { a, b, c } = object; (or const). Destructuring into existing variables also looks very weird.

Typically I'm all for making things concise, but I think it goes a step too far here given how complicated the grammar already is. More signals to explain what's going on greatly benefit readability.

Syntax special cases are generally a bad design.

I'm not sure that I agree with this statement as a generality. Syntax special cases that lead to weird esoteric grammar aren't great, but special cases that work intuitively (as I would argue this one would) are generally beneficial.

There's no reason that when if ( and if ( couldn't both work if you like. But I'm not convinced by the refactoring argument, particularly since it seems that the convention would be for the if to come last meaning you likely would add when { foo } or to the start of the line anyway unless the order of the short-circuit logic was important. In that case, you would just have the when if construct. That, or the special case could treat a starting if ( ) as equivalent to when if ( ) and permit if ( ) or { foo } to just work. I dislike the match lines not starting with a keyword, but if the keyword is if that's just as good as it being when. It's still probably better to re-order the statement unless the order is critical though.

@ljharb
Copy link
Member

ljharb commented Oct 5, 2024

@zeel01 i was responding only to

I assume I'm missing a reason why this can't be allowed?

The when denotes pattern space, the if denotes expression space, and this distinction is necessary and desirable.

@zeel01
Copy link

zeel01 commented Oct 5, 2024

@ljharb I was only responding to @tabatkins.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants