Closed domenic closed 6 years ago
You could even conceivably get rid of the special case matching for objects and arrays as long as you had a sentinel value for any value
:
match (p) {
{ x: anything, y: anything }: ...
}
which would simply make a new object with x and y properties of the particular value, and O.p[Symbol.matches] does a property-by-property comparison.
Also curious what others think.
I like the "any value" sentinel, as long as it's short and built-in. _ would be ideal, if it wasn't already used by things. I guess we'd have to go outside of ident syntax, like a bare #
or something?
Ah, but you can't get rid of the special-casing for ...
in objects and arrays, unless you add that as a special object-creating syntax as well. (Like Python has the Ellipsis
object.) I'm okay with that, fwiw.
A nice property of this is that you can reuse the array/object matching syntax with other types of specialty objects, just passing the "pattern" array/object as an argument to a function, like mySpecialArraylike([foo, ...])
- with mySpecialArraylike()
doing some processing on its argument and returning an object with a custom Symbol.matches
property.
Yeah I'd rather see pattern matching be expression : expression
.
I actually wrote a library that I never formally released that works very similarly to the proposed Symbol.match
proposed here.
What I learnt from it is that it's very nice to be able to nest patterns inside other patterns when validating complex structures, e.g. I was using pattern matching to validate a js AST I was generating and had rules like this:
import Expression from "./Expression.js"
class ArrayExpression {
static [matchSym](value, matches) {
const pattern = {
type: 'ArrayExpression',
elements: arrayOf(or(Expression, null))
}
return matches(pattern, value)
}
}
// true assuming expr1, expr2 both match Expression
match(ArrayExpression, {
type: 'ArrayExpression',
elements: [expr1, expr2, null, null]
})
I don't really see a way of doing complex stuff like that using the array/object special syntax given that it seems to only do length checking in the case of Array patterns and key existence in the case of Object patterns, I'd prefer something considerably more powerful by default.
If we were to go with the form of expression : expression
though I think it'd be useful to have a way to destructure in there as well e.g.:
function sum(array) {
return match (array) {
[]: 0,
[number]: [x] -> x,
[number, ...]: [x, ...rest] -> x + sum(rest),
// Assuming we have some way to throw in a match clause
else: throw new Error(`Can't sum non-number`)
}
}
Which I think would be reasonably powerful and elegant.
Oooh, separating out the matching from the destructing/binding actually solves a lot of issues, at the cost of making it a little more verbose in some common cases.
foo@pattern
syntax a la Haskell, when you want to both bind something by name and match it against something (currently only possibly in the object syntax).Foo
looks for Foo[Symbol.matches]
, but [Foo]
binds the first element of a length-1 array to the name Foo
).The only downside is a bit of repetition - if you just want to see if an array is length-1, and grab that element if so, you need to write [anyvalue]: [x] -> ...,
; similarly for an object where you just want to grab a particular property: {x: anyvalue}: {x} -> ...,
. It's not a huge deal, but it does make a smallish usability difference.
As your example notes, the only thing required to make that work then is the addition of ...
actually creating some Ellipsis-like object, so you can use it in array matches.
edit: I came into this thread from a discussion elsewhere and I lost the context while mixing the two conversations together. I don't think my comment here is directly related to the thread here so I've moved it behind a <details>
.
@zkat I'm confused what you're responding to. I don't think anyone in this thread has proposed typeof-based matching, nor does the proposal in the README...
From the proposal:
A basic implementation might simply be return
value instanceof this.constructor
. Such a basic implementation could be created by default for types created viaclass
keyword.
Sure, but that's not typeof, and is also unrelated to the OP... if people want to discuss that, I'd suggest a different issue?
(I've edited my comment)
I wonder if you could have both the separate and combined syntax:
match (something) {
// No binding at all
[]: 0,
// Seperate binding to matching
[any]: [x] -> x.toString(),
// Combined binding and matching equivalent to previous
[x] :-> x.toString(),
// Works fine with objects too
{ x: any } : { x } -> x.toString(),
{ x } :-> x.toString(),
// Would need some form of additional syntax
// to further match inner types though
[x@number] :-> x.toString(),
{ x@number : y } :-> y.toString(),
}
Also regarding ellipsis as a value, I think it'd be nice to be able to pass types into the ellipsis e.g.:
match (dataLine) {
['open', ...string /* 0 or more strings */] : ...,
}
Perhaps a slightly different token e.g.:
match (dataLine) {
// Probably confusing
['open', ..string] : 1,
// Also probably confusing
['open', string...] : 2
}
Or alternatively an addition to the ArrayExpression
/ObjectExpression
grammars:
// & Makes it very clear its separate syntax from spread
const pattern1 = ['open', &...string]
// Could just add a Symbol field for rest:
pattern1[Symbol.rest] === string // true
// Also works with objects nicely:
const pattern2 = { x: 10, y: 20, &...empty }
pattern2[Symbol.rest] === empty // true
// Might take more work to support left/inner rest though
const pattern3 = [&...number, Function]
pattern3[Symbol.restIndex] === 0 // true
// or
pattern3[Symbol.rest].index === 0 // true
pattern3[Symbol.rest].value === number // true
const pattern4 = [number, &...string, number]
pattern3[Symbol.restIndex] === 1 // true
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).
The new extractor-based API in this proposal should clarify this particular usecase pretty well.
It seems at least doable that numeric, string, and boolean literals (see #3 for the latter) could go through
Number.prototype[Symbol.matches]
.I'm not sure whether this buys you much though, given that you already have special-cases for the object and array patterns.
I guess maybe it would allow you to simplify the syntax to:
where in the third case you go the Symbol.matches route. This third case would subsume the current identifier and literal cases.
Curious what people think.