IMHO, this proposal might benefit from a simpler syntax, one modelled upon Perl/Ruby/Awk's =~/!~ operators:
~~~js
var outcome = matchable ~= pattern expression;
var outcome = matchable ~= pattern { block }; // Sugar for do operator
// Negation
var matched = matchable ~! pattern { block };
// Evaluates to undefined if nothing matches
const undef = true ~= false | null { "This will never be reached" };
~~~
Pattern combinators would be accepted only at top-level; i.e.,
~js
log("Subject is...")
matchable ~= {foo: 1} | {bar: 2} log("... an object containing either %o or %o", {foo: 1}, {bar: 2});
matchable ~= (foo || bar) log("... equal to foo if truthy, bar otherwise");
~
A limitation of this is that nested pattern combinators don't work:
~js
matchable ~= {foo: 1 | 2} "Doesn't work";
matchable ~= {foo: 1} | {foo: 2} "Works";
~
Thanks to chainability, however, this does work:
~js
matchable ~= {foo ~= 1 | 2} "Works";
~
Note this isn't an assignment operation, so logical operators have different precedence:
~~~js
var foo = matchable ~= pattern1 expr1 || pattern2 expr2;
// The above is equivalent to:
var foo =
pattern1.exec(matchable) ? expr1 :
pattern2.exec(matchable) ? expr2 :
undefined;
~~~
Patterns call Symbol(matcher) if defined, with successful matches indicated by an object with a {value} property:
~js
Object.defineProprerty(RegExp.prototype, Symbol.matcher, {
configurable: true,
enumerable: false,
writable: true,
value(matchable){
const value = String(matchable).match(this);
if(value) return {value};
},
});
~
If |/& get rejected, then the suggested syntax can be simplified by using regular short-circuiting operators (??/&&/||), subject to the language's usual grouping logic. This also eliminates the need to discriminate "top-level branches" from "nested" branches).
Most of this was written on-the-fly, so there're probably some stupidly obvious oversights that my dumb arse managed to miss. Hopefully you get the idea… 😓
/cc @ljharb
I think i see what you mean - a way to match a single pattern against a matchable. Would it throw when it doesn’t match? How would else syntax work?
There’s already resistance from the committee about & and | being too similar to bitwise operations; ~ seems like it would fall into the same pushback?
Separate from the answers to these questions, this sounds like a great follow on proposal to this one, rather than something that needs to be done immediately.
~ seems like it would fall into the same pushback?
No, I chose ~= and ~! (instead of =~ and !~, respectively) as the latter are both legal JavaScript. This differs from Perl and Ruby's syntax (but theirs differ from Awk's ~ operator, meaning consistency with other languages isn't really an issue here).
How would
elsesyntax work?
An odd-numbered list of pattern/action pairs would have the last "dangling" pattern interpreted as an action instead:
~~~js
matchable =~ pattern1 { action1 } || pattern2 { action2 } || fallback;
// Example
var outcome = matchable =~ {foo: 1} 1 || {foo: 2} 2 || {foo: 3};
// The above is loosely equivalent to:
var outcome =
(matchable.hasOwnProperty("foo") && matchable.foo === 1) ? 1 :
(matchable.hasOwnProperty("foo") && matchable.foo === 2) ? 2 :
{foo: 3};
~~~
Would it throw when it doesn’t match?
Nope, only resolve to undefined. A throw expression could be used instead: Following the previous example:
~js
// Example
var outcome = matchable =~ {foo: 1} 1 || {foo: 2} 2 || throw "No such foo";
~
This dovetails nicely with the "fallback" logic described above.
There’s already resistance from the committee about & and | being too similar to bitwise operations
Truth be known, I only included those because feedback on #179 seems strongly in favour of | and &. The usual ||/&&/() operators can always be used instead (which also eliminates the need for any "top-level" bias..
Choosing anything with = in front could only ever be a form of assignment, so I assumed that's why you chose ~=.
Nope, only resolve to undefined
This violates the priorities mentioned in the readme - matching should be exhaustive by default.
matching should be exhaustive by default.
Why? Unsuccessful matches allow nested patterns to be handled gracefully; i.e.:
~~~js
const subject = {foo: 1, bar: 0};
subject =~
{foo =~ [1 || 2]} && // Pass: expected 1 or 2, subject.foo is 1
{bar =~ 1} // Fail: expected 1, subject.bar is 0
{ … };
~~~
Because a match construct that doesn't match and doesn't throw is far more likely to be a bug that goes unnoticed than a match construct that doesn't match and does throw when that's intentional - ie, safety.
There are plenty of cases where a benign match failure is desirable; e.g.:
~~~js
function makeVector(value){
if(value =~ [
x =~ typeof == "number",
y =~ typeof == "number",
x =~ typeof == "number",
]) return new Point3D(x, y, x);
else if(value =~ [
x =~ typeof == "number",
y =~ typeof == "number",
]) return new Point2D(x, y);
else if(value =~
({x =~ typeof number} || throw "Missing `x` property") &&
({y =~ typeof number} || throw "Missing `y` property")
}) return new Point2D(x, y);
else throw TypeError("Unable to resolve cartesian coordinates");
}
~~~
Whether or not coverage needs to be airtight is a decision only the author can make.
Sure - and that decision, per this proposal's priorities, must be explicitly present in the code - just like how else is required in a match expression to avoid an exception for a failure to match.
The coverage requirement makes sense in Rust, which needs to consider all possible control paths (due to static compilation, etc). That's antipodal to a dynamic, high-level language like JavaScript.
I mean, we could impose this requirement. But doing this reduces the operator's flexibility. Not to mention users will just stick a dummy branch so cases like the above example can work:
~js
if(value =~ [
// HACK: Fallback to undefined to avoid throwing an error
x =~ typeof == "number" || {undefined},
y =~ typeof == "number" || {undefined},
x =~ typeof == "number" || {undefined},
]) return new Point3D(x, y, x);
~
The strict intention is to reduce flexibility, in order to increase correctness. It's totally fine if users want to stick else {} on the end if that's the semantic they want - at least that way it's intentional.
In that case, there's no reason this can't be two separate syntaxes:
match (value) { … } clause like the one this proposal describes, but all pattern matches must be accounted for.The current proposal is your number 2 there.
The operator, as I said, is a great idea for a future proposal, dependent on the semantics established by this one, but I don't think it should be in scope of this proposal.
Wouldn't it be more logical to resolve the pattern-matching semantics first? That's arguably the most important and compelling part.
How exactly would this idea be proposed?
Yes - that’s what the current proposal achieves.
Then, proposing an operator to simplify a perhaps common inline use case becomes much more compelling.
Well then, good luck. Hopefully the end result is sufficiently simple enough that the two ideas gel together. 😄
This is an interesting idea but yes maybe we won't do it now.
But is it possible to join them into one?
One branch mode: _Expression_ matches _MatchPattern_ -> _Expression_
Multiple branch mode: _Expression_ matches { _MatchPattern_ -> _Expression_; _MatchPattern_ -> _Expression_; ... }
Actually, I'm wondering if "branches" should just be regular short-circuiting operators (??/||/&&, etc), subject to grouping. The only difference is the expression terminates once it's followed by something that isn't an operator (signifying the beginning of the action block/expression.
@Jack-Works i'm sure we could come up with grammar that was complementary - but that would still mean it could be easily added as a followon proposal.
@Alhadis There's been a lot of delegate feedback that || and friends are a non-option (due to a number of kinds of confusion: their use in "pattern mode" despite being only otherwise used in an expression context; the concept of "truthiness" vs "falsiness" as it compares to "matched" and "didn't match", etc), so I don't think that's going to be a viable path.
Fair enough. As long as the syntax is consistent and feels like a natural addition to JavaScript (instead of resembling Frankenstein's monster as if it were stitched together from the bodies of other programming languages…) 🐘
In any case, the OP has been updated to accommodate both outcomes (i.e., | vs ||).
Most helpful comment
Because a match construct that doesn't match and doesn't throw is far more likely to be a bug that goes unnoticed than a match construct that doesn't match and does throw when that's intentional - ie, safety.