Skip to content
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

Pattern matching NewExpression/CallExpression #117

Closed
littledan opened this issue Jun 21, 2018 · 15 comments
Closed

Pattern matching NewExpression/CallExpression #117

littledan opened this issue Jun 21, 2018 · 15 comments

Comments

@littledan
Copy link
Member

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 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.

Example

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;

Possible future extensions

  • The above proposal doesn't really handle constructor functions which are overloaded between different argument types. Sometimes, what's textually present as the arguments could give a hint here. In a follow-on proposal, the hint could be passed as the second argument to the invert function.
  • Although this proposal is somewhat similar to variable pinning and collection literals in use cases, it still composes well with them (they don't really interact).

Shameless, irrelevant plug: see my related work in Factor.

@littledan
Copy link
Member Author

littledan commented Jun 21, 2018

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.

@Luftzig
Copy link

Luftzig commented Jun 21, 2018

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'
}

@littledan
Copy link
Member Author

littledan commented Jun 23, 2018

@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.

@zkat
Copy link
Collaborator

zkat commented Mar 27, 2019

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.

@dead-claudia
Copy link

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.

@dead-claudia
Copy link

@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.

@littledan
Copy link
Member Author

@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).

@be5invis
Copy link

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

@Jack-Works
Copy link
Member

Maybe we can use this to deconstruct on Map and Set

case (x) {
    when Set([1, 2, 3]) -> expr
}

(ref: #148 )

@ljharb
Copy link
Member

ljharb commented Apr 14, 2020

I don’t see how; there’s no robust cross-realm way to identify instances, and any first-class instanceof support is something I’d block.

@bakkot
Copy link

bakkot commented Apr 14, 2020

I don’t 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.

@treybrisbane
Copy link

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...
}

@ljharb
Copy link
Member

ljharb commented Apr 21, 2021

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.

@ljharb ljharb closed this as completed Apr 21, 2021
@Jack-Works
Copy link
Member

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.

@ljharb
Copy link
Member

ljharb commented Apr 21, 2021

Indeed, patterns can’t look like function calls right now, and tbh I’m 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’t make much sense, but I’m happy to hear arguments in a new issue :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants