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

Bikeshed issue: pin operator #178

Closed
ljharb opened this issue Apr 21, 2021 · 39 comments
Closed

Bikeshed issue: pin operator #178

ljharb opened this issue Apr 21, 2021 · 39 comments
Labels
champion group discussion help wanted syntax discussion Bikeshedding about syntax, not semantics.

Comments

@ljharb
Copy link
Member

ljharb commented Apr 21, 2021

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)
@ljharb ljharb added syntax discussion Bikeshedding about syntax, not semantics. help wanted labels Apr 21, 2021
@mAAdhaTTah
Copy link

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?

@ljharb
Copy link
Member Author

ljharb commented Apr 21, 2021

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?

@Haroenv
Copy link

Haroenv commented Apr 21, 2021

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

@ljharb
Copy link
Member Author

ljharb commented Apr 21, 2021

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.

@mAAdhaTTah
Copy link

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!

@Haroenv
Copy link

Haroenv commented Apr 21, 2021

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

@j-f1

This comment has been minimized.

@ljharb

This comment has been minimized.

@ljharb

This comment has been minimized.

@nateroling
Copy link

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}] } } }) { … }
}

@tabatkins
Copy link
Collaborator

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.

@ljharb
Copy link
Member Author

ljharb commented Apr 22, 2021

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

@mpcsh
Copy link
Member

mpcsh commented May 11, 2021

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

@ljharb
Copy link
Member Author

ljharb commented May 11, 2021

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.

@Jack-Works
Copy link
Member

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

@ljharb
Copy link
Member Author

ljharb commented May 11, 2021

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.

@theScottyJam
Copy link
Contributor

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.

@ljharb
Copy link
Member Author

ljharb commented May 31, 2021

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

@theScottyJam
Copy link
Contributor

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

@ljharb
Copy link
Member Author

ljharb commented May 31, 2021

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

@theScottyJam
Copy link
Contributor

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.

@ljharb
Copy link
Member Author

ljharb commented May 31, 2021

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

@theScottyJam
Copy link
Contributor

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.

@ljharb
Copy link
Member Author

ljharb commented May 31, 2021

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.

@tabatkins
Copy link
Collaborator

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.

@theScottyJam
Copy link
Contributor

theScottyJam commented Jun 1, 2021

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)

@theScottyJam
Copy link
Contributor

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

@ljharb
Copy link
Member Author

ljharb commented Jun 1, 2021

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.

@theScottyJam
Copy link
Contributor

theScottyJam commented Jun 1, 2021

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

@ljharb
Copy link
Member Author

ljharb commented Jun 1, 2021

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

@mpcsh
Copy link
Member

mpcsh commented Jun 1, 2021

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

@tabatkins
Copy link
Collaborator

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.

@theScottyJam
Copy link
Contributor

theScottyJam commented Jun 1, 2021

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.

@tabatkins
Copy link
Collaborator

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.

@theScottyJam
Copy link
Contributor

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

@bathos
Copy link

bathos commented Jun 4, 2021

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

@j-f1
Copy link

j-f1 commented Jun 5, 2021

@Alhadis proposed \() in #179 (comment):

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

@maxnordlund
Copy link

maxnordlund commented Jul 31, 2021

I'd like to give a bit of background why Elixir has the pin operator in the first place, if nothing else to save those stumbling onto this thread the trouble 😉

Elixir comes from Erlang, so first we must take a look at it's pattern matching (I'm an Erlang developer by day):

Var = 123,
Var = 123, %% Matches
Var = 23 %% Throws a badmatch error

The first is an irrefutable pattern and binds the expression123 to the variable¹ Var. The other two however does a match against an existing value, the first is fine but the second throws badmatch². Instead you would usually write it something like this:

Var0 = 123,
Var1 = 123,
Var2 = 23

As you can imagine, if you have large function you can accidentally refer to an existing variable, and thus match rather then assign, or when editing forget to renumber correctly. Elixir tries to rectify this by auto-numbering your variables:

var = 123
var = 123
var = 23

But what if you want to match on a previous variable? Then comes the pin operator, that works just like this suggestion. Translating the first Erlang example to Elixir:

var = 123
^var = 123
^var = 23

So in Elixirs case you basically never want to pin an expression, only bound variables. For JavaScript I think it means, do we want to only allows matching on existing variables? Then I say go with ^.

Do we want to match on arbitrary expressions? Then go with ${...}.

I think it's a blessing in disguise to only allow variables. You are then forced to extract that, potentially complex, expression and assign it a, hopefully descriptive, name.

match (session.user) {
    when ({ roles: ${Auth.allowedRoles(currentRoute, 1, 2)} }) { ... }
}

// vs

const allowedRoles = Auth.allowedRoles(currentRoute, 1, 2) 
match (session.user) {
    when { roles: allowedRoles } { ... }
}

Another thing to consider is to make it easy to use custom matchers:

match {something) {
    when ^MyClass { ... }

    // vs

    when ${MyClass}
}

I know the current proposal allows for more then just ident for the ^ version, so perhaps the above examples aren't the best, but the authentication example was the first thing that came to mind.

Also, I think allowing function/method calls is a bit to much, dotted path looks simpler even though computed properties can cause side effects. Something that Erlang/Elixirs guards disallows. You can only call a limited set of functions in the guards to allow the BEAM to optimize freely.

¹ There's no variables in Erlang, only const. But they are still referred to as variables, and sometimes bindings.
² Actually it will error, throw is only used by user land exceptions.

@mpcsh
Copy link
Member

mpcsh commented Dec 6, 2021

The champions group met today and reached consensus to switch to ${}. I'll be updating the proposal to reflect this change! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
champion group discussion help wanted syntax discussion Bikeshedding about syntax, not semantics.
Projects
None yet
Development

No branches or pull requests