I'm wondering if we can minimize the amount of repeated syntax in each branch, at least for common cases.
Some examples:
match (x) {
{ status: 200, body, ...rest } {
handleData(body, rest);
}
({ status: 200, body, ...rest }) {
handleData(body, rest);
}
[a, b, c] {
}
([a, b, c]) {
}
^bar {}
{ status: 400 } if (something) {}
({ status: 400 }) if (something) {}
else { throwSomething(); }
}
Note there's bare guards as well, so your example should add in some if (condition) { … }.
That will likely illustrate the inconsistency if only if and ^() has parens, but nothing else does, which is partially why we ended up going with the parens (In every case except when ^bar).
Similarly, if there's no "intro" (no guards, and no when on patterns) then it seems a little odd to me to have else.
It's an idea to consider, but I'm pretty opposed to removing the explicit boundary markers (the parens) - I don't find any value overall in omitting punctuation that helps group and aid understanding.
Removing when is certainly worth discussing. (I do like the symmetry of when and else, but since if lacks it that's not a strong argument)
I'm not a big fan of when either, but in the previous version of the proposal that used case it was mentioned that one of it's purpose was to have faster parsing or at least, simplify parsing. I'd rather have no keywords like in rust, but in the context of js we probably want to parse as fast as possible
Worth noting that Rust uses _ as both the nil pattern and as else - I'm not sure that's feasible in JS.
i wouldn't mind if we got rid of else and just had a wildcard binding like rust. you could use whatever name you want, including _.
With the current proposal, you could avoid else entirely and use when (_) { … } if you like.
Anecdotal but reading this without the when is more difficult to tell where each pattern is for me. I think when provides a nice demarcation visually "here is the next pattern".
Same for me, I'm used to rust which doesn't have a keyword, but it has a => for every branch so you can still easily see where a new branch start, but in this case with no keyword or symbol it's really hard to visually parse. My point being that if you want to remove the keyword you should at least have a symbol or something to make it easier to visually parse.
Yup, I feel pretty strongly we need at least one of when or a separator glyph, for readability if nothing else.
I'd feel much better about the kotlin/rust approach of an =>/-> after than the ml approach of a |/when before.
another up vote for changing when to |
not only it's already there in ML family and Haswell
but also for both erlang and elixir when is for guards, similar to if in this proposal.
so it's bit of the brain twisting for erlang and elixir developer
I think something similar happen to other language for the usage of the keyword when,
so I propose use a symbol like | to avoid the semantic collision of when, moreover I believe use single character
token is both easy for parser and human eyes
I think when would be better use as guard( replace if() in the current proposal)
although if() reads okay for English speakers, it would confuse all the programmers and the parser,
I think it's bad to reuse if in the pattern matching context, programmers could ask if I can use if(),
why can't I use 'X' like another nest switch() expression 🙀
make things worse is that if() is the only why to express complex patterns, so it would be used a lot,
programmers have to do a lots of context switches to understand the semantic
I think it's nice to have options to get rid of the extra ( ), I don't have strong opinion, I'm grateful to have
pattern matching at all, although it looks better and cleaner, but I don't know the parsing cost for it
so put 3 things together:
match (x) {
| { status: 200, body, ...rest } {
handleData(body, rest)
}
| [a, b, c] {
}
| ^bar {}
| { status: 400 } when (something) {}
else { throwSomething(); }
}
alternative would be ->, besides it needs two character to parse, it also has semantic collision with function return type in language like swift
match (x) {
-> { status: 200, body, ...rest } {
handleData(body, rest);
}
-> [a, b, c] {
}
-> ^bar {}
-> { status: 400 } when (something) {}
else { throwSomething(); }
}
I think | aligns nicely and feels cleaner
@mko-io some delegates are concerned that | is too similar to bitwise OR to be used for a pattern combinator; I don't think there's any way it'd fly to make it precede patterns. To me, that particular symbol does not in any way suggest a new match clause.
@ljharb That makes sense, I totally understand this concern. what about other symbol like -> or anything other than the when keyword. I'm speaking for most Erlang and Elixir programmers who would have a difficulty time use when for clause and if for guard
| { status: 200, body, ...rest } {
handleData(body, rest);
}
I am fairly strongly opposed to this syntax; I find this very hard to read.
Yeah, really not a fan of it either. It's not JS-y, and quite counter to all existing semantics of |-like things.
i feel like a lot of the points being discussed across the issues in this repo come down to "this proposal has a lot of line noise." i think it would be good to take a holistic look at all the various syntax and whittle it down a bit.
match (x) {
({ status: 200, body, ...rest }) {
handleData(body, rest);
}
([a, b, c]) {
}
^bar {}
({ status: 400 }) if (something) {}
else { throwSomething(); }
}
this seems very usable to me.
There's a lot of cases where having "nothing explicit" start the line would be much noisier, though - especially if we add a "bare expression RHS" form.
alternative would be
->, besides it needs two character to parse, it also has semantic collision with function return type in language likeswift
I'm not sure why this is even a concern. This is a JS proposal, it has nothing to do with swift.
({ status: 200, body, ...rest }) {
handleData(body, rest);
}
This version is actually a bit more noisy to me, because it's harder for my brain to figure out where each piece of the clause starts and ends. It's still usable, but it's a bit more cognitive load.
I share Jordan's priority that we should be optimizing for readability, not writeability. Accordingly, I'm less worried about line noise than cognitive load.
i'm arguing about readability as well. i find it more difficult to read.
Completely understand that; I'm not trying to discredit your perception of readability, it's equally valid. I'm just trying to say that less noise / less syntax doesn't always make things more readable for me.
@devsnek If you look at the prior art section, every other language with a pattern matching operator uses either a RHS keyword/symbol or a LHS symbol. None of them uses no symbol or keyword. With bigger or nested objects you would have a bunch of curly braces with almost no visual way to see where a new pattern starts. Unless you are proposing to use whitespace as a separator, but that would again be very uncommon.
@IceSentry i'm cool with a rust-like arrow or something for the body (i said so above). i don't really understand the "hard to find the start of a pattern" thing though. are you worried about people writing multiple patterns+bodies on one line or something? that seems bad no matter what syntax we choose.
My concern is that it won’t always be clear when the preceding RHS ends and the next LHS begins without an explicit syntactic marker.
I'm assuming most people won't separate it with whitespace (which is common in other languages with the feature) like this:
match (x) {
({ status: 200, body, ...rest }) { handleData(body, rest); }
([a, b, c]) { }
^bar { }
({ status: 400 }) if (something) { }
else { throwSomething(); }
}
And if you add a few lines of logic in each branch it would become a whole lot of curly with almost nothing to visually break it up.
I still find it more readable without the when. maybe that's just me, but I find that unlikely given the design of other languages.
I propose match not only pattern but also type (by comparing constructors or class ids via instanceof internally) at the same type as doing in many langs. And use .. (double dot) for skipping this:
const getLength = vector => match (vector) {
.. ({ x, y, z }) Math.hypot(x, y, z);
.. ({ x, y }) Math.hypot(x, y);
.. ([...etc]) vector.length;
};
is same as current proposal but if you match only Vec3 class it may look like this:
class Vec3 { constructor(x, y, z) { ... } }
const getLength = vector => match (vector) {
Vec3 ({ x, y, z }) Math.hypot(x, y, z);
Vec3 ({ x, y }) Math.hypot(x, y);
.. ([...etc]) vector.length; // ArrayLike
.. { /* deafult */ 0; } // others
};
@MaxGraey the "type" check you mention is instanceof semantics, which are brittle and harmful and I will simply not permit to be privileged in the language. You can use a guard or a custom matcher if you're willing to accept that brittleness in your application.
Hmm, but why this brittle and harmful? Especially is this optional? For example look at Python's match:
coordinate = Coordinate(1, 2, 3)
match coordinate:
case Coordinate(0, 0, 0):
print('Zero point')
case Coordinate(x, y, z) if z == 0:
print('Point in the plane XY')
case _:
print('Another point')
Rust and Haskell use same approach. Basically I think PM without this important feature is half-baked
I can't speak to Python's semantics, but instanceof is forgeable, and does not work across realms (like, an iframe's Array is not the same object as the main page's Array).
I can't speak to Python's semantics, but instanceof is forgeable, and does not work across realms
It's not be instanceof ofc. It just as example. I agree much better is matching by constructor which much more stricter:
match (obj) {
Array () { console.log('array') }; // or Array .. { }
String () { console.log('string') }; // or String .. { }
Object({ length }) { console.log('array like object') };
.. { console.log('string') }
}
which equivalent to:
switch (obj.constructor) {
case Array: { console.log('array'); break; }
case String: { console.log('string'); break; }
case Object: { if ('length' in obj) console.log('array like object') }; break; }
default: { console.log('string') }
}
The constructor property is also unreliable, and i would be staunchly opposed to any language feature that relies on it being present.
In this case, having such a feature for PM, but more reliable, is even more necessary than I thought!
This proposal could utilize it if it exists, but shouldn’t be attempting to add it.
This issue is about changing or removing the when keyword, this discussion feels a bit out of scope. @MaxGraey you should probably create a new issue.
I agree with needing the LHS keyword or symbol. I prefer the readability of a keyword myself. I have a suggestion which addresses a couple things:
match (item) {
pattern ({ thing: 'dog' }) {
console.log('matched first pattern');
}
pattern ({ thing: 'cat' }) {
console.log('matched second pattern');
}
pattern ({ thing }) when (thing.length >= 3) {
console.log('matched third pattern');
}
pattern () {
console.log('matched "else" pattern');
}
};
Pros:
item to pattern then do item to pattern when condition is true, then dodo if no matches abovewhen is a guard (as it sounds to me, and maybe just a bonus that the keyword is used like that in other languages 🤷 )Cons:
pattern is 3 characters longer than whenpattern () is ambiguous whether it matches _everything_ or _nothing_ but given the context and the concept behind pattern matching, I think developers would assume it matches everythingOverall I think getting rid of the namespace collisions of if/else and having the "else" written the same way as the other patterns helps reduce "noise" and ambiguity by promoting consistency and clear, specific verbiage. I'm happy to move this into a separate issue if people think they'd like to discuss it more, just adding here because of relevant content.
@davidprae the other problem with that is that pattern(^(expression)) is then more confusing.
@ljharb Hmm, you're saying this would be confusing?
const LF = 0x0a;
const CR = 0x0d;
match (token) {
pattern (^LF) { ... }
pattern (^CR) { ... }
pattern () { ... }
}
I could see the special pin operator being fine without parens since there is _some_ symbol there for visual assistance, and I think it makes sense conceptually:
const LF = 0x0a;
const CR = 0x0d;
match (token) {
pattern ^LF { ... }
pattern ^CR { ... }
pattern () { ... }
}
The parens are necessary for a complex expression.
I suppose it's very subtle - i'm saying that when (<pattern>) vs when ^(<expression>) seems fine to me, but I find pattern (<pattern>) vs pattern ^(<expression>) to be a bit confusing.
@ljharb Gotcha. I'm not sure how to word this but the <expression> winds up being a pattern though right? We're using an expression to represent or evaluate to(?) a pattern. We're always matching a "pattern" with this Pattern Matchingâ„¢ functionality aren't we? Unless by Pattern Matching, we mean Pattern or Expression Matchingâ„¢.
So it always winds up as pattern (<pattern>), even if written as pattern ^(<expression>).
I don't know anything about an implementation so my thoughts are only semantics-deep.
Not exactly - the expression ends up being a value, that's matched against using leaf pattern _semantics_, but a value is not itself a pattern.
Ah, ok 👌 Well how about using the word you just used to describe the process: "against"? It has no namespace collisions, it's very general so matching "against" _anything_ makes sense (patterns, expressions, whatever else the future has in store), and it is still very readable. The "catch-all" case even reads better than the pattern () syntax. against () can be read as "match against nothing".
match (item) {
against ({ thing: 'dog' }) {
console.log('matched first case');
}
against ({ thing: 'cat' }) {
console.log('matched second case ');
}
against ({ thing }) when (thing.length >= 3) {
console.log('matched third case');
}
against () {
console.log('matched "else" case');
}
};
const LF = 0x0a;
const CR = 0x0d;
match (token) {
against ^LF { ... }
against ^CR { ... }
against () { ... }
}
I think having the catch all pattern using the same keyword as the other branches is fine, but is unrelated to the concept of changing or removing the when keyword.
Personally, I don't see why against or any other keyword would be better than when. This issue was originally created because some people feel that using a keyword at all is too verbose, so choosing a keyword that is even more verbose makes even less sense to me. At least not in the context of this issue.
I would prefer no keyword, but if we need one, I would much prefer a shorter keyword considering how often pattern matching is used in languages that have it.
@IceSentry My ideas were more of a response to comments in this issue, not a response to the initial issue (a commenter was resistant to when because it sounds like a guard to them).
But for me, I don't think a difference of just 3 characters is significant in a keyword (when vs against or pattern). In general, we should optimize for readability, not type-ability. That said, I think when is readable and a fine option so I'm not advocating against it 👌
@IceSentry
but in the previous version of the proposal that used case it was mentioned that one of its purpose was to have faster parsing or at least, simplify parsing.
I'm interested in the context around this. Do you have any of the links handy that mention why this might be the case, or go into detail around parsing perf w.r.t keyword vs no keyword?
Similarly, I'd very much prefer to see no keyword here. I don't quite understand which part would make parsing more difficult, and no keyword -- perhaps coupled with a comma-separated list -- would feel more in-line with other parts of the language. I can think of two ways to execute this in a way that would already be familiar to javascript developers:
const x = match (expr) {
({ status: 200, body, ...rest }) { ... },
([a, b, c]) { ... },
else { ... }
}
const x = match (expr) {
({ status: 200, body, ...rest }) => { ... },
([a, b, c]) => { ... },
else => { ... }
}
Both are somewhat pleasantly reminiscent of an object initializer with shorthand method names, or arrow function expression. _I think_ I'm more in favor of the latter, but have an affinity for both.
The upside here is that there's prior art: shorthand method names are battle-tested in the wild and have proven to be quite readable.
I'm fine with "->" if people prefer that. But definitely not "=>", especially since the block afterwards is a do block, not a function body. That's just too syntactically similar to arrow functions while having different semantics in the body of the curly brackets.
const result = match (data) {
({ x }) => {
f() // f() is called and result gets set to the return value.
}
// ({ a }) is pattern matching, ({ b }) is destructuring
({ a }) => ({ b }) => {
f() // f() is called but its return value is ignored.
}
}
I don't have strong preferences on this matter (I guess that's why this is "bikeshedding"), but I'll state them anyways.
I would prefer using the keyword "when" - I feel like it's more consistent with how the rest of the language is shaped.
if (...) { ... }
while (...) { ... }
catch (...) { ... }
function (...) { ... }
when (...) { ... }
It also looks better when paired with if or else.
match (obj) {
// This just feels a little off
(...) { ... }
else (...) { ... }
}
I would also prefer to leave symbols out of this (e.g. replacing "when" with a symbol, or putting a symbol between the pattern and the block). Reasons include:
I don't think there's a need to be concise here any more than there is with "if", "while", "catch", etc.
@dpchamps I don't have any links unfortunately. The discussion didn't really go in any details. It was just pointed out that with a keyword at the start of the line it's easier to determine if something is a new case instead of having to parse until a symbol or the next block. Is it really an issue in practice? I have no idea and until someone actually implements it, or someone actually knowledgeable about this confirms it I don't think we should focus too much on this.
I'm against this,.. it's shorter, yes, but less readable
That's very subjective, to me it's more readable since it's what I'm used to and adding a keyword adds more noise which makes it more annoying to read.
Most helpful comment
I think having the catch all pattern using the same keyword as the other branches is fine, but is unrelated to the concept of changing or removing the
whenkeyword.Personally, I don't see why
againstor any other keyword would be better thanwhen. This issue was originally created because some people feel that using a keyword at all is too verbose, so choosing a keyword that is even more verbose makes even less sense to me. At least not in the context of this issue.I would prefer no keyword, but if we need one, I would much prefer a shorter keyword considering how often pattern matching is used in languages that have it.