In the May 2018 TC39 meeting, @zkat presented the possibility of having pattern matching on NewExpressions. I think this is a great idea, but I didn't see anything in this repository for it. I wanted to suggest some concrete syntax and semantics
Syntax of new things that can be put as the thing which comes after when:
CallExpression[?Yield, ?Await] Arguments[?Yield, ?Await]
new MemberExpression[?Yield, ?Await] Arguments[?Yield, ?Await]
new MemberExpression[?Yield, ?Await]
When matching against any of the three constructs, the expression (either CallExpression or MemberExpression) is evaluated, and a Symbol-named method is called, with the object being patched passed in as the argument. For the new constructs, let's call that method Symbol.invertConstruct; for calls, let's call this Symbol.invertCall. The method is expected to return either null (indicating a failed match) or an iterator. If it returns non-null, that value is treated as an iterator and is matched against the argument list of the construct/call, with the same algorithm as pattern matching on Array literals.
Let's do Lisp! Stack space and useful variable names be damned.
const map = (fn, list) => case (list) {
when cons(car, cdr) -> cons(fn(car), map(fn, cdr))
when nil() -> nil()
};
(Note, the above depends on the ability to use the above example depends on the ability to use case expressions, as proposed in #116.)
There's a million ways this could be implemented, but here's an especially poorly designed one:
function nil() { return null; }
nil[Symbol.invertCall] = arg => arg === null ? [] : null;
function cons(car, cdr) { return {car, cdr}; }
cons[Symbol.invertCall] = arg =>
typeof arg === 'object'
&& Object.getOwnPropertyNames(arg).toString() === "car,cdr" ?
[arg.car, arg.cdr] : null;
invert function.Shameless, irrelevant plug: see my related work in Factor.
Note, there would be a compatibility risk to supporting this construct within destructuring assignment, as that syntax has existing semantics (evaluate both sides and then throw IIRC). But I see no risk for destructuring bind.
As for matching constructors, why not support matching on the constructor member of a value?
function A(val) { this.val = val }
const a = new A(1)
case (a) {
when {constructor: A, val: 1} -> 'ok'
when {constructor: A} -> 'wrong value'
when {} -> 'wrong type'
}
@luftzig Maybe that would make sense with the pinning feature? The proposal above adds something further--datatype-specific pattern matching logic, not just a way to pattern match on property accesses.
I really like this, thank you! We should probably move this to a separate proposal, though, as I'd much rather this get integrated into destructuring at the same time as it lands in pattern matching. Making sure those are in sync is important to me.
As an alternative to @Luftzig's idea, you could just define Function.prototype[Symbol.invertConstruct] = function (v) { return v instanceof this ? [v] : undefined }. Or alternatively, you could define new X(...args) to fall back to that in the absence of X[Symbol.invertConstruct].
I would be okay if it were punted to a follow-up proposal.
@littledan How breaking would it be to change that to something else? Typically, changing errors to non-errors is a lot less breaking than changing non-errors to anything else, including other non-errors.
@zkat Yeah, I agree that this is a clean place to separate out. The place where it hooks into pattern matching is the protocol for indicating "no match" (so it'd be hard to add this separate proposal first and then pattern matching second unless we decide on that).
Symbol.invertCall reminds me Scala's unapply.
With combination of TypeScript's type guards the typing of [Symbol.invertCall] might be something like this:
(x: TS) => [A, B, C, ...] & (x is T & TE) | null
Maybe we can use this to deconstruct on Map and Set
case (x) {
when Set([1, 2, 3]) -> expr
}
(ref: https://github.com/tc39/proposal-pattern-matching/issues/148 )
I don鈥檛 see how; there鈥檚 no robust cross-realm way to identify instances, and any first-class instanceof support is something I鈥檇 block.
I don鈥檛 see how
Here's a quick summary of Scala's unapply.
(Briefly, though, it would be via a presumably symbol-keyed method on Set/ Map, not by special-casing those types.)
Or maybe I don't understand your objection?
Edit: though I guess the cross-realm thing might still be an issue? But I don't think that's an issue which would come up much in practice.
it would be via a presumably symbol-keyed method on Set.prototype / Map.prototype
Note that Scala's extractor methods are defined on the companion objects of a type, meaning they'd be equivalent to static methods rather than instance methods (i.e. defined on the Set / Map constructors themselves).
I _think_ Scala's extractor methods could work for this usecase without cross-realm issues.
Something like
case (x) {
when Set(1, 2, 3) -> { some statements... }
}
could na茂vely desugar to something like
const [a, b, c] = Set[someMagicUnapplySymbol](x);
if (a === 1 && b === 2 && c === 3) {
some statements...
}
The proposal has been updated in #174. The current design has no facility for matching against NewExpression or CallExpression.
If, taking into account the updated proposal, someone still feels there's a plausible way it could, or should do so, please open a new issue.
Maybe it still the case. The old syntax seems like enables the custom pattern (x(y, z)) to know the "pattern"(y, z) but the new syntax ^Sth cannot.
Indeed, patterns can鈥檛 look like function calls right now, and tbh I鈥檓 not sure how that really even makes sense. Now that the proposal much more explicitly separates patterns, expressions, and bindings, it seems clearer to me that this idea doesn鈥檛 make much sense, but I鈥檓 happy to hear arguments in a new issue :-)
Most helpful comment
I really like this, thank you! We should probably move this to a separate proposal, though, as I'd much rather this get integrated into destructuring at the same time as it lands in pattern matching. Making sure those are in sync is important to me.