tc39 / proposal-pattern-matching

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

Decoupling of "pattern matching" and "subsume switch" features #241

Closed PaulKiddle closed 2 years ago

PaulKiddle commented 2 years ago

Hi everyone, thanks for the work you're doing on this proposal.

I'd like to explore the idea of having pattern matching as a distinct feature from "better switch", which may be combined together to fulfil the use cases of this proposal but could also individually fit use cases of their own. Doing this could also vastly reduce the complexity of the proposal.

I think a pattern matching expression could be very useful in statements when there are only one or two branch cases:

const isFormController = object matches { view: String, submit: Function }

In addition, by having this as a standalone operator that can be combined with a new case-matching statement in a user-defined way, this proposal woudn't then need to explicitly define how to do things like match regular expressions or items in arrays, since the language already provides ways to do this:

// Use Array.prototype.includes to check for "is in array"
// borrowing the ? syntax from the partial-function-application proposal
match(?.includes(file.extension)){
  when(['htm', 'html']): handleHtml(file)
  when(['jpg', 'gif', 'png']): handleImage(file)
}

// Use a new pattern matching statement to check for a match
match(object matches ?) {
  when({ view: String, submit: Function }): new FormController(object)
  when({ view: String }): new ViewController(object)
  default: new ErrorController(object)
}

Just to clarify, I'm not saying this is should be the final syntax, I just thing it's worth exploring separating these two parts of the proposal.

ljharb commented 2 years ago

It very much isn’t; if matching won’t subsume switch, it’s not worth adding it to the language with syntax at all imo.

What would be the advantage of this more limited form you’re suggesting? Just using “includes” on arrays, for example, would fail the criteria that this be pattern matching; and it wouldn’t allow for any nested matching, since .includes just uses SameValueZero.

theScottyJam commented 2 years ago

@ljharb - perhaps it's a tad bit more verbose, but I don't think it's more limiting. Using his match construct and matches operator together, you've pretty much got the same pattern matching that the proposal has today (the only difference is you have to add a matches ? to it), but having them separated lets you use both of these constructs in new and different ways.

ljharb commented 2 years ago

How would you match an array of arrays of something?

theScottyJam commented 2 years ago
match (nestedArray matches ?) {
  when ([ [x, y] ]): 'yes'
  default: 'no'
}

The RHS of his proposed matches would still be able to follow the same syntax we use today.

Whether it's worth actually doing this split, I don't know, but from what I understand it should still be just as capable as the current proposal.

ljharb commented 2 years ago

And what if i had an array where item 0 is a number and item 1 is a string?

theScottyJam commented 2 years ago

It's possible I'm misunderstanding the original idea, but here's what I see.

The way I read it, the RHS of matches would use the same matching syntax we use today. But, technically, that would mean the original example should look like this:

const isFormController = object matches { view: ${String}, submit: ${Function} }

(He didn't mention that he intentionally wanted to change the pattern-matching syntax itself, which is why I assume this was just a mistake? @PaulKiddle - could you confirm or deny this? If I'm wrong here, then all of @ljharb's concerns would be valid)

And, I assume that within the match construct, the ? could be used where an expression or pattern-match syntax goes, and then every when would follow suit. If you placed the ? when an expression was expected, each when would need an expression inside of it. And if you placed a ? when a pattern-matching syntax would go, each when would need pattern-matching syntax within it.

So, if these assumptions are correct (maybe they're not), then I would just do this:

match (thing matches ?) {
  when ([${Number}, ${String}]): ...
}
ljharb commented 2 years ago

And in that case, what would be the advantage to this approach?

theScottyJam commented 2 years ago

Well, I know a number of people would like to have a matches construct by itself, so they can pattern-match in an if-statement, for example. We would get that for free. And, we would be able to "switch" against other operations besides just pattern-matching (like array.includes, as he showed) - I don't know how useful this really is or not, I could see some interesting ideas come out of it. If the ability to "switch" against different expressions (not just pattern matching) turns out to have a lot of good use cases, then it could be nice to split pattern-matching in half like this. But, these two extra powers need to be useful enough to outweigh the fact that we now have to do matches ? whenever we want to do a normal pattern-match, so for that to happen, we'll need to see a lot of good and useful things that can come from doing a "switch" against other operations besides pattern matching.

PaulKiddle commented 2 years ago

(He didn't mention that he intentionally wanted to change the pattern-matching syntax itself, which is why I assume this was just a mistake? @PaulKiddle - could you confirm or deny this? If I'm wrong here, then all of @ljharb's concerns would be valid)

Yes that was my mistake. As I say, the syntax is not what I'm concerned about.

The other motivation for this split is that I worry there may be unforseen ways of matching things that developers want to do that have fallen through the gaps. But by splitting these out it would be easier for devs to cover any shortfalls using their own userland functions.

I can think of a few examples of a "better-than-switch" statement would be useful that wouldn't require a new matching syntax (instanceof with a multiple-signature method, or regex testing against urls, for example), and frankly as a developer I would rather learn two new smaller language features at separate times when I have use for each one, than have to learn one larger one when not all of it is strictly needed for a given use case.

ljharb commented 2 years ago

@theScottyJam you can already use pattern matching in an if statement; if (match (x) { when (y) { true } default { false } }).

@PaulKiddle given that the current proposal has a protocol, i can't imagine what userland would be unable to cover.

PaulKiddle commented 2 years ago

@PaulKiddle given that the current proposal has a protocol, i can't imagine what userland would be unable to cover.

Ah I did miss that section in the docs. Tbh it looks a bit more verbose than just writing a method, but fair enough that it's covered. I think my other concerns still stand though.

ljharb commented 2 years ago

You've suggested syntax, which tempts folks to rathole on that - but conceptually, what about your suggestion is different than the existing proposal? What can or can not be done with your proposal versus the existing one?

PaulKiddle commented 2 years ago

I've explicitly said twice that I'm not suggesting any syntax changes, beyond what's required to split these two features

I believe the benefits are:

And as a result of these i believe it also fits closer to tc39's guiding principles than the current combined proposal

tabatkins commented 2 years ago

A point that @ljharb is making somewhat obliquely is that an important aspect of pattern matching is that the pattern is not ordinary JS syntax, nor can it be if we want it to properly address the needs we're aiming for. You can't just use array literal and object literal syntaxes, any more than you could do a destructuring API with normal userland JS syntax (it would require awkward contortions at best).

There are several reasons why this is so, but probably the most significant is that there's no way to create bindings - no equivalent to when([a, b]). The idea of pattern matching expressed here is a purely boolean test, and that is one important component of pattern matching but not all of it; virtually every pattern matching syntax in existence has a way to extract parts of the value being matched against to use in computing the result. (Note that the OP's match statement is just an if/else chain where every if() uses the same test but with one part made swappable.)

The two features (matching against a pattern, and extracting values from the matchable based on a pattern) can't be realistically decoupled; JS doesn't have the syntax chops for that (and even if it was a Lisp or something that could, doing so would require a pretty complex macro that we'd want to build in anyway).

If we assume that the matches keyword is sufficient to allow us to switch into the pattern syntax (it's very likely not, but go with it), then the only case that is made easier by this proposal is returning a bool; x matches ... is indeed shorter and simpler than writing match(x) { when(...): true; default: false; }. I suppose a simple trinary based on the single test and not using anything from the matchable is also slightly simpler; x matches ... ? foo : bar versus match(x) { when(...): foo; default: bar; }, but that's a much closer comparison.

All other cases are equal difficulty, harder/verboser, or impossible.

So, this sort of dramatic change to the proposal is not on the table.