Proposal-pattern-matching: matchable ~= pattern {…}

Created on 28 Apr 2021  ·  19Comments  ·  Source: tc39/proposal-pattern-matching

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

design idea

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.

All 19 comments

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 else syntax 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:

  1. Pattern matching operators (this issue)
  2. A 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 ||).

Was this page helpful?
0 / 5 - 0 ratings

Related issues

JLHwung picture JLHwung  ·  6Comments

caub picture caub  ·  8Comments

aem picture aem  ·  7Comments

tabatkins picture tabatkins  ·  6Comments

tabatkins picture tabatkins  ·  4Comments