tc39 / proposal-pattern-matching

Pattern matching syntax for ECMAScript
https://tc39.es/proposal-pattern-matching/
MIT License
5.45k stars 89 forks source link

We should reframe the approach here #281

Closed codehag closed 6 months ago

codehag commented 2 years ago

Edited Note: I've injured my wrist quite badly. Some context is missing here, it will be filled in later (in a week or so hopefully).

As mentioned on matrix -- and in past meetings of the champion group, i believe this proposal is too complex in its current form, and as a champion i have raised this and the epics concept as a way to alleviate this. When this appeared on the agenda for stage 2, i was not informed. I have rushed to put this together, it is only an idea, or a write up of one, of how this could be done: https://github.com/codehag/pattern-matching-epic

and here, is a suplimentary document. https://docs.google.com/document/d/1dVaSGokKneIT3eDM41Uk67SyWtuLlTWcaJvOxsBX2i0/edit

There are many forms of simplification and layering that are possible here. this is not the only one. Perhaps this is too fine grained. As i wasn't informed of this moving to stage 2 (likely because i was ill at the time), its clear that my contribution has been small so i've also stepped down as a champion.

sorry i can't say more now as its quite hard to type.

CadenP commented 1 year ago

for(...) if(...) {...} is currently usable syntax, capable of skipping items that don't pass the condition in the if without breaking the loop. while(<condition>) breaks the loop, and for(...; <condition>; ...) breaks the loop as well.

Is for(let x of items if <test>) supposed to break the loop or skip the item?

tabatkins commented 1 year ago

I described the behavior in https://github.com/tc39/proposal-pattern-matching/issues/281#issuecomment-1486016245 (and alluded to it https://github.com/tc39/proposal-pattern-matching/issues/281#issuecomment-1490976620 with talk of Python's list comprehensions). It would skip the item.

(I didn't grok that for(...) if(...) {...} was already valid syntax, but you're right, it is. Phew, braceless blocks are really a trip sometimes.)

tabatkins commented 1 year ago

Just carrying some conversation over from the chat room, because Ron needs to be on vacation and stop responding but this is good to store for later response:

(from @rbuckton )

The let { x, y } = example is verbose, yes, but uses what could otherwise be regular js in the initializer. In a way it's meant to illustrate that a let statement isn't a good fit for pattern matching itself, because matching is conditional and let is not. The closest you get to a useful example is the assert one, because you need to validate the condition was successful.

This is simply wrong - let is not unconditional. (Or at least, let with destructuring isn't.) let {foo} = undefined; throws a runtime error! let {foo} = {} executes successfully (binding undefined), but let {foo: {bar}} = {}; again throws. The destructuring behavior matches what you'd get if you instead wrote a series of assignments, each digging into the value with an appropriate dotted/bracketed path, and that can def fail.

Matchers can just fail a little earlier - let when {foo} = {}; would throw because the match failed. But I don't think that's a difference that makes a difference here.

tabatkins commented 1 year ago

Since the immediate reaction to my suggested syntax changes a few days ago was dislike of the increased verbosity, here's a new draft.

Quick summary:

The function arg syntax is the one I'm still most unsure about, it could probably still use some fiddling for maximum readability. Possibly the is operator could be fiddled with more, too - not married to the name, and the argument order is opposite how matchers are used elsewhere.

The rest, tho, are I think minimal, clear, and importantly, very consistent and predictable.

tabatkins commented 1 year ago

Been fiddling with my draft proposal for "matchers everywhere" more, based on feedback from several people.

Major changes from above:

ljharb commented 1 year ago

Why would custom matchers be required to return their values as an iterator? That sounds very confusing.

tabatkins commented 1 year ago

Foo(a, b) matches the result of invoking Foo[Symbol.matcher] against [a, b]. If we match up interpolated matchers as ${Foo}(a, b), as I suggest above, then that's also matching the result against [a, b].

The return value can be an array or something; it just needs to be iterable.

(A benefit to doing this, besides the confluence in syntax, is that we no longer need the special "result object". If you return an iterable, that's a successful match; we can also let you return true/false to indicate a successful or failed match. Any other value would be a runtime error.)

ljharb commented 1 year ago

Maybe I'm confused. Why would Foo() matching syntax ever invoke the iterator protocol? (separate from user code doing so, ofc)

tabatkins commented 1 year ago

That's intrinsic to the syntax? It's always been the case, both in the Extractors proposal, explicitly, and in all of my adaptations of extractors into matchers. (Foo(a, b) has always been equivalent to ${Foo} with [a,b].) I'm not sure how you think when Foo(a, b) would work, otherwise.

ljharb commented 1 year ago

I hadn't realized that implication, and to me that's a very very strong argument against considering that to be the desugaring. Forcing the iterator protocol when it's not absolutely necessary seems like a very unwise idea.

tabatkins commented 1 year ago

I have no idea what else the desugaring could possibly be. And this exact desugaring is used by other langs already, like Python's class matchers.

ljharb commented 1 year ago

I would expect it to only ever accept one argument, and have that be the pattern tested - so that if you wanted array iterator syntax, you'd type that.

tabatkins commented 1 year ago

That loses the very nice syntax mirroring of function-call/construction <=> matcher pattern, which we have with array and object literals. Again, this sort of matcher syntax is accepted by many existing langs in their matcher patterns, for this reason - that syntax mirroring is pretty attractive.

For example, given a Point object with a constructor like new Point(x, y), giving it a custom matcher that returns the x and y values, and is usable like when Point(x, y): Math.hypot(x, y); is pretty sweet.

Forcing that to be when Point([x, y]) is a lot less attractive, and brings up reasonable questions like "well what're the parens for, then?".

Jack-Works commented 1 year ago

Maybe I'm confused. Why would Foo() matching syntax ever invoke the iterator protocol? (separate from user code doing so, ofc)

For code

let Foo(a, b) = expr

Foo[@@unapply](expr) will return an array [aVal, bVal], then destructing is happening (a, b) therefore @@iterator is called