Currently the "match construct" of form match (matchable) { ... } is an eagerly evaluated expression against matchable.
I wonder if it's possible to also include a let matcher = match { ... } contruct, without the (matchable) part, that would return a matcher function which can be called later as matcher(matchable).
The key value proposition is that, this matcher function should at the same time has the [Symbol.matcher] property, value of which is a function (matchable) => MatchResult that encapsulate the pattern matching logics. Otherwise it would be equivalent to let mather = (matchable) => match(matchable) {...}, render the proposal unnecessary.
let matcher = match {
when ("foo") {
"bar"
}
when ("zoo") {
"boo"
}
}
Above is equivalent to:
let matcher = (matchable) => match (matchable) {
when ("foo") {
"bar"
}
when ("zoo") {
"boo"
}
}
matcher[Symbol.matcher] = (matchable) => match (matchable) {
when ("foo") {
({ matched: true, value: matchable })
}
when ("zoo") {
({ matched: true, value: matchable })
}
else {
({ matched: false })
}
}
The most valuable part is the auto created [Symbol.matcher] function. User would be otherwise forced to repeat most part of the code, simply to substitute a MatchResult for return value, so as to obtain an equivalent.
Shouldnât it be something like this instead?
matcher[Symbol.matcher] = (matchable) => match (matchable) {
when ("foo") {
({ matched: true, value: "bar" })
}
when ("zoo") {
({ matched: true, value: "boo" })
}
else {
({ matched: false })
}
}
No, the intended usage would be just to capture the matching part of the logic, not the actual business logic that happens after one pattern is matched.
Itâs an interesting idea indeed to have some form of syntax that allows for easy creation of match result objects - but i donât think it would make sense or be performant to have the default behavior of the construct to create a function, that you then have to invoke for the common case (which isnât what youâre suggesting).
I suspect that omitting the matchable is more likely to be a bug than an intentional choice to make a function - ie, match (matchable) { being eager and match { being sugar for x => match (x) { plus adding the match result object wrapper. I also suspect that more people would want to use this syntax merely as the sugar without the object wrapper - just like authoring decorators, creating custom matchers is likely to be a relatively rare behavior, and we generally try to optimize for the consumers of things like this rather than the authors.
We could avoid a restricted production by reusing existing keyword cues
{
case pattern: ...
}
In places where an ObjectLiteral can appear, we could allow a CaseBlock which starts with { followed by the token case followed by a token that is not : so is distinct from the ObjectLiteral ({ case: 0 }).
So the example desugaring above might apply to
({
case ("foo"):
"bar"
case ("zoo"):
"boo"
})
@mikesamuel one of the explicit priorities for the proposal is âno overlap with switchâ, so case is a nonstarter.
@mikesamuel Iâm confused. Howâs it related to my proposal?
@hackape, Please ignore. I didn't know about the "no overlap on switch" requirement.
@ljharb My thought about it, pattern matching is really two things mixed together.
It's a two-phase process by its nature, first we test, if matched then evaluate.
The test part is side-effect free, while the evaluate part is not. They're organically tied together syntax-wise, but feature-wise they're separated. Because of this complication, it's can only be gracefully addressed on language level.
I get the inspiration from elixir's receive do ... end block, where pattern matching is used as a predicate to test against message to decide if it should proceed to handle the message, or just pass and leave it intact. It's a good example to illustrate the usefulness of such language feature.
We could model the elixir's receive block with generator function
function* coroutine() {
const retValue = yield match {
when ({ type: "foo" }) "bar"
when ({ type: "boo" }) "zoo"
when ({ type: "other" }) undefined
}
}
The coroutine driver on the other side can take the matcher function, use its [Symbol.matcher]() to find the message from mailbox, without worrying incur unexpected side-effect.
The test part is side-effect free, while the evaluate part is not.
The test part isn't necessarily side-effect free, note. Getters or proxies can react to keys being tested in an object matcher, iterators can react to being consumed in an array matcher, and custom matchers can do anything when invoked. (It is usually side-effect free, tho, and having side effects in these situations is probably a mistake.)
I think I'm with Jordan in being moderately against this idea, unfortunately. It's intriguing, but it's unprecedented in syntax - that doesn't make it impossible, it just means it needs to be really obviously a great idea to justify innovating in such a way, or an obvious extension point the language will use more of later. I don't think it qualifies for either.
In particular, this makes several assumptions about common behavior:
match(), with no manipulation of the matchable before passing it into the match().I don't think any of these are particularly well-justified, unfortunately, and so I can't see this proposal pulling its weight.
I very well may have missed something crucial here, but I don't see how this is different from:
let matcher = (matchable) => {
return match (matchable) { ... }
};
If my understanding here is correct, then I agree with Tab and Jordan that this probably doesn't pull its own weight. But please correct me if this is not actually equivalent and I've misunderstood something.
The OP is suggesting that this syntax both defines a function that returns the result of a match(), as you show in your example, and adds a [Symbol.matcher] to the function that uses the same match logic but just returns the matchable.
I see - I did in fact misunderstand the OP, apologies for not reading closely enough.
I do still find it hard to see this pulling its own weight. What are some concrete use-cases?
From https://github.com/tc39/proposal-pattern-matching/issues/193#issuecomment-836941467 something like this with a "one clause only" matcher could be used to succinctly compose patterns, since the pattern expression is only valid as an argument of the when clause.
I see this making it quicker to define a parsing expression grammar like e.g. PEG.js but something like that may be better suited to a single recursive match expression with many different clauses.
const nodeCharacters = match when({ kind: 'characters', value: /^\w+$/ }) { value };
const nodeCodepoints = match when({ kind: 'codepoints', value: [...values] }) { values.map(String.fromCharCode) };
const nodeString = match when(^nodeString | ^nodeCharacters);
assert(Symbol.matcher in nodeCharacters)
assert(Symbol.matcher in nodeCodepoints)
assert(Symbol.matcher in nodeString)