In the example,
match (res) {
if (isEmpty(res)) { … }
when ({ data: [page] }) { … }
when ({ data: [frontPage, ...pages] }) { … }
else { … }
}
The introduction of control-flow constructs into the top-level match statement is difficult to reason about for the following reasons:
WhenClause production includes MatchGuard, which also uses an if token, but the meanings are _slightly_ different. Seeing multiple guarded whens, adjacent to top-level ifs is cognitively taxingif/else implies flow-control that is potentially misleading or confusing. if else if else, terms carry with them an association of precedence that should not applicable in this context. What we mean to say is, "catchall and default to some behavior. Default to some specific behavior under these guarded circumstances." That's _subtly different_ from what the above example implies. Specifically it's not immediately clear what this would produce:match str {
// else what?
else { 1 },
when "hello" { 2 }
};
// This also looks odd
match str {
else { 1 },
};
Instead, a "catch-all" default token should be used to provide exhaustive matching. This can also be guarded against:
Here I will use default, as I feel it's the most expressive.
match (res) {
default if (isEmpty(res)) { … }
when ({ data: [page] }) { … }
when ({ data: [frontPage, ...pages] }) { … }
default { … }
}
which resolves the ambiguity and prevents the need for two separate "catch-all" signifiers as well as imposing order on those signifiers.
match str {
default { 1 },
when "hello" { 2 }
};
I argue that this expresses intent more clearly, and is in line with expectations from other pattern matching impls such as what's available in Rust and OCaml.
Pros of using default _over _ or something shorter are:_
default already associated with catchall functionality. Maintain parity with other aspects of the lang: switch and default exportsThe downside I see here is that default is quite long. However, from experience it's out of the ordinary to use a catch-all more than once or twice per match
Thanks for the suggestion! However, default would have syntactic overlap with switch, which is an explicit priority of this proposal to avoid.
There's not really any reason we need to avoid introducing new keywords; it's a new construct. Also, else is an existing keyword.
However, default would have syntactic overlap with switch, which is an explicit priority of this proposal to avoid.
The "Subsumption of switch" priority explicitly states that it must be easily searchable, separate from "switch". Someone searching for this particular functionality is most likely going to google something similar to "javascript match default clause" or "javascript match catchall clause".
Either way, I'm not proposing that we use default to avoid adding keywords -- just calling out that that is a _pro_ of using default (over _ or something shorter).
I do think that it carries somewhat of an important _semantic association_. Default is really what we're after here, and nicely descriptive.
Regardless, if/else are I think unsuitable for top-level matching for the reasons I outlined (specifically #3). I'm less tied to default (though strongly in favor of it!), and more concerned about the cognitive complexity these constructs add.
I mean, "else" is really what I'm after here, not "default" :-)
Specifically, it implies order that is not actually present: I would not expect this match statement to short circuit in the case of isEmpty(res).
I'm not sure what this means - else runs when nothing _else_ has matched.
:) I feel like we're talking about slightly different things, let me try to restate -- let me know if it's more / less clear or just the same.
From the README, the third sample given is:
match (res) {
if (isEmpty(res)) { … }
//^ This strikes me as highly unusual
when ({ pages, data }) if (pages > 1) { … }
when ({ pages, data }) if (pages === 1) { … }
else { … }
}
That first if -- without indication of any default -- carries with it the implication that it's going to be evaluated before anything else but is unbalanced from the subsequent catch-all. That seems misleading to me.
I mean, "else" is really what I'm after here, not "default" :-)
I hear ya. I'm advocating for a singular default keyword here, emphasizing the appropriateness of default, but will defer that for now.
Experience with Rust and OCaml, _ would seem to be the most natural. I believe F# and Elixir follow suit. But regardless of what they've adopted, a singular optionally guarded default token is I think the most important aspect here. Even if that ends up being else if (...) { ... }
It will be evaluated before anything else - it’s the first match clause. What’s misleading?
An underscore is a valid identifier, so it’s not an option for anything to claim.
An underscore is a valid identifier, so it’s not an option for anything to claim.
Which is why I'm not suggesting that it be used :)
It will be evaluated before anything else - it’s the first match clause. What’s misleading?
In that it allows for three ways to express two distinct concepts. On top of that if/else do not express the intent of that second concept clearly. For example, in Rust
match str {
_ if str == "hello" => 0,
_ => 1,
"hello" => 2
};
match str {
_ => 1,
"hello" => 2
};
One can look at that, clearly see the catchall intent, and anticipate the short-circuit in both cases
The equivalent of the proposal:
// This on the surface seems fine
match str {
if str == "hello" { 0 },
// but really, else _what_? How does this relate to the below when?
else {1},
when "hello" {2}
};
// Further, this is highly ambiguous
match str {
else { 1 },
when "hello" { 2 }
};
Compared to
match str {
<default-token> if str == "hello" { 0 } ,
<default-token>{ 1 },
when ("hello") {2}
};
// Much less ambiguous
match str {
<default-token> { 1 },
when ("hello") { 2 }
};
This example illustrates well what I'm trying to articulate in terms of being misleading, will update issue
The current proposal requires that a bare else always be the last item in a clause - there will never be a when or an if after it.
Specifically, the “catch-all” is required to always the the last item - regardless of how the token is spelled.
Specifically, the “catch-all” is required to always the the last item - regardless of how the token is spelled.
This is part of the "... (arguably) needlessly complex" shoutout in the second reason I give. And the call for simplification of the proposal.
Further, if I'm iterating code I think something like this looks very odd
const fn(something) => match something {
else { throw new Error("Not implemented!") },
}
// and I'd prefer not to have to use different syntax such as
const fn(something) => match something {
if(true) { throw new Error("Not implemented!") },
}
Anyways, intent to let the issue breath :).
I strongly feel that if/else as top level pattern matching clauses deviates far enough from what we've come to expect from pattern matching in other languages, that a unique signifier is warranted. Interested to hear what other folks think.
Also, thanks a ton for the engagement @ljharb ! Hopefully my excitement to see PM in ECMA someday is not being misconstrued as surliness 🙌
I'm not sure I understand your examples - why would you ever make a pattern match with just an else?
But you're right, it does look odd when else stands by itself.
To be honest I’d never even considered that someone would make a match clause with just an else (-‸ლ)
we could prohibit it, i suppose, but I’m not sure that would provide a lot of value, and it’d make partial match constructs annoying (ie, start with just an else, reload, add a clause, reload)
we could prohibit it, i suppose, but I’m not sure that would provide a lot of value...
If you would entertain my perhaps contrived example-- I can see this flow unfolding:
That seems like a perfectly reasonable thing that shouldn't be prohibited, even if on the edge.
I can also imagine declaring a base matcher with intent to compose it with other matchers.
I'm not sure I understand your examples - why would you ever make a pattern match with just an else?
Approaching this with fresh eyes, I'm not trying to provide examples of what common cases might yield, but instead what should be possible to express with the new syntax.
From this vantage, there are a few things that run counter to what I would expect from pattern matching IMHO. The need to constrain when specific keywords are used within the block, and an unbalanced order of semantically similar keywords feels inelegant:
if/when/else does<catch-all | pattern> <guard>, is much more balanced. There's less procedure involved for how to write what.I still don't see any compelling argument that <catchall | pattern> <guard> is any better than <catchall | pattern | guard>. I think this purely comes down to one's subjective aesthetic preferences and don't see how one or the other could involve "[more or] less procedure for how to write what".
@mpcsh But is it really<catchall | pattern | guard>?
Isn't it more like, <catchallguard | pattern guard? | catchall>, and rules for where each can be used?
The current grammar in spec.emu appears to be out of date, but should be fine to illustrate:
MatchStatement ::
match [no LineTerminator here] ( Expression ) { WhenClauses }
match [no LineTerminator here] ( Expression ) as LeftHandSideExpression { WhenClauses }
WhenClauses :
WhenClause
WhenClauses WhenClause
WhenClause :
when MatchPattern Initializer[opt] MatchGuard[opt] { WhenClauseBody }
MatchGuard :
if ( Expression )
if/else is more like ( keep me honest here ):MatchStatement ::
match [no LineTerminator here] ( Expression ) { MatchClauses }
match [no LineTerminator here] ( Expression ) as LeftHandSideExpression { MatchClauses }
MatchClauses:
CatchAllClause
CatchAllIfClauses WhenClauses[opt] CatchAllClause[opt]
CatchAllIfClauses:
CatchAllIfClause
CatchAllIfClauses CatchAllIfClause
CatchAllIfClause:
MatchGuard WhenClauseBody
CatchAllClause:
else WhenClauseBody
MatchArm :
when MatchPattern Initializer[opt]
<default-token>
WhenClause :
MatchArm MatchGuard[opt] { WhenClauseBody }
I would appeal to this simplicity as a compelling enough argument.
Other compelling reasons (synthesized from above)
compare:
_guarded catchalls first, then maybe when clauses, then maybe else clause. Else clause has to be last. A bare guard indicates a catchall, but also else does as well_
as opposed to:
_catchall or when clause followed an optional guard_
That's undeniably less cognitive effort.
Eliding the catchall token for a guarded catchall, and using adjacently the same syntax in a WhenClause is confusing, because it overloads two concepts.
All the use cases for guards require them to be interleaveable. The only thing it makes any sense to me to mandate ordering of is that the catch-all be last.
_edit: sry tone was off in first response_
All the use cases for guards require them to be interleaveable. The only thing it makes any sense to me to mandate ordering of is that the catch-all be last.
Doesn't really change the thesis, maybe I'm missing your point
Interleavable guards
MatchStatement ::
match [no LineTerminator here] ( Expression ) { MatchClauses }
match [no LineTerminator here] ( Expression ) as LeftHandSideExpression { MatchClauses }
MatchClauses:
CatchAllClause
CatchAllIfClauses CatchAllClause[opt]
CatchAllIfClauses:
CatchAllIfClause
CatchAllIfClauses WhenClauses
CatchAllIfClauses CatchAllIfClause
CatchAllIfClause:
MatchGuard WhenClauseBody
CatchAllClause:
else WhenClauseBody
and
_guarded catchalls, then maybe when clauses or guarded catchalls, then maybe else clause. Else clause has to be last. A bare guard indicates a catchall, but also else does as well_
I've thus far put forward a pretty comprehensive case for why a simpler grammar is warranted. But also:
```fsharp
let rangeTest testValue mid size =
match testValue with
| var1 when var1 >= mid - size/2 && var1 <= mid + size/2 -> printfn "The test value is in range."
| _ -> printfn "The test value is out of range."
</details>
<details>
<summary> <a href="https://caml.inria.fr/pub/docs/oreilly-book/html/book-ora016.html">OCaml </a> </summary>
```ocaml
let fruit name = match name with
| "delicious" -> "banana"
| "delightful" -> "grapefruit"
| _ when my_impure_bit -> "sour"
| _ -> "breadfruit"
;;
```rust
match num {
Some(x) if x < 5 => println!("less than five: {}", x),
Some(x) => println!("{}", x),
_ => (),
}
</details>
<details>
<summary> <a href="https://github.com/purescript/documentation/blob/master/language/Pattern-Matching.md#guards"> PureScript </a> </summary>
```purescript
case _ of
n
| 0 == mod n 15 -> "FizzBuzz"
| 0 == mod n 3 -> "Fizz"
| 0 == mod n 5 -> "Buzz"
| otherwise -> show n
```haskell
bmiTell :: (RealFloat a) => a -> String
bmiTell bmi
| bmi <= 18.5 = "You're underweight, you emo, you!"
| bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
</details>
<details>
<summary> <a href="https://elixir-lang.org/getting-started/case-cond-and-if.html"> Elixir </a> </summary>
```elixir
case {1, 2, 3} do
{1, x, 3} when x > 0 ->
"Will match"
_ ->
"Would match, if guard condition were not satisfied"
end
```scala
x match {
case x if x > 0 => "It's good!"
case _ => "other"
}
</details>
<details>
<summary> <a href="https://2ality.com/2017/12/pattern-matching-reasonml.html"> ReasonMl </a> </summary>
```reason
let max = switch tuple {
| (x, y) when x > y => x
| (_, y) => y
};
Nearly every single mainstream implementation of pattern matching or case expression has not seen the need to embed if/else clauses top-level, or introduce multiple ways to express catch-all/wildcard expressions.
Python, rejected an else clause
So, conversely, what is the justification for deviating? Introducing this into the language as-is has got to be stronger than
I mean, "else" is really what I'm after here, not "default" :-)
Why should ECMAScript be different? Have yall done any qualitative research around it?
Correct me if I'm wrong, but I presume the reason is because they're avoiding the nil pattern variable (_) that all other implementations have. Javascript already has a pre-existing idea that bare commas in an array mean "skip the value", whether you're creating an array literal or destructuring one (e.g. const [,b] = 'abc'). Allowing this behavior in pattern-matching removes most of the needs for a special nil-pattern variable. Choosing to not implement the nil pattern provides the benefit that you won't be forced to pick a variable, such as "_" or "$" which shadow commonly used libraries, but also means you have to do something about the one remaining case that hasn't been dealt with, an empty when clause. In other languages, you're often able to do something like the following when (_) if (...) or when (_), in Javascript, they decided to allow top-level if and an else instead. You mentioned at the beginning that you're also against using "_" for a solution, which means we're left trying to figure out how best to deviate from the norm, and there's going to be conflicting ideas.
Python, rejected an else clause
Note that none of the listed reasons for Python rejecting an else apply in JS.
else suffixes like Python's for/elseIsn't it more like,
<catchallguard | pattern guard? | catchall>, and rules for where each can be used?
It's not, at least not in the mental model we're working with and trying to promote for the spec. It looks like your mental model is tripping over "if without when" as being a fundamentally different sort of clause, but that's not the intended interpretation here - instead, you either have a catchall (else, which also has to be the last clause in the list, for coherency reasons) or you have a matcher clause, which is a when and/or an if. There's nothing special about "if without when" that makes it get treated differently from "when without if" or "both when and if".
I also think you might have inferred a non-existent rule restricting the placement of "if without when" - if so, there isn't one; all the non-catchall clauses can be in any order.
Does this help?
Nearly every single mainstream implementation of pattern matching or case expression has not seen the need to embed if/else clauses top-level, or introduce multiple ways to express catch-all/wildcard expressions.
Several of your examples do show if-conditions at the top level. The Haskell and PureScript examples are only that, in fact. (I wouldn't be surprised if, under the covers, this is actually doing some pattern-matching on the <= function or something, but that's Haskell tricksiness for you. It still looks like a plain boolean condition at the top-level.) A bunch of the rest appear to just require you to put a trivial pattern in front of the conditional first, presumably for parsing disambiguation reasons, but that's effectively still just a top-level conditional. (And I suspect all of them allow it, it's just that some of your examples only have non-trivial patterns.)
Most helpful comment
Correct me if I'm wrong, but I presume the reason is because they're avoiding the nil pattern variable (
_) that all other implementations have. Javascript already has a pre-existing idea that bare commas in an array mean "skip the value", whether you're creating an array literal or destructuring one (e.g.const [,b] = 'abc'). Allowing this behavior in pattern-matching removes most of the needs for a special nil-pattern variable. Choosing to not implement the nil pattern provides the benefit that you won't be forced to pick a variable, such as"_"or"$"which shadow commonly used libraries, but also means you have to do something about the one remaining case that hasn't been dealt with, an empty when clause. In other languages, you're often able to do something like the followingwhen (_) if (...)orwhen (_), in Javascript, they decided to allow top-level if and an else instead. You mentioned at the beginning that you're also against using "_" for a solution, which means we're left trying to figure out how best to deviate from the norm, and there's going to be conflicting ideas.