Proposal-pattern-matching: Bikeshed issue: pin operator

Created on 21 Apr 2021  ·  37Comments  ·  Source: tc39/proposal-pattern-matching

The proposal includes the "pin operator", currently spelled ^. Please suggest alternatives, ideally with compelling arguments for them, and I'll update the OP to include those options. Thanks!

Parentheses

An earlier draft of the updated proposal used parentheses to distinguish between a pattern and an expression, but we decided this would both be too subtle and confusing, and also be difficult to make work with nested patterns.

Keyword

Note that choosing a keyword here might get a bit awkward with parens/precedence, since we need to be able to "escape" pattern mode deep inside nested patterns. Here's an example (that needs improving) using an expr keyword:

const E = 3;

match (matchable) {
  when expr E { … }
  when expr Math.pow(E, 2) { … }
  when expr (E * 2) { … }
  when ({ a: { b: { c: [d, expr E, expr (E * 2)] } } }) { … }
}

Options:

  • expr: short for "expression" ¯\_(ツ)_/¯
  • matches

Sigil

Using a sigil (a syntax token) is a bit more concise, which is why the champion group currently prefers it.

Options:

  • ^: this has precedence from Elixir, and visually seems subtle while also being explicit.
  • ${ … }: analog to template literal substitution, allows for easy nesting, curly brace boundaries allow for consistent grouping (meaning, we wouldn't need to special-case identifiers etc)
champion group discussion help wanted syntax discussion

Most helpful comment

What about ${E} , being an analogue to placeholders in template literals, and meaning “evaluate this”:

const E = 3;

match (matchable) {
  when ${E} { … }
  when ${Math.pow(E, 2)} { … }
  when ${E * 2} { … }
  when ({ a: { b: { c: [d, ${E}, ${E * 2}] } } }) { … }
}

All 37 comments

What about = (or == or ===)?

match(matchable) {
when = E { ... }

Perhaps the second one implies coercion but the behavior (if I understand) is "when matchable equals this variable/expression, run this other expression", so maybe that works?

It doesn't have equality semantics if the expression is an object that participates in the matching protocol, so that seems like it'd be confusing?

I haven't looked much into it, but intuitively I don't understand what the expr or ^ is for. (also I'm not a fan of dead characters used as punctuation, in languages where they're actually used, you need to press ^ and then space to make sure you don't type â by accident).

if I understand it correctly, you need some kind of symbol to distinguish between "evaluate the expression, then match" and destructuring matching, maybe the destructuring needs some kind of operator. In that case it could be something like in or from

Basically correct - a way to distinguish between "patterns" - which include irrefutable matches (like foo, which otherwise would refer to a variable in scope), destructuring patterns, regex literal patterns, primitive literal patterns, etc - and "expressions" (like foo, the variable in scope, which may or may not participate in the matching protocol).

One of our top priorities is that this is _pattern_ matching, and so we will always optimize for the ergonomics of patterns - so we prefer to add syntax weight to expressions instead of patterns, if needed.

New keyword suggestion: matches:

match(matchable) {
  when matches E { ... }
}

Con off the top: matches isn't necessarily clear that it matches an expression only (vs a pattern in the normal case), but worth considering.

Really excited by the revamped proposal!

I know it's not an option since it's already in use, but I feel like eval would have worked nicely

Maybe parentheses? Might be confusing though.

match (matchable) {
  when Foo { /* match any value */ }
  when (Foo) { /* match against the value of Foo */ }
}

@Haroenv If it wasn't already a word attached to shame and insecurity and badness, sure :-)

@j-f1 good call! we actually had parentheses originally, but decided it was too subtle (and doesn't work well with nested patterns), and switched to this form.

I've added this to the OP, and I'll link to and hide these comments :-)

What about ${E} , being an analogue to placeholders in template literals, and meaning “evaluate this”:

const E = 3;

match (matchable) {
  when ${E} { … }
  when ${Math.pow(E, 2)} { … }
  when ${E * 2} { … }
  when ({ a: { b: { c: [d, ${E}, ${E * 2}] } } }) { … }
}

That... is not bad. Slightly heavier-weight for single variables (${x} vs ^x) but I'm not sure that's a bad thing. Always-wrapping does let us avoid having to teach the distinction between syntax that can take the prefix directly and syntax that needs a wrapping paren. The call-back to template literals avoids the "too subtle" issue Jordan mentioned wrt just using parens. And the semantics are pretty spot-on, meaning "evaluate me as normal JS, then sub the result into my surrounding context" in both strings and here. I like it.

Agreed, a pretty decent suggestion. I'll add it to the OP!

@tabatkins and I had a short discussion on the pin operator at the end of the champions call this morning. One possibility that hasn't been recorded here is restricting the operand - currently, we're allowing the following without parentheses:

  • Identifiers (foo)
  • Chains (foo.bar)
  • Function calls (foo())
  • Combinations of the above (foo.bar())

What if those were the _only_ things you could stick on the right of ^? It would simplify the semantics a bit and obviate some questions & concerns, and the composability sacrifice might not actually be too bad.

The only difference would seem to be "4 things with a parenthesized expression" or "4 things" - iow, the simpler form seems to be to _just_ allow parenthesized (or any specific token-bounded) expressions.

The only difference would seem to be "4 things with a parenthesized expression" or "4 things" - iow, the simpler form seems to be to _just_ allow parenthesized (or any specific token-bounded) expressions.

So ${expr} is the perfect choice if you require that

I agree, "always use ${<expression>}" would be the logical choice if we think it's critical to have a single, simple form. I'm not sure that's necessary, though.

Here's one thing I currently find a bit jarring about the pin operator:

when (2) { ... } // Does the value equal 2?
when (x) { ... } // Assign the value to x
when (^x) { ... } // Does the value equal x?

// Similar issue during destructuring:
when ({ a: 2 }) { ... } // Does it equal 2?
when ({ a: x }) { ... } // Assign to x
when ({ a: ^x }) { ... } // Does it equal x?

Might I propose that we're also required to pin literals (whatever syntax we choose)? I know comparing against literals is a common scenario and shouldn't be overly complicated to do, and I know this is a bit different from prior art, but I feel like it adds consistency and reduces confusion.

@theScottyJam that wouldn’t allow us to safely check undefined, for one

@ljharb - could you expound? Are you suggesting that when (^undefined) would match both undefined and null? What comparison algorithm is used with a pin operator, is it not SameValueZero?

@theScottyJam no, i'm suggesting that const undefined = 4; match(void 0) when (undefined) { } needs to match. Using when (^undefined) would be matching 4, because not every literal is a keyword.

This issue would only happen if the user assigned to a variable called undefined in the same file where they're using this match syntax, correct? Aren't we past the time when you might use a third-party library that could accidentally globally override the value of undefined?

I'm not sure I really see this as an issue then. If the code author wishes to redefine undefined in their current scope, then they will be forced to use void 0 whenever they want to compare against undefined, whether we're inside this match block, or if we're just writing a simple if (x === undefined) statement.

If you're using a very archaic bundling algorithm that just smashes files together without giving each module its own scope, then perhaps a bizarre third-party library that has const undefined = 4 at the module level would override undefined in your script, but surely this would cause your script to break in lots of places - anywhere that compares against undefined, unless you're rigid enough to use void 0 everywhere instead of undefined, in which case you can also use void 0 when pinning.

The small likelihood of this being a problem still means it’s nonzero. Additionally, the semantics of -0 as a pattern are still unclear.

It may be nonzero, but the stars and moons have to align for this "fix" to really accomplish anything. I'm going to ignore the case where someone decides to shoot themself in the foot and locally redefine undefine - this same person will probably also redefine globalThis.Date to be 3, and run into the exact same type of problems, and we're not making any attempts to protect against that kind of behavior.

So, here's everything that needs to happen for this fix to provide any value:

  • A third-party library has to deliberately decide to redefine undefined. (const undefined = 4 does not happen by accedent)
  • This third party library has to make this redefinition happen at the module-level scope
  • You have to be using an error-prone bundling system that does not provide a scope for each module (such a bundling system would create the potential for all sorts of bugs, not just this undefined issue)
  • You have to decide to trust and use this bizarre library
    If all of the previous things happen, I would consider your project broken. Surely there are some === undefined checks somewhere, in either your project or another third-party library you're using, and the best thing that could happen would be for Javascript to fail everywhere possible to try and get you to notice this bug, so you can track it down and do something about it. Because of this, I would actually prefer when (undefined) to use the bad value, to help make some noise about the fact that your project is broken. But, for the sake of argument, let's presume Javascript takes the route of trying to silently cover up this issue. Here's what else must happen:
  • You or your third-party libraries must not use === undefined anywhere - otherwise this bug would just manifest itself somewhere else, and the match expression didn't help to cover it up.

I will also note that the redefinition of undefined is just a very specific and unlikely version of a broader class of bugs - messing with built-ins. Every once in a while I see people who really like to protects against this undefined issue by using void 0, but many of them never seemed to worry about values such as globalThis.Map being altered by a third-party library - something that seems far more probable, especially if libraries attempt to apply bad/incomplete pollyfills.

Now let's look at the flip side of things. I would argue that the consistency that's added when requiring a pin on literals would actually help prevent a lot of bugs. One of the biggest criticisms of python's new pattern-matching syntax is how easy it is for someone to forget to pin a variable when pattern-matching. I think some added consistency would help developers to be able to easily remember when a pin is supposed to be used, and when it's not supposed to be used. The problem right now is that a simple literal _looks_ like an expression, so it's easy to think that you can just replace it with another expression, such as the variable x. But you can't! Not without pinning x.

This feature is pattern matching, and as such, patterns must be privileged with syntax. Your suggestion would violate one of the primary constraints in the readme, and this proposal will not be including it.

I don't think the rebinding possibility of undefined is that important. It's certainly nice that in our current write-up it's impossible to confuse (since the literal matcher undefined is always the undefined value), but in practice people use undefined assuming it's the undefined value all the time, and they're virtually always correct. We don't need to worry about it too much, imo.

I can see the logic in making all literals use the pinning syntax, but it's also something we expect to be quite common, and the sort of thing you learn once when it screws up in obvious ways and then get right from then on.

Thanks @tabatkins - I can appreciate that point of view.

So, if the biggest issue is that we don't want to pin every time we use a literal, what if we flip things around? Pinning is the default behavior, and explicit syntax is required to do an irrefutable match?

Here's an example of what I mean. In this example, "^" is the pick-off operator (not a pin operator), and is used to assign that value to a variable.

const value = 3

match ({ x: 2, y: 3, z: 4 }) {
  when ({ x: 2, y: value, ^z }) {
    // Matches! z is assigned to 4.
  }
}

match ([2, 3, 4]) {
  when ([2, value, ^z]) {
    // Matches! z is assigned to 4.
  }
}

This gives us the benefit that we don't have to make a bunch of special case matching scenarios for some literals - people can toss in whatever expression they feel like, and it'll be matched against. So, all of these would be valid (while with the current proposal, some of these expressions have to be pinned while others should not be pinned)

match (obj) {
  when ('abc') { ... }
  when (`abc`) { ... }
  when (String.raw`a\b\c`) { ... }
  when (`a${x}c`) { ... }
  when (Math.PI) { ... }
  when (/\d+/) { ... } // Regex doesn't have to be special-case - it can just implement the matcher symbol
  when (2n) { ... }
  when (BigInt(2)) { ... }
  when (-1 * Infinity) { ... }
}

Update: Just realized there's a syntax issue with this idea. If, by default, I'm evaluating everything as an expression in the when() clause, then when ({ x: 2 }) would mean I'm supposed to match against a just-created object that contains an x property (which would match by identity and always return false). There would need to be some way to indicate that we're trying to pattern-match against the contents of the object, and I can't think of any good way to do that. So, this idea is bust too, unless someone can find a good way around this issue.

Update 2: Actually, there is a way around this issue. An arrow function always assumes that the curly brackets after an arrow signify a block. Parentheses have to be put around the curly brackets to force it to be interpreted as an expression and indicate that we're wanting it to return an object literal. Similarly, curly brackets (and square brackets) in a when expression will always be interpreted as pattern matching. If parentheses were added, it would force us to interpret it as an expression instead (which is useless - comparing against a new object's identity will never be true)

Just so I don't ignore @ljharb's concern about -0, if SameValueZero is the chosen algorithm, I don't think -0 would end up being a huge problem. Since it's not a very common need for people to distinguish between 0 and -0, I'm ok if it's a bit more verbose to do so. In the simplest case, Object.is() can be used in the guard clause. When a lot of -0 matching needs to be done, a custom matcher can be created:

const minusZero = {
  [Symbol.matcher]: value => ({ matched: Object.is(value, -0) })
}

with (stuff) {
  case ([minusZero, 0, minusZero, 0]) { ... }
  case ({ x: minusZero, y: minusZero }) { ... }
}

Pinning isn't just for a single identifier - it's also for any expression - and one of the priorities in the readme is still that syntax aesthetics should prioritize patterns over anything else.

Sorry, I probably didn't present the idea clearly, but you can still do entire expressions.

// Current proposal
const value = 3
match (something) {
  when ({
    a: ^(2 + 2), // a pinned expression
    b: 4,
    c: 'string',
    d: ^`string`,
    e,
  }) { ... }
}

// What I'm proposing
const value = 3
match (something) {
  when ({
    a: 2 + 2,
    b: 4,
    c: 'string',
    d: `string`,
    ^e, // pick e off
  }) { ... }
}

You keep saying that "patterns" should be prefered with syntax, could you expound? - I'm not really sure what you mean by this. What are you defining as a pattern? Shouldn't both of these scenarios be considered part of the pattern syntax that we're trying to privilege:

  • Comparing with any expression
  • Ensuring a object property/array entry exists and picking it off.

There's no way to have both of these features without making a syntax sacrifice in at least one of these areas. Right now, we're putting that sacrifice on most expressions and are exempting certain literals. It's been decided that we don't want to change that to be a sacrifice on all expressions. I'm now suggesting that we move the sacrifice to "ensuring something exists and picking off those variables".

3 is a pattern, foo is a pattern (an irrefutable match), [foo] is a pattern (an iterable containing one irrefutable match), etc.

(see also: https://github.com/tc39/proposal-pattern-matching#pattern)

Pinning isn't just for a single identifier - it's also for any expression - and one of the priorities in the readme is still that syntax aesthetics should prioritize patterns over anything else.

Yeah, this is why we can't flip the default around - it would confuse too much with the actual pattern syntaxes. You could do something more complicated that allows simple values to be unpinned while more complex values need something else, but then people have to learn the difference and recognize when they need to "upgrade" to the other syntax, and it's just overall not a great time.

@theScottyJam, you already ran into the difficulties this flipping-around can cause in your earlier comment - if bare values are value-matched, then [1,2] is doing something with a literal array object, rather than an array matcher. Now, at best, you have to flip things around so that all the "patterns" are prefixed with something that opts them out of expression mode, and as a result you've reinvented the current proposal but with patterns and values swapped in terms of what the syntax leans you towards.

You keep saying that "patterns" should be prefered with syntax, could you expound? - I'm not really sure what you mean by this. What are you defining as a pattern?

Patterns are the special bits of syntax in this proposal that aren't values: array patterns, object patterns, ident patterns, and combinations of those with the | operator and others. These can't mix with ordinary values, because they clash syntactically.

When Jordan says patterns should be preferred with syntax, that's a principle we've been heavily leaning on here to help guide our intuitions. You can already match things by value with if() or switch() pretty easily; the major value-add this new proposal brings is to make it easy to match things structurally with patterns, and pull out bits of those things to work on in the match bodies. Matching against a value is still needed at times, so we need syntax for it, but it's not the primary use-case here; it should be possible, but it doesn't need to be trivial. And as it turns out, as long as the value you want to match against is a literal, we can merge that up into the "pattern" syntax-realm, where it can coexist with the other patterns unambiguously without causing any syntax clashes, so for those cases we can indeed make the value-match just as easy as any other pattern match.

Some thought about syntax clashes should show why more complex expressions that produce values can't be mixed in with the patterns (is 2 | 5 two patterns joined with an "or", or is it a single value 7?), so the only real question left is the syntax collision between ident matchers and "value in a variable". There's a simple, consistent way to resolve that, which we've done in this proposal; swapping it around requires introducing additional novel syntax, and makes the distinction between "patterns" and "expressions" less clear.

Thanks again @tabatkins for the helpful constructive feedback.

I do think what I proposed there would create just as much syntax conflict as an arrow function. Here's some examples of how things would get interpreted (and how it's similar to arrow functions):

(I'll use a syntax for when() that more closely resembles arrow functions to help show the correlation - I saw elsewhere that some syntax is still under debate. In this case, I'm just removing the outer-most parentheses and using an -> as a deliminator. The same principles I'm showing work just as well with the currently proposed syntax too)

match (obj) {
  when { x: 2 } -> { ... } // Interpreted as a pattern, just like `{}` in `() => { x: 2 }` get interpreted as a block
  when ({ x: 2 }) -> { ... } // interpreted as an object (and is completly useless), just like `({})` in `() => ({ x: 2 })` is interpreted as an object
  when { x: 2 } + 2 -> { ... } // Illigal syntax, just like it is with `() => { x: 2 } + 2`
  when ({ x: 2 }) + 2 -> { ... } // Adds an object to a number, just like with `() => ({ x: 2 }) + 2`
  when { a: { b: ({ x: 2 }) } } -> { ... } // Just a reminder that these same syntax rules apply at every level of the pattern-matching. In this case, a.b is matching with an object literal.
}

So, I don't think we can say it's impossible to do, or too confusing to do, otherwise we couldn't have done it with arrow functions. We both can agree that it's never fun having these types of syntax issues, just like no one's really fond of having to put parentheses here () => ({ x: 2 }), but that's not really a show-stopper. And with pattern matching, there's generally not a need to match against an object or array literal, so unlike arrow functions, people aren't going to be using parentheses to force these patterns to be interpreted as expressions instead (making these even less annoying than the arrow-function curly-bracket conflict).

That's a good point with the | issue that I haven't thought of. I have seen some discussion about using the or keyword too, which would work here:

match (obj) {
  when (2 or 3) { ... }
}

In general, as I've thought about this all, I'm realizing that a "pick-off" operator isn't even needed. Here's an updated way to do it:

// Current proposal
match (obj) {
  when ({ x: ^(2 + 2), y: 4, z }) { /* You now have acess to z */ }
  when ([^(2 + 2), 4, z]) { /* You now have acess to z */ }
}

// Updated idea
match (obj) {
  when ({ x: 2 + 2, y: 4, z }) { /* You now have acess to z */ }
  when ([2 + 2, 4, z]) { /* You now have acess to z */ }
}

So, here are the pros and cons I see, that we're weighing against.
pros:

  • We don't have to make a list of special case expressions (the literals) which can be used without a pin.
  • Since there are fewer possible patterns, the learning curve won't be as steep
  • Developers are never going to forget to pin a variable (a common mistake)

cons:

  • We introduce an annoying syntax conflict, a little less severe than the arrow function's syntax conflict, but it's still there.

Now maybe that syntax conflict is bad enough that it outweights those pros - and if that's the way people feel (which seems to be the current sentiment), then I can respect that. But - the idea of completely getting rid of the pin operator and treating all literals and other expressions equally feels pretty nice.

We don't have to make a list of special case expressions (the literals) which can be used without a pin.

We could do that with the current proposal by just removing the literal patterns and requiring you to use the pin operator.

Since there are fewer possible patterns, the learning curve won't be as steep

I don't think literals are a significant learning hurdle to deal with here. ^_^


Putting those to the side, tho, note that your suggestion now has no way to match against the value of a variable. That's the biggest reason we can't combine pattern and value syntaxes together; this is a fundamental clash that needs to be resolved somehow, and our current approach draws a fairly simple line in "everything that should be interpreted as an expression goes in the pin operator, everything else is a pattern". There are other ways to deal with the conflict, but in our explorations none of them ended up better, and they're generally markedly worse.

Then the fact that expression-syntax and pattern-syntax might clash in other ways is a significant secondary concern. We can engineer around it, such as by using or instead of |, but there's still a lot of complexity there, and it hampers our ability to evolve either syntax in the future, as they're now tied together and have to avoid each other. Our current proposal eliminates all that, by safely enclosing expression-syntax in the pin operator's parentheses, cleanly separated from pattern-syntax. The two syntaxes can thus evolve on their own terms.

We could do that with the current proposal by just removing the literal patterns and requiring you to use the pin operator.

That was my first idea that got shot down - I just wanted to make literals require a pin as it felt more consistent - I would still be happy with that 😜

I don't think literals are a significant learning hurdle to deal with here. ^_^

Fair point

Putting those to the side, tho, note that your suggestion now has no way to match against the value of a variable.

Ah whoops, I felt like I was missing something 🤦, alright, so an unpin operator would be required

Then the fact that expression-syntax and pattern-syntax might clash in other ways is a significant secondary concern. We can engineer around it, such as by using or instead of |, but there's still a lot of complexity there, and it hampers our ability to evolve either syntax in the future, as they're now tied together and have to avoid each other. Our current proposal eliminates all that, by safely enclosing expression-syntax in the pin operator's parentheses, cleanly separated from pattern-syntax. The two syntaxes can thus evolve on their own terms.

That's a pretty strong case there. Can't argue with that.

Well, thanks for taking time to help work through these ideas :)

There is an existing binary operator which communicates something like “hey, we’re resolving a reference here” and which I think _might_ be unambiguous if repurposed as a unary prefix in this context:

const E = 3;

match (matchable) {
  when .E { … }
  when .Math.pow(E, 2) { … }
  when .(E * 2) { … }
  when ({ a: { b: { c: [d, .E, .(E * 2)] } } }) { … }
}

Okay, yeah, it’s a stretch. The parenthetical expressions ruin whatever story I was telling myself I guess ... and the ASI people would probably consider it a straight up declaration of war ...

but ... I really do kinda like it? ... :)

@Alhadis proposed \() in https://github.com/tc39/proposal-pattern-matching/issues/179#issuecomment-854655882:

let E = 3;

match (matchable) {
  when \(E) { … }
  when \(Math.pow(E, 2)) { … }
  when \(E * 2) { … }
  when ({ a: { b: { c: [d, \(E), \(E * 2)] } } }) { … }
}

Prior art for this syntax is Swift’s string interpolation (`hello ${world}` in JS is comparable to "hello \(world)" in Swift).

Was this page helpful?
0 / 5 - 0 ratings

Related issues

JLHwung picture JLHwung  ·  6Comments

samuelgruetter picture samuelgruetter  ·  3Comments

elijahdorman picture elijahdorman  ·  6Comments

MoritzKn picture MoritzKn  ·  5Comments

Haroenv picture Haroenv  ·  6Comments