Closed tabatkins closed 6 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).
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?
Ignore the second one, I thought that you could name a variable else
.
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.
@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.
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): ... }
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.
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.
@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:
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.";
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]
.
@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.
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?
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
}
@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.
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.
@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.
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 :-/
@ljharb I agree, it's unlikely we can advance this proposal without also figuring out brand checking. Is could be a future extension.
@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 }
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'
}
@rntz Actually, it would be is Person of {name, address}
in my proposal. Just thought I'd clarify.
[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)
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...
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.
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 collapsex: x is Number
to justx is Number
and_ is Number
intois Number
when you don't care about the binding (thus makingx: is Number
a non-binding form ofx is Number
):
@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.
@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 Pattern
s. 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 RuntimePattern
s (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 RuntimePattern
s 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.
@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.
@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:
is "string"
tests typeof value === "string"
and is Type
tests value instanceof Type
by default.Symbol.test
is always called to test, Symbol.unapply
is optionally called to customize destructuring.@isiahmeadows instanceof
by default is not something i'd want to see land; instanceof
doesn't work cross-realm.
@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?
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.
@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).
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:
{ x: 0, y } with { y }: y
, which is a much-lauded feature from other languages that is otherwise hard to do.match { 1: 'one', 2: 'two' }
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.
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.
@bterlson I see your point about how nesting RuntimePattern
s inside Pattern
s 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:
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.
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
}
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 _]: ...,
}
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.
@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}}: ...
}
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).
@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:
You can't write RuntimePatterns like { [Symbol.matches]: ... }
. This means that RuntimePatterns are no longer just expressions, they're a restricted subset of expressions.
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 RuntimePattern
s aren't "just expressions that you can match against".
Accept that RuntimePatterns have a nasty corner case if you put Symbol.matches
in them. Yuk.
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.
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!!
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:
x
,x
is a variable reference, forming an identifier pattern - thex
variable in the containing scopes will be queried for aSymbol.matches
property. In[x]
,x
is a binding name - it matches a length-1 array and binds its first value tox
. It's thus impossible to nest an identifier pattern matching inside of an array matching. However, it is possible to nest it into an object matching, via{x: identifier}
.else
,else
is raw syntax - it denotes the fall-through case. It's thus impossible to have an identifier pattern named byelse
. This is a small hazard for user code (disallowed names sucked in ES3), and is an extension hazard for the future, as you'll have to wrestle with the possibility of breaking userland code already using a new name as an identifier pattern.I don't have good solutions for either of these, tho.