-
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
Is there too much syntax magic with identifiers and matchers? #210
Comments
Pattern matching is much less useful if you can't create bindings from the matched parts. The existence of an irrefutable match - an identifier - is something that's needed for a number of use cases:
|
Pattern matching is still completely possible without binding values to variables: match (something) {
when ([1, 2,]) { something[2] }
when ({ a: { b: { c: { d } } } }) { something.a.b.c.d } // You already noted this was possible, it's just not pretty.
}
const array = [...iterable]
match (array) {
when ([,]) { array[0], array[1] }
} The purpose of those examples are simply to illustrate that pattern matching stands alone just fine without binding syntax. Certainly, pattern matching without binding makes a number of tasks more inconvenient, and certainly, I would never advocate that we proceed with this proposal without some sort of binding syntax. But, just because binding is super convenient, does not automatically mean it should have equal footing compared to the rest of the proposal when it comes to the proposal's syntax. I can think of a number of other convenient things we can do in the middle of the pattern matching syntax that could improve the usefulness of this proposal. However, I think the README's clause fits really well here - binding is a useful aspect to pattern matching, but isn't central to it, therefore we're justified in giving it a little syntax weight if it improves the clarity of the entire proposal. So, the question is, does the suggested syntax weight improve clarity? If not, are there alternative syntax ideas that could be put in place, that mitigate the original issue described? |
For objects, yes, but not if iow: function* gen() {
yield 1;
yield 2;
}
match (gen()) {
when ([a, b]) { /* impossible to get 1 and 2 here unless `a` and `b` create bindings */ }
} |
That was solved by this example: const array = [...iterable]
match (array) {
when ([,]) { array[0], array[1] }
} the README states that pattern matching consumes the entire iterable anyways, so this example should do the trick. |
It's one of the top priorities of this proposal to match destructuring, so it's non-negotiable that it match non-array iterables with the |
That's fine. And I haven't broken that concept either (edit: At least in the context of what we're currently talking about). You can still pattern match against non-array iterables without binding to values. It just means you can't use the values from the iterator in the RHS, unless you've turned it into an array first. But the point still stands - pattern matching does not absolutely require bindings to exist - bindings are just a very useful feature to mix in, and the primary purpose of binding is, well, to bind, not to pattern match. I feel like every time we've talked about that clause in the README, it tends to get interpreted as "whatever is inside this proposal is 'pattern matching', and any foreign suggestions is not", making this proposal pretty inflexible to further adjustments :(. I mean, what if the initial idea for this proposal started with another syntax option, where you could inline arbitrary checks into the pattern matching syntax like this: So, I still don't think that README clause has any power to auto-veto the idea of adding a small syntactic burden to bindings. If you personally don't like the proposed syntax, or if you don't want to add a syntactic burden to bindings, that's a different story that can be discussed - I expected that people might not like the proposed syntax. But, we can at least give the idea a fighting chance, and not just kick it out for being different from the current proposal. |
The issue is weighing different sorts of confusion against each other. In particular, the current syntax biases toward acting like destructuring when it matches destructuring's syntax: Switching to an The fact that some literals play double-duty as patterns, and thus behave identically whether they're pinned or not, definitely is slightly inconsistent. But it's a harmless sort of inconsistency that we don't believe will cause confusion.
It's not; that example code only "solves" things if the iterator is the top-level pattern. If it's nested, you can't reasonable adapt it. |
Fair point. I still don't think it makes binding a central feature of pattern matching that we're not allowed to add extra syntax weight to though.
A syntax error. Either do
This is probably my primary concern with the solution I proposed and is the reason why I didn't expect a lot of love for it (the fact that there's deviations between the syntax for destructuring and pattern matching) :p. There may be other solutions to the problem I described, but that one was the best I could think of, which is why I was hoping to open this issue up as a way to explore the problem with different possible solutions and see if the trade-offs are actually worth it. It could be that there's no good solution to this problem. But, just so we can open up our perspective a bit, here's another possible solution that actually preserves the relationship with object destructuring - this solution also doesn't create an extra syntax baggage for bindings like my original one. When matching an object, we can use a matches operator (like when({
a, // a must exist and gets bound to a.
b: B, // b must exist and gets bound to B
c -> 2, // c must exist, gets bound to c (maybe?), and is pattern matched against 2
d: D -> ^(2 + 2), // d must exist, gets bound to D, and is pattern matched against `2 + 2`
}) I really like how that looks, but the issue is that there's not an easy way to translate this idea to arrays. The obvious solution would be something like But, there's more than one way to solve the issue. I just don't know yet if there's any good ways to do so. |
That still breaks destructuring patterns, or if it doesn't it's inconsistent: when( {a: {b}} ) ... (If I'm understanding your suggestion correctly, you'd have to write this as If we want to keep things consistent with destructuring, it means we must keep ident matchers and array/object matchers in the same syntax space. If we abandon that, we need a pretty good reason to do so, since we'll be breaking with very reasonable author intuition. |
No, sorry, I wasn't very clear. You would still do Inside when() you destructure an object. If you're unable to destructure the object (e.g. a particular, expected attribute was missing), then the pattern will fail. While destructuring, we've enabled an extra syntax option, I know that explanation isn't complete, but it gives a good reason for why Update: So, I shouldn't have said that |
Destructuring creates a binding already - what further explanation is needed when a destructuring pattern does the same? |
Oh, huh, so you're still allowing ident-matchers and object/array matchers to live together, you're just making primitive matchers and pinned/custom matchers require a I'm not sure what we particularly get out of this, then. Pin syntax already disambiguates itself from the other patterns, and, save for the theoretical issue of a variable named "undefined", all the literals are also unambiguous. Your original complaint was about ident matchers having the unique "assign to this name" semantic while the other matchers didn't, so I don't see this addressing that problem at all. In that case, can you restate what problem you're currently having with the proposed syntax? Edited to add: Since primitive matchers are identical whether pinned or not, effectively this is just saying "remove primitive matchers and just let authors use pinning for this", which isn't an unreasonable position (I mentioned it in my first post in this thread), but it doesn't solve your initial stated issue. |
Pattern matching currently behaves almost the same as destructuring, but deviates in a number of ways that creates a lot of cognitive overhead for this proposal. My first proposed idea (requiring So while I do think the original issue I brought up in this thread is important (and I'll touch on it more later on), maybe the broader problem I've been trying to untangle is the fact that there's a disconnect between pattern matching and destructuring. So, let me start over. The problem is that there's a disconnect between pattern matching and destructuring. I've been digging around through previous discussions in the github issues, and have found a couple of old issues where people have previously shared some of the same issues I've been trying to rectify. I'll start out by enumerating them:
Solutions I'm going to go ahead and present a complete solution using the For issue const {| a, b |} = myObj
const [| a, b |] = myArray In the above example, if myObj did not have an const {| a, b, ...rest |} = myObj
const [| a, b, ...rest |] = myArray With this in place, we can add some consistency to the pattern matching proposal. match (data) {
when({
x, // Just like with destructuring, x does not have to exist on data for this to match
y -> 2, // y has to equal 2
// (I'm using the same arrow syntax here that I've explained previously
// if I wasn't, then this would be `y: 2` instead).
z -> undefined, // z could either be explicitly set to undefined, or it might not exist,
// in which case, it'll be set to undefined by default, just like with destructuring.
}) { ... }
when({| // Notice our use of {| ... |} - we're now destructuring a "sealed bag"
x, // x must exist
y -> 2, // y has to equal 2
z -> undefined, // z must exist, and must equal undefined
...etc // The rest param must be used if we want to allow this object to contain other properties
|}) { ... }
} You can imagine a similar principle for array destructuring. You'll notice that the only pattern-like feature that destructuring brings is the fact that if it normally would have thrown an error, it fails the pattern instead (that's a very easy to remember rule-set!). This sort of removes the concept of an irrefutable match. It can be emulated with Edit: I'll also note that this cleanly fixes a previous concern brought up here, which is the fact that in the current proposal, there isn't a way to match against an empty object. Well, now there is, use Now to address issue Compare these two scenarios: when ([('N' or 'S') as dir]) { ... }
const [dir] = ... The inconsistency here is in the fact that when destructuring, you can bind to a variable by simply putting the variable name there, while in the pattern matching version, if you're wanting to use a pattern in that slot, then you've shadowed your ability to bind that value to a variable. So, what's the fix? We've introduced an In other words, in an effort to prioritize pattern matching syntax, we've added a new operator to do something that should already be possible if pattern matching was really just extra syntax on top of destructuring. This is also the crux of the original issue I did a poor job of explaining. My first post mentioned that I didn't like how in the example Notice that the Some when ([dir -> 'N' or 'S']) { ... }
when ([-> 'N' or 'S']) { ... }
when ([dir]) { ... }
when ({ dir -> 'N' or 'S' }) { ... }
when ({ dir: DIR -> 'N' or 'S' }) { ... }
when ({ dir }) { ... } Phew - it took me a couple of days to work through my thoughts, research what others have said, and figure this all out. But I think this sums up the real problem I'm hoping to solve. I know we've talked previously about how we don't want to add extra syntactic baggage to leaf patterns, so syntax like |
Now to specifically address your concerns @tabatkins
Try looking at it this way. When pattern matching, we may do some object/array destructuring, which if it fails, the whole pattern fails (we're not imbuing array/object destructuring with other pattern matching abilities like we previously were doing). As previously explained, irrefutable patterns are more of an emergent principle of pattern matching now - when you match a zero-length array against
This was not my intention. I've already tried to advocate that here and failed :p. My thoughts were that after the
We get rid of "as", the pin operator, and the list of special cased literals - that's a lot of simplification. I see all of those things as hacks that we've layered on, because we're trying to push expression patterns into the same place where identifiers go, and they just don't fit there, so we've created all of this extra stuff to make it possible to disambiguate things. Pros of the complete proposal presented above
Addressing potential arguments against this idea
|
@theScottyJam ok this is suuuuper long to respond to, so hopefully i'm not missing anything :-)
This is true, and also unavoidable, since they're for different purposes. The goal is to extend destructuring in a way that's intuitive for pattern matching. They need not be identical, and couldn't be. Points 1 and 2 are valid, but I think they're perfectly sensible and expected here.
I don't think this is fishy at all - anything that can be a variable will be bound as that variable, much like if
|
Yeah, it did get really long :p. But you addressed the main points. I'm curious @tabatkins if you have any opinions on this matter too? |
(lots of individual responses to particular points, and a tl;dr at the end)
You use
Afaict, "if it had an additional c property" is not at all what the sealing-syntax proposal is doing; it would be impossible to reasonably do for the same reason that our object-matcher syntax doesn't have an exclusive version, either: destructuring, like pattern-matching, will walk up the prototype chain, so the notion of "an extra property" will virtually always be satisfied. This also means this doesn't help match against an "empty object" (because there's no reasonable notion of such that's compatible with how destructuring or pattern-matching need to otherwise work).
Note that we never claimed it was just extra syntax on top of destructuring. Our claim was and is that pattern-matching is compatible with destructuring as much as possible, such that destructuring patterns should be valid pattern-matchers with nearly identical behavior (the only divergence being that we actually do check the value matches the specified structure, rather than just allowing
Okay, so afaict this is just inverting the order of patterns vs bindings. Rather than Your example doesn't show further nesting of array/object matchers, but given your stated motivations, I assume those would not require the If so, this then, eh, this doesn't really buy you any more consistency, and might actually reduce it.
Both of these seem not great to me! In the first, we expect literal-matchers to be very common, particularly with strings or numbers, and so the additional In the second, we have two completely different ways to bind intermediate values to names, depending on what kind of pattern we're descending into (and one isn't even defined yet, and relies either on the TC passing a new proposal, which is unbounded in time, or on the TC being comfortable binding its future self to use the same syntax we decide on here if they later add intermediate-value bindings to destructuring). The overall result is that this proposal stays slightly closer to destructuring syntax in some situations where we're extending past what destructuring can do (like
Notably, the thing on the RHS of the
We're not imbuing identifiers with special powers. It fails in our proposal because the array matcher has special powers slightly exceeding that of destructuring, where it checks the value's length against its pattern length. I think this would actually be a very bad switch to make for several other reasons, tho:
We don't get rid of We don't get rid of the pin operator either, at least not entirely - we need something to let us switch into "expression mode" so we can resolve an arbitrary expression into a custom matcher value. (And it needs to be syntactically distinct from the other matchers.) The "list of special-cased literals" is just "all literal primitive values"; this is spec-complexity only (and then, only barely), not author-facing complexity. Removing it won't make the proposal any simpler for authors, at least not a priori.
This doesn't, it just shifts them around. In all cases where the pattern would be a valid destructuring, your proposal and the current proposal are identical in syntax and behavior. This only changes syntax in places that are already not valid destructuring. (Unless you're referring to things like length-checking, where your proposal relies on seal-literal syntax in destructuring. In which case, see next point.)
As I argued above, I actually think this ends up being a very bad aspect of this proposal. I'd fight very strongly against this bit.
As I argued above, we haven't; the seal-literal syntax doesn't (and can't) work like that. In conclusion, I find that I'm ambivalent to weakly opposed on the part of your proposal that introduces |
Wow @tabatkins - what I thought was an idea with some potential now looks like swiss cheese - you blew all sorts of gaping holes into it.
Oops, you're right, that's not what that proposal is doing 😳️. And I didn't think about this prototype tying - I doubt there's a reasonable way to get around that without making more inconsistencies.
Maybe you didn't claim that (and that's not the current goal), but trying to get as close as reasonably possible to having "pattern matching be extra syntax on top of destructuring" would make this proposal easier to bite off and chew. So, the three issues I'm pointing out are mostly about trying to bring it closer to the "extra syntax on top of destructuring" idea. Each divergence, like "check the value matches the specified structure, rather than just allowing undefined to get thrown around" makes pattern matching much harder to pick up and learn, and leads to sticky questions, like how to deal with default assignment - there are valid consistency related arguments for both sides of that issue, we can either be consistent with how we've diverged (and have non-existent properties fail the pattern match), or be consistent with destructuring (and have non-existent properties receive a default value). The answer to this will become another thing that developers will have to learn related to pattern matching. We create a source of bugs and "another thing to remember" any time we create some syntax that could reasonably have two different meanings, like, default assignment within pattern matching. This proposal seems to have a lot of those scenarios - but as I'm finding out, it's really, really difficult to get rid of them.
technically true, but there's a conceptual difference, which is:
OK, maybe that's a weak argument - but it felt more right to me 🤷♂️️
You are correct
Umm, alright, this absolutely destroys the
I'm mostly suggesting that some features may work better if shared between both pattern matching and destructuring. I wouldn't be surprised if that proposal doesn't get very far (it's looking like records/tuples may devalue it's objectives), and wouldn't want to pin the success of this proposal on a side feature of that one, but we could still find certain principles that both pattern matching and destructuring can benefit from. It could even be done in a way where we first add them to pattern matching, then later add them to destructuring.
Yeah ... so while it solves some of these issues I brought up, it does make default behavior not as nice, and maybe even turns defaults into footguns :(. I guess one solution is to make it so you're required to use
Not quite, template literals are an exception and yet another thing for code authors to remember. @ljharb mentioned multiple valid ways to work with them in this issue - multiple options seem valid, and whichever one we choose may not be the one that a particular developer expected. Alright, I think you've thoroughly destroyed my The object destructuring + prototype argument destroys any hope of resolving I think issue I know I'm not the only one who's worried about this, as this other issue brings up the same issue, along with a different way to solve it - you're required to use a "!" any time you want to make a binding into an irrefutable match. I've pulled this issue into so many directions, that I'm thinking about just opening a new issue to discuss potential solutions to this one further, like this "!" idea. |
@theScottyJam I'd strongly encourage you to indeed open some new issues for specific changes you'd like to discuss. I personally found this thread rather difficult to engage with due to the number of entangled moving parts (though Tab hit the points I'd have raised). I think you might benefit from distilling the things you want to propose into individual pieces that stand on their own - it'll make for more fruitful discussion. Thank you for taking so much time to consider the future of this proposal! 💜 |
Yeah ... sorry about that, I really should have split that up into multiple issues. Here's the new issue to further discuss the remaining point I had. Thanks, @ljharb and @tabatkins for your time and thoughtful comments, I appreciate it :) |
Consider the following pattern match
What comes after the
:
has one of two very different meanings, depending on the type of expression found there:We've gotten used to this behavior from other languages that implement a pattern matching syntax, but I argue that it's not a healthy one. In fact, there's a lot of magic to it that steepens the learning curve to pattern matching, and thus to the overall language. It would be as if I made a proposal to introduce assertions using the following syntax:
'abc' = myVariable
- that's absurd! We're overloading assignment to mean two different things depending on what's found on the LHS, yet we've convinced ourselves that it's ok to do that with pattern matching, because there's prior art. (maybe the analogy is a bit of a stretch, but the point still stands)There's different possible solutions to this issue - the one I'll present here tries to follow the following advice from the README.
Assigning to a variable in the middle of a pattern-matching expression is not pattern matching, it's just a convenient thing to do while you've got the object's structured opened up. So, lets add a little syntax weight to it, to help disambiguate intentions - all assignments now require the usage of the
as
keyword.Our original example would then become:
Whatever comes after the
:
will now always be a pattern that you match against. You're not allowed to put an identifier there anymore.Here's a similar example for arrays
In a similar spirit to objects, picking out values must be done using the
as
keyword, to help visually distinguish assignment from matchers.I expect there will be some push-back on this idea, which is fine, let's discuss the possibilities :). I do think having literals and identifiers in the same place with two different behaviors is odd semantics for a language feature. Maybe you don't agree with the solution, but do you at least agree with the presented issue? This proposed "required to use as" is just one possible fix. I've entertained other ideas too, some of which looked nicer, but I couldn't figure out a consistent way to implement those within both the object and array destructuring syntax, which is why I didn't present them, but maybe others could come up with syntax ideas to better disambiguate these two pieces of functionality.
The text was updated successfully, but these errors were encountered: