-
Notifications
You must be signed in to change notification settings - Fork 90
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
Comments
What about match(matchable) {
when = E { ... } Perhaps the second one implies coercion but the behavior (if I understand) is "when |
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 |
Basically correct - a way to distinguish between "patterns" - which include irrefutable matches (like 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: match(matchable) {
when matches E { ... }
} Con off the top: Really excited by the revamped proposal! |
I know it's not an option since it's already in use, but I feel like |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
What about
|
That... is not bad. Slightly heavier-weight for single variables ( |
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:
What if those were the only things you could stick on the right of |
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 |
I agree, "always use |
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 |
@ljharb - could you expound? Are you suggesting that |
@theScottyJam no, i'm suggesting that |
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 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 |
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 So, here's everything that needs to happen for this fix to provide any value:
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 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 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 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 |
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, 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:
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". |
|
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
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 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 |
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 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 That's a good point with the 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.
cons:
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 could do that with the current proposal by just removing the literal patterns and requiring you to use the pin operator.
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 |
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 😜
Fair point
Ah whoops, I felt like I was missing something 🤦, alright, so an unpin operator would be required
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 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 ( |
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 expression 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 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 |
The champions group met today and reached consensus to switch to |
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: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)The text was updated successfully, but these errors were encountered: