This proposal has several parts:
-
A new syntax construct, the "matcher pattern", which is an elaboration on (similar to but distinct from) destructuring patterns.
Matcher patterns allow testing the structure of an object in various ways, and recursing those tests into parts of the structure to an unlimited depth (similar to destructuring).
Matcher syntax intentionally resembles destructuring syntax but goes well beyond the abilities and intention of simple destructuring.
-
A new binary boolean operator,
is, which lets you test values against matchers. If the matcher establishes bindings, this also pulls those bindings out into the operator's scope. -
A new syntax construct, the
match()expression, which lets you test a value against multiple patterns and resolve to a value based on which one passed.
Destructuring matchers:
-
array matchers:
[<matcher>, <matcher>]exactly two items, matching the patterns[<matcher>, <matcher>, ...]two items matching the patterns, more allowed[<matcher>, <matcher>, ...let <ident>]two items matching the patterns, with remainder collected into a list bound to<ident>. (Can useconstorvaras well; see "binding matchers". Only binding matchers allowed in that position; not anything else.)
-
object matchers:
{<ident>, <ident>}has the ident keys (in its proto chain, not just own keys), and binds the value to that ident. Can have other keys. (aka{a}is identical to{a: let a}){<ident>: <matcher>, <ident>: <matcher>}has the ident keys, with values matching the patterns. Can have other keys.{<ident>: <matcher>, ...let <ident2>}has the ident key, with values matching the pattern. Remaining own keys collected into an object bound to<ident2>.
-
binding matchers:
let <ident>/const <ident>/var <ident>. Binds the matchable to the ident. (That is,[let a, let b]doesn't test the items in the array, just exposes them asaandbbindings.)- (To bind a matchable and apply more matchers, use
andto chain them:let a and [b, c].)
Value-testing matchers:
-
literal matchers:
1,"foo",- etc. All the primitives, plus (untagged only?) template literals.
- also unary plus/minus
-0and+0test for the properly-signed zero,0just uses===equality.NaNtests for NaN properly.
-
variable matchers
-
<plain-or-dotted-ident>evaluates the name.If the name has a custom matcher (see below), it passes the matchable to the custom matcher function and matches if that succeeds. Otherwise, it just matches based on equality. (Uses
===semantics, except that NaN is matched properly.) -
<plain-or-dotted-ident>(<matcher-list>)evaluates the ident, grabs itsSymbol.matcherproperty, then invokes it on the matchable. (Throws if it doesn't have aSymbol.matcherproperty, or it's not a function.) If that succeeds, it further matches the result against the arglist, as if it was an array matcher.Option.Some(foo) examples goes here
-
-
regex matchers:
/foo/matches if the regex matches. Named capture groups establishletbindings./foo/(<matcher-list>)is identical to custom matcher - if the regex matches, then the match result (the regex match object) is further destructured by the matcher list.
Boolean matcher logic:
<matcher> and <matcher>: Tests the matchable against both matchers (in order), succeeds only if both succeed. Accumulates bindings from both. If first fails, short-circuits.<matcher> or <matcher>: Tests the matchable against both matchers (in order), succeeds if either succeeds. Accumulates bindings from both, but values only from the first successful matcher (other bindings becomeundefined). If first succeeds, short-circuits.not <matcher>: Tests the matchable against the matcher, succeeds only if the matcher fails. No bindings.- Matchers can be parenthesized, and must be if you're using multiple keywords; there is no precedence relationship between the keywords, so it's a syntax error to mix them at the same level.
-
New
match(){}expression:match(<val-expr>) { when <matcher>: <result-expr>; default: <result-expr>; }
Find the first "arm" whose matcher passes, given the val. Evaluates to the corresponding result for that arm. The matcher can produce bindings that are visible within the matcher and within the result; they don't escape the arm they're established in. (Are
varmatchers allowed or disallowed?)defaultarm always matches. If no arm matches, throws. -
New
isoperator<val-expr> is <matcher>
Evaluates to true/false if val passes the matcher or not. If the matcher has binding patterns, within the matcher they behave as normal; see below for behavior outside of the matcher.
Doing it manually with match() would be:
let passes = match(<val-expr>) { when <matcher>: true; default: false; }
-
When
isis used and the matcher establishes bindings:-
In
if(), the bindings are lifted to a scope immediately outside theif()block, encompassing the followingelseas well. (Likely, we define an analogous scope to whatfor(of)uses.) Lexical bindings are TDZ if the matcher doesn't match.varbindings simply don't set a value if the matcher doesn't match.(Bindings will often not be useful in the
else, but will be in cases likeif(!(x is <matcher>)){...}else{...}, where the matcher successfully matches but theiffails.) -
In
while()anddo{}while(), same behavior. (Indo{}while(), lexical bindings are TDZ on the first iteration.) -
In
for-of, the bindings exist in the current outerforscope, same as any other bindings established in theforhead.(TODO: write an example of for-of usage; I'm not clear how it's supposed to work.)
-
(We've lost matchers in plain let/etc statements, which I guess also means we lose matchers in function arglists. Unfortunate.)
I have a few comments about terminology, since the term "matcher" has been used to mean different things.
I think, formally, the syntax contract to the right of
isand that is part of awhenclause should be called a "match pattern" orMatchPattern, to disambiguate from an "assignment pattern"/AssignmentPatternor "binding pattern"/BindingPattern.We've also used the term "matcher" for the thing being matched, but I think a better term might be "subject", i.e., "the subject of the pattern". The "subject" would be the given value being matched at any particular depth of the pattern. For example, given the value
and the expression
{ a: 1 or 2, b: let y }is the value ofx1 or 2would be the value ofx.alet ywould be the value ofx.bWe also use the term "custom matcher" to represent an object with special matching behavior. This is probably fine since it is itself a "thing that matches", though I imagine most cases of "custom matchers" in this sense will often be referred to as "extractors".
Just to be sure I am clear on the meaning here, given the following code:
the expectation is that the pattern
{ a }is the equivalent of{ a: a }, in that it establishes a property match pattern forawhose value is derived from the referencea. The distinction here being that{a}in an assignment pattern assigns to a free variable nameda, and{a}in a binding pattern binds a local variable nameda. For the equivalent binding-like behavior for match patterns we would use{ a: let a }.If that's correct, then the line that follows later is incorrect:
I would expect this to instead read:
The term I would suggest is
<qualified-name>or maybe<dot-qualified-name>, and will likely match the restrictions of qualified names in the Decorators proposal (soa.b[c](d)will not be a valid extractor pattern).Is this to avoid repetition? I'm unsure about introducing magical
letbindings, especially if they might only be named for the purpose of\k<name>references. It means users would have to ensure they don't use a group name that shadows a local variable, even if they don't intend to reference that group outside of the match. It also reminds me too much ofRegExp.$1. Perhaps I am in the minority, but I don't findtoo arduous, though it does make me wonder if might want to introduce a property binding shorthand for object patterns, i.e.:
{ let x }as meaning the same as{ x: let x }to mirror the shorthand property pattern.I want to be sure we're still considering the
ifclause in some way, whether that is amatch-specific clause or a special kind of pattern, i.e.:The upside of it being a pattern would be that it could be used in-situ within an
isexpression, though without it we could still emulate anifpattern with${}depending on how things fall for predicate functions, i.e.:Also, I wonder whether we should use
;or,here.;seems like the way to go when you comparematchtoswitch, but;also may require special consideration with respect to ASI. Some pattern syntaxes use,to separate clauses, which is a bit more in keeping with other expressions. You only really find;in an expression when it is nested in the body of a class or function. No other expressions make use of;.The analog would be
for(;;), which introduces a lexical environment around theforitself, and copiesletbindings into each per-iteration environment (constbindings aren't copied because they can't change per-iteration).for-ofandfor-inonly introduce per-iteration environments.There are two positions in
for-of,for-await-of, andfor-inin which either amatchorisexpression could occur. Most often it is likely to occur on the right-hand side ofof/in, as in this case:Though in the equivalent version above,
arwill not have TDZ, though it will in the pattern-matching version.I haven't been quite as concerned about
let, since you could do something like:Though that does make me want to try to bring
throwexpressions back. For bothletand parameters we already have binding patterns and will hopefully have extractors there as well, but we could still consider a keyword or token opt-in (assuming we can find something that isn't ambiguous with array patterns), i.e.:Anything we do with parameters becomes tricky due to ambiguity, so things like
work until they become ambiguous with
unless we can retrofit an existing reserved word like
case.catchwill is a bit easier since we can inject syntax nearcatch: