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

matchable ~= pattern {…} #191

Closed
Alhadis opened this issue Apr 28, 2021 · 20 comments
Closed

matchable ~= pattern {…} #191

Alhadis opened this issue Apr 28, 2021 · 20 comments

Comments

@Alhadis
Copy link

Alhadis commented Apr 28, 2021

IMHO, this proposal might benefit from a simpler syntax, one modelled upon Perl/Ruby/Awk's =~/!~ operators:

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.,

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:

matchable ~= {foo: 1 | 2}        "Doesn't work";
matchable ~= {foo: 1} | {foo: 2} "Works";

Thanks to chainability, however, this does work:

matchable ~= {foo ~= 1 | 2} "Works";

Note this isn't an assignment operation, so logical operators have different precedence:

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:

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

@ljharb
Copy link
Member

ljharb commented Apr 28, 2021

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.

@Alhadis
Copy link
Author

Alhadis commented Apr 28, 2021

~ 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:

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:

// 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..

@ljharb
Copy link
Member

ljharb commented Apr 28, 2021

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.

@Alhadis
Copy link
Author

Alhadis commented Apr 28, 2021

matching should be exhaustive by default.

Why? Unsuccessful matches allow nested patterns to be handled gracefully; i.e.:

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
	{};

@ljharb
Copy link
Member

ljharb commented Apr 28, 2021

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.

@Alhadis
Copy link
Author

Alhadis commented Apr 28, 2021

There are plenty of cases where a benign match failure is desirable; e.g.:

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.

@ljharb
Copy link
Member

ljharb commented Apr 28, 2021

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.

@Alhadis
Copy link
Author

Alhadis commented Apr 28, 2021

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:

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);

@ljharb
Copy link
Member

ljharb commented Apr 28, 2021

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.

@Alhadis
Copy link
Author

Alhadis commented Apr 28, 2021

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.

@ljharb
Copy link
Member

ljharb commented Apr 28, 2021

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.

@Alhadis
Copy link
Author

Alhadis commented Apr 28, 2021

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?

@ljharb
Copy link
Member

ljharb commented Apr 28, 2021

Yes - that’s what the current proposal achieves.

Then, proposing an operator to simplify a perhaps common inline use case becomes much more compelling.

@Alhadis
Copy link
Author

Alhadis commented Apr 28, 2021

Well then, good luck. Hopefully the end result is sufficiently simple enough that the two ideas gel together. 😄

@Jack-Works
Copy link
Member

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; ... }

@Alhadis
Copy link
Author

Alhadis commented Apr 29, 2021

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.

@ljharb
Copy link
Member

ljharb commented Apr 29, 2021

@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.

@ljharb
Copy link
Member

ljharb commented Apr 29, 2021

@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.

@Alhadis
Copy link
Author

Alhadis commented Apr 29, 2021

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 ||).

@mpcsh
Copy link
Member

mpcsh commented Dec 6, 2021

The champions group met today and reached consensus to not include this in the proposal. If there's sufficient interest, this can be implemented as a follow-on proposal, but it's out of scope for the base proposal.

@mpcsh mpcsh closed this as completed Dec 6, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants