tc39 / proposal-pattern-matching

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

More ergonomic custom matchers #268

Closed theScottyJam closed 2 years ago

theScottyJam commented 2 years ago

Currently, it's very cumbersome to create custom matchers. I worry that there's going to be tons of use-casese where people need to make one-off matchers, and they're going to have to go through all of the trouble of creating a custom matcher that's half as big as the pattern-match statement they're coding up. It would be nice if we provided a more concise way to make custom matchers, with an API that tailors to their most common use-case.

To handle this, I'd like to propose to additional helper functions (which, perhaps could be part of a follow-on proposal, but I feel it could really help make the base proposal more user-friendly). its defined as follows:

globalThis.matches = predicate => ({
  [Symbol.matcher]: value => ({
    matched: predicate(value),
    value: null
  })
});

Usage example:

const negZero = matches(x => Object.is(x, -0));
match (coord) {
  when ({ x: ${negZero}, y: ${negZero} }): ...
  when ({ x: ${negZero}, y: ${Number} }): ...
  when ({ x: ${Number}, y: ${negZero} }): ...
  when ({ x: ${Number}, y: ${Number} }): ...
}

const isNegatize = matches(x => x < 0);
match (coord) {
  when ({ x: ${isNegative}, y: ${isNegative} }): ...
  ... you get the idea ...
}

const isInstanceOf = theClass => matches(instance => instance instanceof theClass)
match (thing) {
  when (${isnstanceOf(Thing1)}): ...
  when (${isnstanceOf(Thing2)}): ...
}

It's true that a lot of the use cases for this matches() function could be handled in a guard statement afterwards, but that's often less readable, especially when dealing with larger patterns that use the custom matcher many times.

This isn't a major point, but I'll also point out that this technology provides the ability to have a short-circuiting guard statement, as requested in https://github.com/tc39/proposal-pattern-matching/issues/255. Instead of allowing the guard syntax to be moved earlier in the pattern, we just use this matches() function.

// Instead of the proposed syntax change from that ticket:
when (x if (x > 10) & AnotherMatcher): ...
// you can do this:
when (matches(x => x > 10) & AnotherMatcher): ...
Jack-Works commented 2 years ago

Maybe we can allow returning a true.

theScottyJam commented 2 years ago

Allow a Symbol.matcher function to optionally return a true instead of an object?

Yeah, I guess that's an option as well. Though, I would prefer the actual Symbol.matcher API to be kept simple, so non-pattern-matching code could grab the symbol values and easily use the function found on it if it so wants to, without having to understand different possible return values. But, that's just a minor preference.

ljharb commented 2 years ago

Given that this hasn't been a problem with custom iterators, I'm not sure it will be much of a problem here.

We could certainly special-case matchers that return a boolean, and normalize it to a match result object, but I'm not sure return false; is meaningfully simpler than return { matched: false }, and custom matchers will hopefully want to supply a value when matching true, so that they can provide bindings.

I think that the idea of providing a way to do an in-pattern guard with a predicate is an interesting follow-on, but I don't think that needs to be conflated with authoring custom matchers.

theScottyJam commented 2 years ago

I think that the idea of providing a way to do an in-pattern guard with a predicate is an interesting follow-on

Yeah, I guess this is pretty much the use-case I'm trying to solve here, I just implemented the concept via custom matchers.

Jack-Works commented 2 years ago

I think we should allow returning true/false as a shortcut of {matched: bool, value} and we should adopt them in the built-in matchers

ljharb commented 2 years ago

@Jack-Works we could certainly do that. altho return true as a shortcut for return { matched: true, value: undefined } might be an error - another possibility is that we could say that return true throws an error when used with with - iow, you'd only be allowed to provide bindings when an explicit value was returned.

tabatkins commented 2 years ago

Disagree - I think true should be treated identically to {matched:true, value:undefined}. It's not like you can meaningfully destructure such a case anyway, so using with is gonna be a problem in either case unless you do a bare with foo to just grab the value. And if you do have a bare with foo, it seems more useful to have that work (with foo bound to undefined) in both cases, rather than requiring custom matcher authors to either use only bools (no with allowed) or never bools (always with allowed) and requiring custom matcher users to know which way to invoke the matcher.

We generally shouldn't be distinguishing between "not present" and "present but undefined" in most cases anyway.

ljharb commented 2 years ago

Makes sense to me.

ljharb commented 2 years ago

See #278