tc39 / proposal-pattern-matching

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

Collision/confusion of syntax, bindings, and variable refs #11

Closed tabatkins closed 6 years ago

tabatkins commented 7 years ago

In the pattern syntax, raw syntax, binding names, and variable references are mixed in ways that are not immediately obvious, and which restrict what you can actually do in seemingly confusing and unfortunate ways:

I don't have good solutions for either of these, tho.

tabatkins commented 7 years ago

Actually, the first bullet point (can't bind sub-values of an array) would be solved by #10 - [x, foo@[...]] would bind x to the first element, and foo to the second element (while ensuring that foo is an array).

bterlson commented 7 years ago

Agreed, first bullet is a great motivator for #10.

I don't understand the second bullet, though. I don't understand how this is different from how you can't declare a variable named else. Can you elaborate?

tabatkins commented 7 years ago

Ignore the second one, I thought that you could name a variable else.

samth commented 7 years ago

This is my major concern as well. I would suggest that in addition to #10, we should have a special-case for the current meaning of identifier reference, and have a plain identifier at the top level be a binding.

7fe commented 7 years ago

@tabatkins why not support something like Mathematica's extensions like the following. instead of [x, foo@[...]].

do [x,[foo___]] the amount of underscores denote either 1,1+,0+ selection.

jneen commented 7 years ago

The use cases of identifier reference could be handled with a postfix boolean filtering expression as well, though only slightly clunkier:

var x = ...
match(expr) { x2 if compare(x, x2): ... }
rntz commented 7 years ago

One approach to disambiguating "identifier reference" from binding names would be to default to binding names (as @samth suggests), but to allow a form Identifier pattern to mean "test this against Identifier via Symbol.matches, and also match it against pattern". So for example:

match ([0, 10, 20])   { [Number x, Number y, Number z]: x + y + z } /* result is 30 */
match ([0, 10, "20"]) { [Number x, Number y, Number z]: x + y + z } /* fails to match */

There is a potential gotcha here with objects, however:

match ({x: 2}) {
  {x: Number x}: x
  /*         ^ This x is *not* optional; if omitted, the name `Number` will be
   * bound to `2` within the body of the clause.
   */
}

One could extend the syntax to make object matching more natural; simply allow putting an Identifier before a name in an object match:

match ({x: 2, y: "foo"}) {
  {Number x, Number y}: x + y /* this doesn't match, because `y` is a string */
  {Number x}: x               /* this matches */
}

Another possibly useful extension would be to allow the Symbol.matches method to return a meaningful result on successful match, rather than just "yes it matches". Identifier pattern would then match pattern against the result of the Symbol.matches method. For example, a regex might return a structure containing the results of matching the regex against the string, a la RegExp.exec:

let rex = /w(o+)ah/;
match ("that's like, woooooooah") {
   rex { index }: console.log("match at index " + index)
   /* prints "match at index 13" */
}

It would be neat to allow at least some expressions in the Identifier position, because then you could in-line your regexes:

match ("that's like, woooooooah") {
   /w(o+)ah/ { index }: console.log("match at index " + index)
   /* prints "match at index 13" */
}

Just some ideas. The "just put an identifier next to a pattern" syntax is very convenient, but might be confusing; maybe a more explicit syntax would be better.

ljharb commented 7 years ago

I'd really prefer a way to avoid imbuing an otherwise valid identifier with magical powers.

Over here I suggested:

const result = a => match (a) {
    { x: 0 }: someCode(),
    * as _: { // could be any identifier, _ used by convention
        throw new Error("this is the default");
    }
}

which would allow for a custom identifier; it would avoid imbuing special magic powers to _; it would avoid a bikeshed over the identifier name; it could be identifierless with *:; etc.

However, that wouldn't automatically work as well with multiple holes, but hopefully it's still worth exploring.

rntz commented 7 years ago

@ljharb If I understand correctly, you're suggesting the following: Instead of defaulting to binding names (so x would be a pattern that always matches, and binds x to the scrutinee), we default to testing against identifiers with Symbol.matches (as the original proposal says). Second, we add a form PATTERN as NAME that tests PATTERN and binds NAME to the scrutinee. Finally, we add a wildcard pattern * that always matches. Is that right?

* as a wildcard pattern seems fine; I don't care about the color of the else/_/* bikeshed. But I dislike this for other reasons. Namely, it either:

  1. Has the same problem pointed out at the top of the issue: "In x, x is a variable reference, forming an identifier pattern - the x variable in the containing scopes will be queried for a Symbol.matches property. In [x], x is a binding name - it matches a length-1 array and binds its first value to x.";

  2. Or else, if you let arbitrary patterns be nested inside of array patterns, it makes array patterns very awkward; you'd have to write [* as x, * as y, * as z] instead of just [x, y, z].

bterlson commented 7 years ago

@rntz those are some good ideas. This proposal does have meaningful Symbol.matches under ptional extensions. I think you're on to something with your proposal. Will ponder.

bterlson commented 7 years ago

I agree that Identifier in a pattern should be a binding identifier. It makes it possible to rename bindings within object patterns, and preserves the object literal congruency between { x } and { x: x }. I also like idiomatic _ (or any other identifier if you care about the value) instead of else (also I think addressing concerns from @ljharb above).

I'm not sure how to preserve the runtime pattern matching aspect of this proposal. I still find it to be important.

@rntz proposes a solution above using as, which I think is fairly nice and aligns with the similar oft-discussed extension to destructuring syntax. It also addresses #10 (with basically equivalent syntax). With this we have something like:

match (val) {
  Number as x: x,
  { x: Number as x, y: Number as y } as outer: x + y,
  [ Number as x, Number as y ]: x + y
  { x }: x
  [ y ]: y
}

There are some downsides. It's a strange that the LHS is the same pattern semantics except at top level where identifiers use the runtime pattern matching protocol.

Also, you must bind an identifier if you want to pattern match an object property's or array index's value. This is already true for the fields of an object and elements of an array so seems fine. I also suspect purely structural matches will be more common (and more efficient) but would love to hear from everyone about it.

Another possible solution is to take a cue from F# and say that lower case identifiers are bindings and upper case identifiers are runtime values. That let's us keep, for example: { ssn: Number, name: personName }. But may be hard to swallow in JavaScript :-P

Other proposals I've missed?

bterlson commented 7 years ago

Another solution is from @isiahmeadows's proposal: you can attach runtime pattern matching to a prefix operator is:

match (val) {
  x is Number: x,
  { x: x is Number, y: y is Number } as outer: x + y,
  [ x is Number, y: y is Number ]: x + y
  { x }: x
  [ y ]: y
}

as remains as an orthogonal extension to both destructuring and this syntax. This has the nice property that it can collapse x: x is Number to just x is Number and _ is Number into is Number when you don't care about the binding (thus making x: is Number a non-binding form of x is Number):

match (val) {
  { x is Number, y is Number }: x + y,
  [ x is Number, y is Number ]: x + y
}
rntz commented 7 years ago

@bterlson I did not propose any solution using as. I am not sure what you intend the semantics of as to be. Did you mean to tag ljharb rather than me? Or have I not explained my suggestion clearly?

It's a strange that the LHS is the same pattern semantics except at top level where identifiers use the runtime pattern matching protocol.

This is exactly what I think is bad. The top-level should never, ever be special.

Also, you must bind an identifier if you want to pattern match an object property's or array index's value.

I'm not sure what you mean by this. Could you give an example?

EDIT: The proposal you give by @isaiahmeadows is almost exactly what l was trying to propose, just with different syntax. They say x is Number, I say Number x. Their proposal has the benefit that is Number is a pattern of its own that binds no variables, whereas for mine you'd need to say Number _ or similar.

bterlson commented 7 years ago

Uhh yeah I guess I meant @ljharb sorry :-P (although re-reading I'm not sure that's what he meant to propose).

Example would be [Number as x] when you don't care about x but can't do [Number] because it's not a pattern check.

bterlson commented 7 years ago

@rntz I think your proposal above and @isiahmeadows's is are roughly the same except with ordering swapped and required keyword? I'm liking this approach.

ljharb commented 7 years ago

in general I'm concerned about using things like Number and terms like is, because without a brand checking mechanism that raises questions about cross-realms :-/

bterlson commented 7 years ago

@ljharb I agree, it's unlikely we can advance this proposal without also figuring out brand checking. Is could be a future extension.

rntz commented 7 years ago

@bterlson Yes, @isiahmeadows's is and my proposal are basically the same. is has the advantage that you can check without binding a name: match (2) { is Number: "blah" }. My suggestion needs a wildcard pattern: match (2) { Number _: "blah" }, which doesn't read as nicely.

I think my ordering works better if you're destructuring the object as well as checking it:

/* my version */   match (...) { Person { name, address }: ... }
/* `is` version */ match (...) { {name, address} is Person: ... } 

is doesn't seem to combine nicely with the extension where you match on the result of the Symbol.matches method:

/* my version */
match (...) { /yes/ m: m.index }
/* a hypothetically extended `is` operator; the name `is` no longer seems accurate */
match (...) { m is /yes/: m.index }
/* the original proposal */ 
let yes = /yes/
match (...) { yes -> m: m.index }
Jamesernator commented 7 years ago

I personally find the restriction of expressions to Identifiers just confusing and generally unhelpful. I'd expect any form of pattern matching that uses Symbol.matches to also support inline expressions in the matching clause, which is why I suggested making pattern matching expression : [destructuring_pattern -> ] expression in another issue, of course shorthand destructuring/binding is nice which is why I suggested a combined syntax as well.

I think the is syntax would be fine too for the goals of the syntax I came up with in the other thread, but I'd definitely want to see the RHS be an expression not a Identifier, it'd just be cumbersome to have to hoist every single pattern that happens to be an expression.

e.g.

class Point {
    static [Symbol.matches](point) {
        return point instanceof Point
    }

    static get origin() {
        return new Point(0, 0)
    }

    constructor(x, y) {
        this.x = x
        this.y = y
    }

    [Symbol.matches](point) {
        return point instanceof Point
            && point.x === this.x
            && point.y === this.y
    }
}

// Should this really be necessary
const origin = Point.origin
const atOrigin = match (point) {
    is origin: true,
    is Point: false,
}

// I'd rather see and write this
const atOrigin = match (point) {
    is Point.origin: true,
    is Point: false,
}

// Also for function calls e.g. this would just be tedious
// without inline expressions
function greaterThan(x) {
    return {
        [Symbol.matches](number) {
            return number > x
        }
    }
}

const size = match (value) {
    is greaterThan(1000000): 'Colossal',
    is greaterThan(1000): 'Massive',
    is greaterThan(100): 'Large',
    else: 'Small'
} 
dead-claudia commented 7 years ago

@rntz Actually, it would be is Person of {name, address} in my proposal. Just thought I'd clarify.

rattrayalex commented 7 years ago

[Syntax Bikeshedding]

Given that a consequent clause is currently limited to being an expression, the utility of renaming variables per branch is relatively low in this proposal. As such, I expect there to be a minority (albeit greater-than-zero) of cases where variable-renaming is used. In contrast, a pattern will be used in 100% of (non-else) cases.

As a result, the syntax Pattern [as name] seems to better support the majority use-case than [name] is Pattern.

Furthermore, is may imply === or instanceof to some readers, which is sometimes correct but misleading.

Using this syntax, the last part of @Jamesernator 's most recent example could be written as:

const size = match (value) {
    greaterThan(1000000): 'Colossal',
    greaterThan(1000): 'Massive',
    greaterThan(100): 'Large',
    else: 'Small'
} 

or, with renaming, written as

const size = match (value) {
    greaterThan(1000000) as colossalNum: `${colossalNum} is Colossal`,
    greaterThan(1000) as massiveNum: `${massiveNum} is Massive`,
    greaterThan(100) as largeNum: `${largeNum} is Large`,
    else as smallNum: `${smallNum} is small`
}

(which, of course, illustrates the disutility of renaming in many cases – ${value} is Colossal would have sufficed)

bterlson commented 7 years ago

After noodling on this for a while, I feel like my preferred path is something that cleanly separates the runtime pattern matching from destructuring and binding (this is not surprising, I've been here before :-D). This seems similar to some of the other proposals raised here as well.

In the following sketch, match legs are of the form RuntimePattern -> Pattern : Expression where RuntimePattern -> is optional.

match (val) {
  // structural matching scenarios, same as current proposal
  { x, y }: x + y,
  [ x, y ]: x + y

  // if you want type checking, add a RuntimePattern clause followed by ->
  { x: Number, y: Number } -> {x, y}: x + y,
  [Number, Number] -> [x, y]: x + y

  // gets annoying with deep patterns
  { tag: { name: String, id: Number }} -> { tag: { name, id } }: ...;,

  // enabling destructuring of the runtime match side enables regexp matching
  someRegExp -> { matches: { name, date } } : x + y,
  someRegExp -> [, name, date] : x + y

  // if regexp named capture groups were properties of the match object
  someRegExp -> { name, date } : x + y,
}

Cleanly separating the runtime pattern matching piece from the refutable destructuring piece lets us innovate on each without too much worry. I also like this approach because the LHS of the -> is just a normal object literal with normal expression evaluation semantics and the RHS is very close to destructuring. It should feel pretty natural to JS developers.

This proposal also has room to grow syntactic shortcuts to address the double-naming problem. E.g. if you only care about the runtime matching side you could omit the destructuring pattern and have legs like greaterThan(1000) ->: doAThing().

Many of the proposals here (especially partial to the as/is variants) are extremely capable and flexible but come at the cost of lots of complex syntax. I personally love them, but I worry that the syntactic complexity is untenable.

Any thoughts on the above strawman (positive or negative)? I think solving this issue is critical before moving on to addressing the others...

Jamesernator commented 7 years ago

I rewrote my pattern matching package here, it works assuming something similar to seperate pattern/destructuring which tries to capture the semantics of RuntimePattern -> Pattern : Expression e.g.:

import match, { number, string, is, obj } from "@jx/match"

// Match itself is just a checking function
match(number, 2) // true
match(is(NaN), NaN) // true
match(is({}), {}) // false
match({ x: number }, { x: 10 }) // true
match(
    array([string, string], number /* rest pattern */], 
    ['foo', 'bizz', 2, 4, 5, 8]
) // true
match(2, 2) // true, some patterns are auto wrapped e.g. primitives
            // get wrapped with is

It allows for defining any patterns you want using match itself like the proposal does e.g.:

function greaterThan(x) {
    return {
        [match](value) {
            return value > x
        }
    }
}

match(greaterThan(5), 8) // true

It supports the RuntimePattern -> Pattern : Expression semantics using match.on which also consider objects returned from [match](val) functions e.g.:


RegExp.prototype[match] = function(obj) {
    const result = this.match(obj)
    if (result) {
        // Objects can be returned instead from [match]()
        // which has a matches: boolean property
        // and the value which is used in match.on as the result
        // value for destructuring against
        return {
            matches: true,
            value: result
    } else {
        // Still accepts booleans if nothing interested happened
        return false
    }
}

const foo = match.on('01-01-1991')
    .if(/(\d\d)-(\d\d)-(\d\d\d\d)/g, ([_, day, month, year]) => 
        ({ day, month, year, type: "British" })
    )
    .if(/(\d\d\d\d)-(\d\d)-(\d\d)/g, ([_, year, month, day]) =>
        ({ day, month, year, type: "American" })
    })
    .else(val => { throw new Error(`${ val } is not a recognized date!`) })

So if you want to play around with semantics of certain patterns feel free to play around with it, there's already a bunch of functions for common use cases (e.g. primitives, array, etc) which I'll add some documentation too later today or tomorrow.

7fe commented 7 years ago

Maybe it is to early to upvote and downvote different ideas but I do not like the -> syntax(and promptly -1) as proposed because in this case order matters which has the same dellimma as current JavaScript Regex [Number, Number] -> [x, y]: x + y and it also has lots of repetition like here { tag: { name: String, id: Number }} -> { tag: { name, id } }: ...;,

I would +1 the as and is syntax. Especially the following idea.

as remains as an orthogonal extension to both destructuring and this syntax. This has the nice property that it can collapse x: x is Number to just x is Number and _ is Number into is Number when you don't care about the binding (thus making x: is Number a non-binding form of x is Number):

bterlson commented 7 years ago

@limeblack by "order matters" do you mean that the runtime matching part has to come before the refutable destructuring part?

Agree that the repetition is annoying, but nested match expressions can help clean this up in some cases.

My problem with as/is is that it is a deep hole of syntax. For example I think you really need something like @isiahmeadows of as well. It gets very complex for me, though I appreciate the fact that it's more flexible.

rntz commented 7 years ago

@bterlson As far as I can see, the only difference between as/is and your proposal, besides syntax, is that you only allow brand-checking (RuntimePattern) at the level of a branch; you can't nest brand-checks inside ordinary Patterns. I don't understand why you would impose this restriction. As you note, it makes deep destructuring awkward.

What seems most natural to me is to allow putting RuntimePatterns (brand or shape checks) inside Patterns, perhaps like so (if we want a syntax that looks like your suggestion):

Pattern :
  [... whatever other pattern syntaxes there are ...]
  RuntimePattern -> Pattern

Your examples all still work, exactly as you wrote them. BUT, they can also now be written more concisely:

match (val) {
  // structural matching scenarios, same as current proposal
  { x, y }: x + y,
  [ x, y ]: x + y

  // if you want type checking, add a RuntimePattern clause followed by ->
  {x: Number -> x, y: Number -> y}: x + y,
  [Number -> x, Number -> y]: x + y,

  // no longer gets annoying with deep patterns
  { tag: { name: String -> name, id: Number -> id } }: ...,

  // enabling destructuring of the runtime match side enables regexp matching
  someRegExp -> { matches: { name, date } } : x + y,
  someRegExp -> [, name, date] : x + y

  // if regexp named capture groups were properties of the match object
  someRegExp -> { name, date } : x + y,
}

And RuntimePatterns are, as you suggest, just expressions that get evaluated normally.

I don't dislike your proposal, and I have no particular opinions about syntax (other than vaguely not understanding why we need any keywords at all and can't just smash a RuntimePattern next to a Pattern; Number x reads so much more nicely than Number -> x to me); I just don't see why it needs to be so restricted.

bterlson commented 7 years ago

@rntz deep destructuring become awkward but I'm not convinced this is fatal - it's not super awkward, and deeply nested patterns would often call for nested match expressions anyway.

I disagree with what is most natural - if you only allow -> at top-level, you can describe pattern matching in two phases. Given X -> Y: Expr, X is evaluated as a normal expression whose value is consulted for runtime matching and Y is a normal destructuring pattern plus refutability whose bindings are visible in the match leg. If you allow -> inside patterns it's more complex to explain and you have to deal with -> interacting with the rest of the pattern and things like as. However, I am not strictly opposed to allowing this syntax inside a pattern (or even is/as+of) so keep the feedback coming :)

Regarding requirement for ->, @sebmarkbage was arguing for the same thing. I'm somewhat ok with it but I worry that it becomes difficult for humans to read when matching large structures. Especially cases where two curly braces will land back-to-back could be problematic, eg:

{ x: Number, y: Number } { x, y }: ...

seems somewhat hard to read for me. Just a preference, am also ok with the sigilless approach.

dead-claudia commented 7 years ago

@rntz With my proposal in #17, here's what your example would look like:

// Utility for matching regexps.
const match = {
    [Symbol.test]: (arg, re) => re.exec(arg),
    [Symbol.unapply]: (arg, exec) => ({
        get: key => { let v = exec[key]; if (v != null) return {value: v} },
        match: (key, value) => exec[key] === value,
    }),
}

case (val) {
  // structural matching scenarios, same as current proposal
  { x, y } => x + y,
  [ x, y ] => x + y

  // if you want type checking, use `is` after the variable (if necessary).
  {x is "number", y is "number"} => x + y,
  [x is "number", y is "number"] => x + y,

  // no longer gets annoying with deep patterns
  { tag: { name is "string", id is "number" } } => ...,

  // enabling destructuring of the runtime match side enables regexp matching
  is match(someRegExp) of { matches: { name, date } } => x + y,
  is match(someRegExp) of [, name, date] => x + y,

  // My additions, to clarify a couple things
  // You can use different names for properties.
  {someReallyReallyLongPropertyName: x is "number"} => ...,
  {x: x is "number"} => ..., // What `{x is "number"}` desugars to.

  // You don't have to save the variable when checking.
  {x: is "number"} => ...,
  [is "number"] => ...,
}

Notes:

ljharb commented 7 years ago

@isiahmeadows instanceof by default is not something i'd want to see land; instanceof doesn't work cross-realm.

dead-claudia commented 7 years ago

@ljharb What would you feel is the best way to handle instance type checking then? In particular, could you elaborate on it further in a comment in #17?

ljharb commented 7 years ago

https://github.com/jasnell/proposal-istypes imo is the best way to do it; instance checking imo shouldn't be built in to matching unless it uses something like that proposal.

I'll comment there also.

sebmarkbage commented 7 years ago

@bterlson In your latest thinking, is IdentifierMatchPattern still allowed on the top level without a -> after it or is that a binding?

match (x) { None: ... }

If this is a binding then it's unfortunate that a common case, matching only the type, would have to be written in the expanded form match (x) { None -> _: ... }.

If this is matching an identifier and is not a binding, then I don't think this addresses the OP (which might be fine/better).

rattrayalex commented 7 years ago

Personally I'm somewhat excited by @bterlson 's recent proposal https://github.com/tc39/proposal-pattern-matching/issues/11#issuecomment-316851782 .

I think having a clear separation between "this is what we're matching on" and "this is what we're destructuring into" is important (and allowing simple defaults that don't require both seems nice).

There are a few reasons that I think this is important:

  1. It allows matching on what an object property or array element should look like, eg { x: 0, y } with { y }: y, which is a much-lauded feature from other languages that is otherwise hard to do.
  2. It (hopefully) handles the case @sebmarkbage points out well, eg; match { 1: 'one', 2: 'two' }
  3. I think it opens the door to or conditions and guards, which I believe are important to success of the feature. This is because it enforces a single place to declare the variables local to the consequent, rather than multiple – which could be problematic.

Personally I dislike the -> since it is a new glyph unlikely to be used frequently in a majority of code, and likely to require a trip to the docs upon sighting (also hard to google for). I've seen as mentioned, which seems nice. with could also work well, especially as a single point of destructuring (rather than renaming of destructured properties), and is what we choose for this feature in LightScript.

7fe commented 7 years ago

I could see people using pattern matching as alternatives to headless browsers in servers using nodejs. Although this may not be intended but I have used pattern matching before to match HTML/XML. If we adopt https://github.com/tc39/proposal-pattern-matching/issues/11#issuecomment-316851782 I'm concerned people are going to using REGEX and evals to try to create a a syntax similar to Isiah's because it avoids a lot of repetition. I could get behind tntz syntax https://github.com/tc39/proposal-pattern-matching/issues/11#issuecomment-317123338 definitely although it appears it doesn't account for of syntax like above.

EDIT: Could we support both syntaxes maybe by adopting the -> syntax and simply supporting both? LOL

@bterlson Yes I believe that is correct. Basically if the order changes of the pattern you have 2 places you have to modify instead of one and the order is absolutely vital.

rntz commented 7 years ago

@bterlson I see your point about how nesting RuntimePatterns inside Patterns means you have to think about their interaction. It's true this makes things more complicated. I think the added complexity is worth it, but that's just my opinion.

I also think it's fair to say that RuntimePattern Pattern is hard to parse, and just doesn't look very nice, if RuntimePattern is complicated. A simple solution is to require -> if the RuntimePattern is anything other than an identifier. In more detail:

  1. You can place an identifier before a pattern to brand-check it; for example, Number x matches any Number and binds it to the name x; or, Person { name, address } matches any Person object with name and address fields and binds the corresponding variables.

  2. More generally, you can write RuntimePattern -> Pattern, which brand-checks the scrutinee against RuntimePattern and matches the result against Pattern. RuntimePattern can be any expression; there is some protocol (Symbol.match or test/unapply) objects implement to say how to match against them. Examples:

    match ("I'm john") {
    /john/ -> { index: 0 }: "match at start of string",
    /john/ -> { index }: "match at index " + index
    }
    match ({name: "Mary", address: "67 bleeker street #3"}) {
    { name: "Mary", address: /[0-9]+/ -> [num, ...] }:
      "The first number in Mary's address is " + num
    }
  3. As in the original proposal, in an object pattern you can write {x} to mean {x: x}; a bare field name just binds against that field. Moreover, you can prefix a bare field name with an identifier (but not an arbitrary expression, just to keep things visually easy to parse) to brand-check the field. For example, {String name} is the same as {name: String name}. Both patterns match any object with a name field that brand-checks against String.

    More examples:

    match (val) {
     // These two are equivalent.
     {name: String, address: String} -> {name, address}: ..., // your proposal's version
     {String name, String address}: ...,                      // shorter, clearer version
     // If you wanted to bind different names, you could write:
     {name: String x, address: String y}: ...,
    }

So here are our running examples:

match (val) {
  // structural matching scenarios, same as current proposal
  { x, y }: x + y,
  [ x, y ]: x + y

  // to type-check, just shove a type name in front
  {Number x, Number y}: x + y,
  [Number x, Number y]: x + y,

  // deep patterns are easy, even with brand-checks
  { tag: { String name, Number id } }: ...,

  // enabling destructuring of the runtime match side enables regexp matching
  someRegExp -> { matches: { name, date } } : ...,
  someRegExp -> [_, name, date] : ...,

  // @isiahmeadow's additions:
  // you can use different names for properties.
  {someReallyReallyLongPropertyName: Number x}: ...,
  {x: Number -> x}: ..., // What `{Number x}` desugars to.

  // Forgetting variable names has to be fairly explicit, using _
  // (or perhaps * given @ljharb has reservations about _).
  {x: Number _}: ...,
  [Number _]: ...,
}
7fe commented 7 years ago

One example that doesn't seem clear to me how it would work (please reference where this is if already talked about) are linked lists using a custom type. Using the Isiah syntax I believe this might be valid yes/no?

match (p) {
  is TypeLL of { cur, next: is TypeLL of {cur:next}}: ...
}

and I believe there is no way to check type and save reference using the arrow syntax proposed. I'm just adapting the example here https://github.com/tc39/proposal-pattern-matching/issues/45 This is the closest I could come up with.

match (p) {
  TypeLL -> { cur,next:{cur:next} }: ...
}

This would also be issue if this was every used to match against Nodes from HTML. I'm happy to write an example if needed. I do agree the syntax is overwhelming but I personally think that is a editor issue more so then a proposal issue. If you use something like Intelij like I do you can select groups up using Ctrl+W.

dead-claudia commented 7 years ago

@limeblack

One example that doesn't seem clear to me how it would work (please reference where this is if already talked about) are linked lists using a custom type. Using the Isiah syntax I believe this might be valid yes/no?

[code...]

Yes, it would be valid, and it's correct mod match (it's case instead for mine) and : between the pattern and expression (it's =>). It'd really look like this:

case (p) {
    is TypeLL of {cur, next: is TypeLL of {cur: next}} => ...
}

and I believe there is no way to check type and save reference using the arrow syntax proposed. I'm just adapting the example here #45 This is the closest I could come up with.

[code...]

The issue is matching types inside, so the open question is if this works:

match (p) {
  TypeLL -> {cur, next: TypeLL -> {cur: next}}: ...
}
rattrayalex commented 7 years ago

The introduction of an if guard could be helpful here, and result in less "cramming" of logic/unpacking into a single pattern:

match (p) {
  TypeLL with { cur, next } if next ~= TypeLL: ...
}

(this also relies on some kind of isMatch operator, keyword, or builtin, and I'm not sure of the status of that)

EDIT: it's worth pointing out that it's often not necessary to perform such redundant checks, especially when a static type system is used, eg:

match (p: TypeLL) {
  { cur, next: { cur: TypeLL } }: ... // the runtime `TypeLL` is only there to ensure it exists
}

and that, furthermore, there will be cases that are simply very complex, and may (subjectively) be best handled by abstracting out a predicate:

match (p) {
  isLinkedListWithNext() with { cur, next }: foo(cur, next.cur)
}

(the case above isn't that complex, but others are – and the current syntax for declaring/using predicates is a bit unwieldy).

rntz commented 7 years ago

@bterlson I have a question about/problem with the proposal in https://github.com/tc39/proposal-pattern-matching/issues/11#issuecomment-316851782 :

// Is this
match (val) { {x: String} -> s: ... }
// equivalent to this?
const pat = {x: String};
match (val) { pat -> s: ... }

If those are to be equivalent, consider the following:

match (val) { { [Symbol.matches]: String } -> s: ... }

By the same logic, this ought to be equivalent to:

const pat = { [Symbol.matches]: String };
match (val) { pat -> s: ... }

That's really odd! We'd expect the second pattern to try to call the Symbol.matches method of pat to determine whether it matches. So it should call String on val and bind that to s. But this is clearly not what the first example is trying to do: it's trying to determine whether val has a field named [Symbol.matches] that is a String. Uh-oh!

I see only a few solutions which are possible without abandoning the "clean separation" between RuntimePatterns and Patterns that @bterlson suggests:

  1. You can't write RuntimePatterns like { [Symbol.matches]: ... }. This means that RuntimePatterns are no longer just expressions, they're a restricted subset of expressions.

  2. The meaning of a RuntimePattern is not just the meaning of the expression it evaluates to. So the answer to the original question would be "no, they are not equivalent". This feels awkward to me; suddenly RuntimePatterns aren't "just expressions that you can match against".

  3. Accept that RuntimePatterns have a nasty corner case if you put Symbol.matches in them. Yuk.

  4. Have a special-case: if a RuntimePattern evaluates to an object v with a [Symbol.matches] field with value m, and m itself has a [Symbol.matches] field, treat m as a regular field and do not call it to determine whether v matches! This is excessively clever and likely to trip someone up eventually.

If we let RuntimePatterns nest within Patterns, on the other hand, we can simply say that objects aren't useful as RuntimePatterns; use object-shaped Patterns instead. Instead of {x: String, y: Number} -> {x, y}, write {String x, Number y}, or in @isiahmeadows syntax, {x is "string", y is "number"}. As a RuntimePattern, { [Symbol.matches]: ... } is an edge-case; as a Pattern, it's no problem.

zkat commented 6 years ago

Hey y'all! #65 has gotten merged, and a lot of issues have become irrelevant or significantly changed in context. Because of the magnitude of changes and subtle differences in things that seem similar, we've decided to just nuke all existing issues so we can start fresh. Thank you so much for the contributions and discussions and feel free to create new issues if something seems to still be relevant, and link to the original, related issue so we can have a paper trail (but have the benefit of that clean slate anyway).

I also spoke with @tabatkins and I think their original concerns are definitely addressed by the new proposal, so I think this is resolved!!