Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Pattern matching syntax for ECMAScript

License

NotificationsYou must be signed in to change notification settings

tc39/proposal-pattern-matching

Stage: 1

Spec Text:https://tc39.github.io/proposal-pattern-matching

Authors: Originally Kat Marchán (Microsoft,@zkat__); now, the below champions.

Champions: (in alphabetical order)

Introduction

Problem

There are many ways to matchvalues in the language,but there are no ways to matchpatternsbeyond regular expressions for strings.However, wanting to take different actionsbased on patterns in a given valueis a very common desire:do X if the value has afoo property,do Y if it contains three or more items,etc.

Current Approaches

switch has the desiredstructure --a value is given,and several possible match criteria are offered,each with a different associated action.But it's severely lacking in practice:it may not appear in expression position;an explicitbreak is required in eachcase to avoid accidental fallthrough;scoping is ambiguous(block-scoped variables inside onecase are available in the scope of the others,unless curly braces are used);the only comparison it can do is===; etc.

if/else has the necessarypower(you can perform any comparison you like),but it's overly verbose even for common cases,requiring the author to explicitly list paths into the value's structure multiple times,once per test performed.It's also statement-only(unless the author opts for the harder-to-understand ternary syntax)and requires the value under test to be stored in a (possibly temporary) variable.

Priorities for a solution

This section details this proposal’s priorities. Note that not every championmay agree with each priority.

Pattern matching

The pattern matching construct is a full conditional logic construct that can domore than just pattern matching. As such, there have been (and there will bemore) trade-offs that need to be made. In those cases, we should prioritize theergonomics of structural pattern matching over other capabilities of thisconstruct.

Subsumption ofswitch

This feature must be easily searchable, so that tutorials and documentation areeasy to locate, and so that the feature is easy to learn and recognize. As such,there must be no syntactic overlap with theswitch statement.

This proposal seeks to preserve the good parts ofswitch, and eliminate anyreasons to reach for it.

Be better thanswitch

switch contains a plethora of footguns such as accidental case fallthrough andambiguous scoping. This proposal should eliminate those footguns, while alsointroducing new capabilities thatswitch currently can not provide.

Expression semantics

The pattern matching construct should be usable as an expression:

  • return match { ... }
  • let foo = match { ... }
  • () => match { ... }
  • etc.

The value of the whole expression is the value of whateverclause ismatched.

Exhaustiveness and ordering

If the developer wants to ignore certain possible cases, they should specifythat explicitly. A development-time error is less costly than a production-timeerror from something further down the stack.

If the developer wants two cases to share logic (what we know as "fall-through"fromswitch), they should specify it explicitly. Implicit fall-throughinevitably silently accepts buggy code.

Clauses should always be checked in the order they’re written, i.e.from top to bottom.

User extensibility

Userland objects should be able to encapsulate their own matching semantics,without unnecessarily privileging builtins. This includes regular expressions(as opposed to the literal pattern syntax), numeric ranges, etc.

Prior Art

This proposal adds a pattern matching expression to the language, based in parton the existingDestructuring Binding Patterns.

This proposal was approved for Stage 1 in the May 2018 TC39 meeting, and slidesfor that presentation are availablehere.Its current form was presented to TC39 in the April 2021 meeting(slides).

This proposal draws from, and partially overlaps with, corresponding features inCoffeeScript,Rust,Python,F#,Scala,Elixir/Erlang,andC++.More recently,Flow has gained a featureclosely matching and clearly inspired by the syntax proposed here.

Userland matching

A list of community libraries that provide similar matching functionality:

  • Optionals — Rust-like error handling, options and exhaustive pattern matching for TypeScript and Deno
  • ts-pattern — Exhaustive Pattern Matching library for TypeScript, with smart type inference.
  • babel-plugin-proposal-pattern-matching — Minimal grammar, high performance JavaScript pattern matching implementation.
  • match-iz — A tiny functional pattern-matching library inspired by the TC39 proposal.
  • patcom — Feature parity with TC39 proposal without any new syntax

Specification

This proposal introduces three new concepts to #"auto">

  • the "matcher pattern",a new DSL closely related to destructuring patterns,which allows recursively testing the structure and contents of a valuein multiple ways at once,and extracting some of that structure into local bindings at the same time
  • thematch(){} expression,a general replacement for theswitch statementthat uses matcher patternsto resolve to one of several values,
  • theis boolean operator,which allows for one-off testing of a value against a matcher pattern,potentially also introducing bindings from that test into the local environment.
  • Matcher Patterns

    Matcher patterns are a new DSL,closely inspired by destructuring patterns,for recursively testing the structure and contents of a valuewhile simultaneously extracting some parts of that valueas local bindings for use by other code.

    Matcher patterns can be divided into three general varieties:

    • Value patterns, which test that the subject matches some criteria, like "is the string"foo"" or "matches the variablebar".
    • Structure patterns, which test the subject matches some structural criteria like "has the propertyfoo" or "is at least length 3", and also let you recursively apply additional matchers to parts of that structure.
    • Combinator patterns, which let you match several patterns in parallel on the same subject, with simple booleanand/or logic.

    Value Matchers

    There are several types of value patterns, performing different types of tests.

    Primitive Patterns

    All primitive values can be used directly as matcher patterns,representing a test that the subject matches the specified value,usingSameValue semantics(except when otherwise noted).

    For example,1 tests that the subject isSameValue to1,"foo" tests that it'sSameValue to"foo",etc.

    Specifically,boolean literals,numeric literals,string literals,untagged template literals,and the null literalcan all be used.

    • Numeric literals can additionally be prefixed with a+ or- unary operator:+ is a no-op (but see the note about0, below),but- negates the value,as you'd expect.
    • Within the interpolation expressions of template literals,seeBindings for details on what bindings are visible.

    The one exception toSameValue matching semanticsis that the pattern0 is matched usingSameValueZero semantics.+0 and-0 are matched withSameValue, as normal.(This has the effect that an "unsigned" zero patternwill match both positive and negative zero values,while the "signed" zero patternswill only match the appropriately signed zero values.)

    (Additional notes forSameValue semantics:it works "as expected" for NaN values,correctly matching NaN values against NaN patterns;it does not do any implicit type coercion,so a1 value will not match a"1" pattern.)

    Note: No coercion takes place in these matches:if you match against a string literal, for example,your subject must already be a stringor else it'll automatically fail the match,even if it would stringify to a matching string.

    Examples

    Variable Patterns

    A variable pattern is a "ident expression":foo,foo.bar,foo[bar] etc.,excluding those that are already primitives likenull,and optionally prefixed by a+ or- unary operator.

    A variable pattern resolves the identifier against the visible bindings(seeBindings for details),and if it has a+ or- prefix,converts it to a number (viatoValue)and possibly negates it.If the result is an object with aSymbol.customMatcher property,or is a function,then it represents a custom matcher test.Seecustom matchers for details.Otherwise, it represents a test that the subject isSameValue with the result,similar to aPrimitive Pattern.

    Note: This implies that, for example,a variable holding an array will only match that exact array,via object equivalence;it is not equivalent to anarray patterndoing a structural match.

    Note that several values which are oftenthought of as literals,likeInfinity orundefined,are in fact bindings.Since Primitive Patterns and Variable Patterns are treated largely identically,the distinction can fortunately remain academic here.

    Note: Just like with Primitive Patterns,no coercion is performed on the subjects(or on the pattern value,except for the effect of+/-,which is explicitly asking for a numeric coercion),so the type has to already match.

    Examples

    Custom Matchers

    If the object that the variable pattern resolves tohas aSymbol.customMatcher property in its prototype chain,then it is a "custom matcher".

    To determine whether the pattern matches or not,the custom matcher function is invokedwith the subject as its first argument,and an object with the key"matchType" set to"boolean"as its second argument.

    If it returns a truthy value(one which becomestrue when!! is used on it)the match is successful;otherwise,the match fails.If it throws,it passes the error up.

    Note:Extractor patterns use the identical machinery,but allow further matching against the returned value,rather than being limited to just returning true/false.

    Examples

    Built-in Custom Matchers

    Several JS objects have custom matchers installed on them by default.

    All of the classes for primitive types(Boolean, String, Number, BigInt, Symbol)expose a built-in Symbol.customMatcher static method,matching if and only if the subject isa primitive (or a boxed object) corresponding to that typeThe return value of a successful match(for the purpose ofextractor patterns)is an iterator containing the (possibly auto-unboxed) primitive value.

    classBoolean{static[Symbol.customMatcher](subject){returntypeofsubject=="boolean";}}/* et cetera for the other primitives */

    Function.prototype has a custom matcherthat checks if the function has an[[IsClassConstructor]] slot(meaning it's theconstructor() function from aclass block);if so, it tests whether the subject is an object of that class(using brand-checking to verify, similar toArray.isArray());if not, it invokes the function as a predicate,passing it the subject,and returns the return value:

    /* roughly */Function.prototype[Symbol.customMatcher]=function(subject){if(isClassConstructor(this)){returnhasCorrectBrand(this,subject);}else{returnthis(subject);}}

    This way, predicate functions can be used directly as matchers,likex is upperAlpha,and classes can also be used directly as matchersto test if objects are of that class,likex is Option.Some.

    RegExp.prototype has a custom matcherthat executes the regexp on the subject,and matches if the match was successful:

    RegExp.prototype[Symbol.customMatcher]=function(subject,{matchType}){constresult=this.exec(subject);if(matchType=="boolean")returnresult;if(matchType=="extractor")return[result, ...result.slice(1)];}

    Examples

    Binding Patterns

    Alet,const, orvar keyword followed by a valid variable name(identical to binding statements anywhere else).Binding patterns always match,and additionally introduce a binding,binding the subject to the given namewith the given binding semantics.

    Binding Behavior Details

    As with normal binding statements,the bindings introduced by binding patternsare established in the nearest block scope(forlet/const)or the nearest function scope (forvar).

    Issue: Or in the obvious scope, when used in a for/while header or a function arglist.Don't know the right term for this off the top of my head.

    Bindings are established according to theirpresence in a pattern;whether or not the binding pattern itself is ever executed is irrelevant.(For example,[1, 2] is ["foo", let foo]will still establish afoo binding in the block scope,despite the first pattern failing to matchand thus skipping the binding pattern.)

    Standard TDZ rules apply before the binding pattern is actually executed.(For example,when [x, let x] is an earlyReferenceError,since thex binding has not yet been initializedwhen the first pattern is runand attempts to dereferencex.)

    Unlike standard binding rules,within the scope of an entire top-level pattern,a given name can appear in multiple binding patterns,as long as all instances use the same binding type keyword.It is a runtimeReferenceErrorif more than one of these binding patterns actually execute, however(with one exception - seeor patterns).(This behavior has precedent:it was previously the case that named capture groupshad to be completely unique within a regexp.Now they're allowed to be repeatedas long as they're in different branches of an alternative,like/foo(?<part>.*)|(?<part>.*)foo/.)

    Examples

    (xor[lety])and(zor{key:lety})

    Valid at parse-time: both binding patterns nameywithlet semantics.This establishes ay binding in the nearest block scope.

    If xor z matches, but not both,theny gets bound appropriately.If neither matches,y remains uninitialized(so it's a runtime ReferenceError to use it).If both match, a runtime ReferenceError is thrownwhile executing the secondlet y pattern,as its binding has already been initialized.

    (xor[lety])and(zor{key:consty})

    Early ReferenceError, asy is being bound twicewith differing semantics.

    xandletyandzandif(y=="foo")

    Valid at parse-time, establishes ay binding in block scope.

    If x doesn't match,y remains uninitialized,but the guard pattern is also skipped,so no runtime error (yet).If z doesn't match,y is initialized to the match subject,but theif() test never runs.

    [letxandString]or{length:letx}

    Valid at parse-time, establishes anx binding.

    or pattern semantics allow overriding an already-initialized binding,if that binding came from an earlier failed sub-pattern,to avoid forcing authors to awkwardly arrange their binding patternsafter the fallible tests.

    So in this example, if passed an object like[5],it will pass the initial length check,execute thelet x pattern and bind it to5,then fail theString pattern,as the subject is aNumber.It will then continue to the nextor sub-pattern,and successfully bindx to 1,as the existing binding was initialized in a failed sub-pattern.

    Structure Patterns

    Structure patterns let you test the structure of the subject(its properties, its length, etc)and then recurse into that structure with additional matcher patterns.

    Array Patterns

    A comma-separated list of zero or more patterns, surrounded by square brackets.It represents a test that:

    1. The subject is iterable.
    2. The subject contains exactly as many iteration itemsas the length of the array pattern.
    3. Each item matches the associated sub-pattern.

    For example,["foo", {bar}] will matchwhen the subject is an iterable with exactly two items,the first item is the string"foo",and the second item has abar property.

    The final item in the array pattern can optionally be a "rest pattern":either a literal...,or a... followed by another pattern.In either case, the presence of a rest pattern relaxes the length test(2 in the list above)to merely check that the subject hasat least as many itemsas the array pattern,ignoring the rest pattern.That is,[a, b, ...] will only match a subjectwho contains 2 or more items.

    If the... is followed by a pattern,like[a, b, ...let c],then the iterator is fully exhausted,all the leftover items are collected into anArray,and that array is then applied as the subject of the rest pattern's test.

    Note: The above implies that[a, b] will pull three items from the subject:two to match against the sub-patterns,and a third to verify that the subject doesn'thave a third item.[a, b, ...] will pull only two items from the subject,to match against the sub-patterns.[a, b, ...c] will exhaust the subject's iterator,verifying it has at least two items(to match against the sub-patterns)and then pulling the rest to match against the rest pattern.

    Array pattern execution order is as follows:

    1. Obtain an iterator from the subject. Return failure if this fails.
    2. For each expected item up to the number of sub-patterns (ignoring the rest pattern, if present):
      1. Pull one item from the iterator. Return failure if this fails.
      2. Execute the corresponding pattern. Return failure if this doesn't match.
    3. If there is no rest pattern, pull one more item from the iterator, verifying that it's a{done: true} result. If so, return success; if not, return failure.
    4. If there is a... rest pattern, return success.
    5. If there is a...<pattern> rest pattern, pull the remaining items of the iterator into a freshArray, then match the pattern against that. If it matches, return success; otherwise return failure.

    Issue: Or should we pull all the necessary values from the iterator first,then do all the matchers?

    Examples

    match(res){whenisEmpty: ...;when{data:[letpage]}: ...;when{data:[letfrontPage, ...letpages]}: ...;  default: ...;}

    Array patterns implicitly check the length of the subject.

    The first arm is avariable pattern,invoking the defaultFunction.prototype custom matcherwhich callsisEmpty(res)and matches if that returnstrue.

    The second arm is anobject patternwhich contains anarray pattern,which matches ifdata has exactly one element,and binds that element topage for the RHS.

    The third arm matches ifdata hasat least one element,binding that first element tofrontPage,and binding an array of any remaining elements topagesusing a rest pattern.

    Array Pattern Caching

    To allow for idiomatic uses of generatorsand other "single-shot" iteratorsto be reasonably matched against several array patterns,the iterators and their results are cached over the scope of the match construct.

    Specifically, whenever a subject is matched against an array pattern,the subject is used as the key in a cache,whose value is the iterator obtained from the subject,and all items pulled from the subject by an array pattern.

    Whenever something would be matched against an array pattern,the cache is first checked,and the already-pulled items stored in the cache are used for the pattern,with new items pulled from the iterator only if necessary.

    For example:

    function*integers(to){for(vari=1;i<=to;i++)yieldi;}constfiveIntegers=integers(5);match(fiveIntegers){when[leta]:console.log(`found one int:${a}`);// Matching a generator against an array pattern.// Obtain the iterator (which is just the generator itself),// then pull two items:// one to match against the `a` pattern (which succeeds),// the second to verify the iterator only has one item// (which fails).when[leta,letb]:console.log(`found two ints:${a} and${b}`);// Matching against an array pattern again.// The generator object has already been cached,// so we fetch the cached results.// We need three items in total;// two to check against the patterns,// and the third to verify the iterator has only two items.// Two are already in the cache,// so we’ll just pull one more (and fail the pattern).  default:console.log("more than two ints");}console.log([...fiveIntegers]);// logs [4, 5]// The match construct pulled three elements from the generator,// so there’s two leftover afterwards.

    When execution of the match construct finishes, all cached iterators are closed.

    Object Patterns

    A comma-separated list of zero or more "object pattern clauses", wrapped in curly braces.Each "object pattern clause" is either<key>,let/var/const <ident> or<key>: <pattern>,where<key> is an identifier or a computed-key expression like[Symbol.foo].It represents a test that the subject:

    1. Has every specified property in its prototype chain.
    2. If the key has an associated sub-pattern,then the value of that property matches the sub-pattern.

    A<key> object pattern clauseis exactly equivalent to<key>: void.Alet/var/const <ident> object pattern clauseis exactly equivalent to<ident>: let/var/const <ident>.

    That is,when {foo, let bar, baz: "qux"}is equivalent towhen {foo: void, bar: let bar, baz: "qux"}:it tests that the subject hasfoo,bar, andbaz properties,introduces abar binding for the value of thebar property,and verifies that the value of thebaz property is the string"qux".

    Additionally, object patterns can contain a "rest pattern":a... followed by a pattern.Unlike array patterns, a lone... is not valid in an object pattern(since there's no strict check to relax).If the rest pattern exists,then allenumerable own propertiesthat aren't already matched by object pattern clausesare collected into a fresh object,which is then matched against the rest pattern.(This matches the behavior of object destructuring.)

    Issue: Do we want akey?: pattern pattern clause as well?Makes it an optional test -if the subject has this property,verify that it matches the pattern.If the pattern is skipped because the property doesn't exist,treat any bindings coming from the patternthe same as ones coming from skippedor patterns.

    Issue: Ban__proto__? Do something funky?

    Object pattern execution order is as follows:

    1. For each non-rest object pattern clausekey: sub-pattern, in source order:
      1. Check that the subject has the propertykey (usingin, orHasProperty(), semantics). If it doesn't, return failure.
      2. Get the value of thekey property, and match it againstsub-pattern. If that fails to match, return failure.
    2. If there's a rest pattern clause,collect all enumerable own properties of the subjectthat weren't tested in the previous step,and put them into a freshObject.Match that against the rest pattern.If that fails, return failure.
    3. Return success.

    Examples

    Object Pattern Caching

    Similar toarray pattern caching,object patterns cache their results over the scope of the match construct,so that multiple clauses don’t observably retrieve the same property multiple times.

    (Unlike array pattern caching,which is necessary for this proposal to work with iterators,object pattern caching is a nice-to-have.It does guard against some weirdness like non-idempotent getters(including, notably, getters that return iterators),and helps make idempotent-but-expensive getters usable in pattern matchingwithout contortions,but mostly it’s just for conceptual consistency.)

    Whenever a subject is matched against an object pattern,for each property name in the object pattern,a(<subject>, <property name>) tuple is used as the key in a cache,whose value is the value of the property.

    Whenever something would be matched against an object pattern,the cache is first checked,and if the subject and that property name are already in the cache,the value is retrieved from cache instead of by a fresh Get against the subject.

    For example:

    constrandomItem={getnumOrString(){returnMath.random()<.5 ?1 :"1";}};match(randomItem){when{numOrString:Number}:console.log("Only matches half the time.");// Whether the pattern matches or not,// we cache the (randomItem, "numOrString") pair// with the result.when{numOrString:String}:console.log("Guaranteed to match the other half of the time.");// Since (randomItem, "numOrString") has already been cached,// we reuse the result here;// if it was a string for the first clause,// it’s the same string here.}

    Issue: This potentially introduces a lot more caching,and the major use-case is just making sure that iterator cachingworks both at the top-level and when nested in an object.Expensive or non-idempotent getters benefit,but that's a much less important benefit.This cachingis potentially droppable,but it will mean that we only cache iterables at the top level.

    Extractor Patterns

    A dotted-ident followed by a parenthesized "argument list"containing the same syntax as anarray matcher.Represents a combination of acustom matcher patternand anarray pattern:

    1. The dotted-ident is resolved against the visible bindings.If that results in an object with aSymbol.customMatcher property,and the value of that property is a function,then continue;otherwise, this throws an XXX error.

    2. The custom matcher function is invoked with the subject as its first argument,and an object with the key"matchType" set to"extractor"as its second argument.Letresult be the return value.

    3. Matchresult against thearglist pattern.

    Note: Whilecustom matchers only require the return value betruthy orfalsey,extractor patterns are stricter about types:the value must beexactlytrue orfalse,or anArray,or an iterable.

    Arglist Patterns

    An arglist pattern is a sub-pattern of an Extractor Pattern,and is mostly identical to anArray Pattern.It has identical syntax,except it's bounded by parentheses (())rather than square brackets ([]).It behaves slightly differently with a few subjects, as well:

    • afalse subject always fails to match
    • atrue subject matches as if it were an empty Array
    • anArray subject is matched per-index,rather than invoking the iterator protocol.

    If the subject is anArray,then it's matched as follows:

    1. If the arglist pattern doesn't end in a rest pattern,then the subject'slength property must exactly equalthe length of the pattern,or it fails to match.

    2. If the arglist patterndoes end in a rest pattern,then the subject'slength property must be equal or greaterthan the length of the pattern - 1,or it fails to match.

    3. For each non-rest sub-pattern of the arglist pattern,the corresponding integer-valued property of the subject is fetched,and matched against the corresponding sub-pattern.

    4. If the final sub-pattern is a...<pattern>,collect the remaining integer-valued properties of the subject,up to but not including itslength value,into a fresh Array,and match against that pattern.

    5. If any of the matches failed,the entire arglist pattern fails to match.Otherwise, it succeeds.

    Other than the above exceptions,arglist patterns are matchedexactly the same as array patterns.

    Issue: Do we cache arglists the same way we cache array patterns?

    Note: TheArray behavior hereis for performance, based on implementor feedback.Invoking the iterator protocol is expensive,and we don't want to discourage use of custom matcherswhen theby far expected usage patternis to just return anArray,rather than some more complex iterable.We're (currently) still sticking with iterator protocol for array matchers,to match destructuring,but could potentially change that.

    Examples

    classOption{constructor(){thrownewTypeError();}staticSome=classextendsOption{constructor(value){this.value=value;}map(cb){returnnewOption.Some(cb(this.value));}// etcstatic[Symbol.customMatcher](subject){if(subjectinstanceofOption.Some){return[subject.value];}returnfalse;}};staticNone=classextendsOption{constructor(){}map(cb){returnthis;}// Use the default custom matcher,// which just checks that the subject matches the class.};}letval=Option.Some(5);match(val){whenOption.Some(Stringandleta):console.log(`Got a string "${a}".`);whenOption.Some(Numberandleta):console.log(`Got a number${a}.`);whenOption.Some(...):console.log(`Got something unexpected.`);// Or `Option.Some`, either works.// `Option.Some()` will never match, as the return value// is a 1-item array, which doesn't match `[]`whenOption.None():console.log(`Operation failed.`);// or `Option.None`, either works  default:console.log(`Didn't get an Option at all.`)}

    Issue: We don't have an easy way to get access to the "built-in" custom matcher,so the above falls back to doing an instanceof test(rather than the technically more correct branding testthat the built-in one does).To work "properly" I'd have to define the class without a custom matcher,then pull off the custom matcher,save it to a local variable,and define a new custom matcher that invokes the original oneand returns the[subject.value] on success.That's a silly amount of work for correctness.

    Combinator Patterns

    Sometimes you need to match multiple patterns on a single value,or pass a value that matches any of several patterns,or just negate a pattern.All of these can be achieved with combinator patterns.

    And Patterns

    Two or more patterns, each separated by the keywordand.This represents a testthat the subject passesall of the sub-patterns.Any pattern can be(and in some cases must be, seecombining combinators)wrapped in parentheses.

    Short-circuiting applies; if any sub-pattern fails to match the subject,matching stops immediately.

    and pattern execution order is as follows:

    1. For each sub-pattern, in source order, match the subject against the sub-pattern. If that fails to match, return failure.
    2. Return success.

    Or Patterns

    Two or more patterns, each separated by the keywordor.This represents a testthat the subject passesat least one of the sub-patterns.Any pattern can be(and in some cases must be, seecombining combinators)wrapped in parentheses.

    Short-circuiting applies; if any sub-pattern successfully matches the subject,matching stops immediately.

    or pattern execution order is as follows:

    1. For each sub-pattern, in source order, match the subject against the sub-pattern. If that successfully matches, return success.
    2. Return failure.

    Note: As defined inBinding Behavior Details,abinding pattern in a failed sub-patterncan be overridden by a binding pattern in a later sub-patternwithout error.That is,[let foo] or {length: let foo} is validboth at parse-time and run-time,even tho thefoo binding is potentially initialized twice(given a subject like[1, 2]).

    Not Patterns

    A pattern preceded by the keywordnot.This represents a test that the subjectdoes not match the sub-pattern.The pattern can be(and in some cases must be, seecombining combinators)wrapped in parentheses.

    Combining Combinator Patterns

    Combinator patterns cannot be combined at the same "level";there is no precedence relationship between them.Instead, parentheses must be used to explicitly provide an ordering.

    That is,foo and bar or baz is a syntax error;it must be written(foo and bar) or bazorfoo and (bar or baz).

    Similarly,not foo and bar is a syntax error;it must be written(not foo) and barornot (foo and bar).

    Guard Patterns

    A guard pattern has the syntaxif(<expression>),and represents a test that the expression is truthy.This is an arbitrary JS expression,not a pattern.

    match expression

    match expressions are a new type of expressionthat makes use ofpatternsto select one of several expressions to resolve to.

    A match expression looks like:

    match(<subject-expression>){when<pattern>:<value-expression>;    when<pattern>:<value-expression>;    ...    default:<value-expression>;}

    That is, thematch head contains a<subject-expression>,which is an arbitrary JS expressionthat evaluates to a "subject".

    Thematch block contains zero or more "match arms",consisting of:

    • the keywordwhen
    • apattern
    • a literal colon
    • an arbitrary JS expression
    • a semicolon (yes, required)

    After the match arms,it can optionally contain default a "default arm",consisting of:

    • the keyworddefault
    • a literal colon
    • an arbitrary JS expression
    • a semicolon

    After obtaining the subject,each match arm is tested in turn,matching the subject against the arm's pattern.If the match is successful,the arm's expression is evaluated,and thematch expression resolves to that result.

    If all match arms fail to match,and there is a default arm,the default arm's expression is evaluated,and thematch expression resolves to that result.If there is no default arm,thematch expression throws aTypeError.

    Bindings

    The<subject-expression> is part of the nearest block scope.

    Each match arm and the default armare independent nested block scopes,covering both the pattern and the expression of the arm.(That is, different arms can't see each other's bindings,and the bindings don't escape thematch expression.Within each arm, they shadow the outer scope's bindings.)

    Examples

    match(res){when{ status:200,letbody, ...letrest}:handleData(body,rest);when{conststatus,destination:leturl}andif(300<=status&&status<400):handleRedirect(url);when{ status:500}andif(!this.hasRetried):do{retry(req);this.hasRetried=true;};default:throwSomething();}

    This example tests a "response" object against several patterns,branching based on the.status property,and extracting different parts from the response in each branchto process in various handler functions.


    match(command){when['go',letdirand('north'or'east'or'south'or'west')]:go(dir);when['take',/[a-z]+ball/and{letweight}:takeBall(weight);  default:lookAround()}

    This sample is a contrived parser for a text-based adventure game.

    The first match arm matches if the command is an array with exactly two items.The first must be exactly the string'go',and the second must be one of the given cardinal directions.Note the use of theand patternto bind the second item in the array todirusing abinding patternbefore verifying (using theor pattern)that it’s one of the given directions.

    The second match arm is slightly more complex.First, aregex pattern is usedto verify that the object stringifies to"something ball",then anobject patternsverifies that it has a.weight propertyand binds it toweight,so that the weight is available to the arm's expression.

    Statement vs Expression

    For maximum expressivity,thematch expression is an expression, not a statement.This allows for easy use in expression contextslikereturn match(val){...}.

    It can, of course, be used in statement context,as in the first example above.However, the match arms still contain expressions only.

    It isexpected that do-expressions will allowfor match arms to execute statements(again, as in the first example above).If that proposal does not end up advancing,a future iteration of this proposal will include some wayto have a match arm contain statements.(Probably just by inlining do-expr's functionality.)

    is operator

    Theis operator is a new boolean operator,of the form<subject-expression> is <pattern>.It returns a boolean result,indicating whether the subject matched the pattern or not.

    Bindings

    Bindings established in the pattern of anisare visible in the nearest block scope,as defined inBinding Patterns.

    This includes when used in the head of anif() statement:

    functionfoo(x){if(xis[lethead, ...letrest]){console.log(head,rest);}else{// `head` and `rest` are defined here,// but will throw a ReferenceError if dereferenced,// since if the pattern failed// the binding patterns must not have been executed.}}functionbar(x){if(xisnot{letnecessaryProperty}){// Pattern succeeded, because `x.necessaryProperty`// doesn't exist.return;}// Here the pattern failed because `x.necessaryProperty`// *does* exist, so the binding pattern was executed,// and the `necessaryProperty` binding is visible here.console.log(necessaryProperty);}

    When used in the head of afor(),the usual binding scopes apply:the bindings are scoped to thefor() head+block,and in the case offor-of,are copied to the inner per-iteration binding scopes.

    while anddo-while do not currently have any special scoping rulesfor things in their heads.We propose that they adopt the same rules asfor-of blocks:the head is in a new scope surrounding the rule,and its bindings are copied to a per-iteration scopesurrounding the{} block.For do-while,the bindings are TDZ on the first iteration,before the head is executed.

    Motivating examples

    Below are selected situations where we expect pattern matching will be widelyused. As such, we want to optimize the ergonomics of such cases to the best ofour ability.


    Validating JSON structure.

    Here's the simple destructuring version of the code,which does zero checks on the data ahead of time,just pulls it apart and hopes everything is correct:

    varjson={'user':['Lily',13]};var{user:[name,age]}=json;print(`User${name} is${age} years old.`);

    Destructuring with checks that everything is correct and of the expected shape:

    if(json.user!==undefined){varuser=json.user;if(Array.isArray(user)&&user.length==2&&typeofuser[0]=="string"&&typeofuser[1]=="number"){var[name,age]=user;print(`User${name} is${age} years old.`);}}

    Exactly the same checks, but using pattern-matching:

    if(jsonis{user:[Stringandletname,Numberandletage]}){print(`User${name} is${age} years old.`);}

    Matchingfetch() responses:

    constres=awaitfetch(jsonService)match(res){when{status:200,headers:{'Content-Length':lets}}:console.log(`size is${s}`);when{status:404}:console.log('JSON not found');when{letstatus}andif(status>=400):do{thrownewRequestError(res);}};

    More concise, more functional handling of Redux reducers (compare withthis same example in the Redux documentation):

    functiontodosReducer(state=initialState,action){returnmatch(action){when{type:'set-visibility-filter',payload:letvisFilter}:{ ...state, visFilter};when{type:'add-todo',payload:lettext}:{ ...state,todos:[...state.todos,{ text,completed:false}]};when{type:'toggle-todo',payload:letindex}:do{constnewTodos=state.todos.map((todo,i)=>{returni!==index ?todo :{          ...todo,completed:!todo.completed};});({        ...state,todos:newTodos,});}default:state// ignore unknown actions}}

    Concise conditional logic in JSX (viaDivjot Singh):

    <Fetchurl={API_URL}>{props=>match(props){when{loading}:<Loading/>;    when{leterror}: do{console.err("something bad happened");<Errorerror={error}/>};    when{letdata}:<Pagedata={data}/>;}}</Fetch>

    Possible future enhancements

    Void Patterns

    The keywordvoid is a patternthat always matches,and does nothing else.It's useful in structure patterns,when you want to test for the existence of a propertywithout caring what its value is.

    This is the most likely proposal to move back into the main proposal;it's pulled out solely because we want to make surethat it stays consistentwithVoid Bindings.

    async match

    If thematch construct appears inside a context whereawait is allowed,await can already be used inside it, just like insidedo expressions.However, just likeasync do expressions, there’s uses of being able to useawait and produce a Promise, even when not already inside anasync function.

    asyncmatch(awaitsubject){when{leta}:awaita;when{letb}:b.then(()=>42);  default:awaitsomethingThatRejects();}// produces a Promise

    Relational Patterns

    Currently there are patterns for expressing various types of equality,and kinda an instanceof (for custom matchers against a class).We could express more types of operator-based checks,like:

    match(val){when<10:console.log("small");when>=10and<20:console.log("mid");    default:"large";}

    Generally, all the binary boolean operators could be used,with the subject as the implicit LHS of the operator.

    (This would slightly tie our hands on future syntax expansions for patterns,but it's unlikely we'd everwant to reuse existing operatorsin a way that's different from how they work in expression contexts.)

    Default Values

    Destructuring can supply a default value with= <expr> which is used when akey isn’t present. Is this useful for pattern matching?

    Optional keys seem reasonable; right now they’d require duplicating the patternlike({a, b} or {a}) (b will be bound to undefined in the RHS if not present).

    Do we need/want full defaulting? Does it complicate the syntax to much to havearbitrary JS expressions there, without anything like wrapper characters todistinguish it from surrounding patterns?

    This would bring us into closer alignment with destructuring, which is nice.

    Destructuring enhancements

    Both destructuring and pattern matching should remain in sync, so enhancementsto one would need to work for the other.

    Integration withcatch

    Allow acatch statement to conditionally catch an exception, saving a level ofindentation:

    try{thrownewTypeError('a');}catchmatch(e){when RangeError: ...;when/^abc$/: ...;// unmatched, default to rethrowing e}

    Or possibly just allow anis check in the catch head:

    try{thrownewTypeError('a');}catch(eisRangeError){    ...}catch(eis/^abc$/){    ...}

    (In both cases, the name used for the subject automatically creates a binding,same ascatch (e) does today.)

    Chaining guards

    Some reasonable use-cases require repetition of patterns today, like:

    match(res){when{ status:200or201,letpages,letdata}andif(pages>1):handlePagedData(pages,data);when{ status:200or201,letpages,letdata}andif(pages===1):handleSinglePage(data);  default:handleError(res);}

    We might want to allow match constructs to be chained, where the child matchconstruct sees the bindings introduced in their parent clause, and which willcause the entire parent clause to fail if none of the sub-classes match.

    The above would then be written as:

    match(res){when{ status:200or201,letdata}match{when{pages:1}:handleSinglePage(data);when{pages:>=2andletpages}:handlePagedData(pages,data);};  default:handleError(res);// runs if the status indicated an error,// or if the data didn't match one of the above cases,// notably if pages == 0}

    Note the lack of a<subject-expression> in the child (justmatch {...}), tosignify that it’s chaining from thewhen rather than just being part anindependent match construct in the RHS (which would, instead, throw if none ofthe clauses match):

    match(res){when{ status:200or201,letdata}:match(res){when{ pages:1}:handleSinglePage(data);when{pages:>=2andletpages}:handlePagedData(pages,data);// just an RHS, so if pages == 0,// the inner construct fails to match anything// and throws a TypeError};  default:handleError(res);}

    The presence or absence of the separator colon also distinguishes these cases,of course.

    About

    Pattern matching syntax for ECMAScript

    Resources

    License

    Code of conduct

    Contributing

    Security policy

    Stars

    Watchers

    Forks


    [8]ページ先頭

    ©2009-2025 Movatter.jp