Author: Bob Nystrom
Status: In progress
Version 1.2 (see CHANGELOG at end)
Summary
This proposal (along with its smaller sister records proposal) covers a
family of closely-related features that address a number of some of the most
highly-voted user requests. It directly addresses:
- Multiple return values (495
👍 , 4th highest) - Algebraic datatypes (362
👍 , 10th highest) - Patterns and related features (379
👍 , 9th highest) - Destructuring (394
👍 , 7th highest) - Sum types and pattern matching (201
👍 , 11th highest) - Extensible pattern matching (69
👍 , 23rd highest) - JDK 12-like switch statement (79
👍 , 19th highest) - Switch expression (28
👍 ) - Type patterns (9
👍 ) - Type decomposition
(For comparison, the current #1 issue, Data classes has 824
In particular, this proposal covers several coding styles and idioms users
would like to express:
Multiple returns
Functions take not a single parameter but an entire parameter list because
you often want to pass multiple values in. Parameter lists give you a flexible,
ad hoc way of aggregating multiple values going into a function, but there is
no equally easy way to aggregate multiple values coming out. You’re left with
having to create a class, which is verbose and couples any users of the API to
that specific class declaration. Or you pack the values into a List or Map and
end up losing type safety.
Records are sort of like “first class argument lists” and give you a natural
way to return multiple values:
(double, double) geoCode(String city) { var lat = // Calculate... var long = // Calculate... return (lat, long); // Wrap in record and return. }
Destructuring
Once you have a few values lumped into a record, you need a way to get them
back out. Record patterns in variable declarations let you destructure a
record value by accessing fields and binding the resulting values to new
variables:
var (lat, long) = geoCode('Aarhus'); print('Location lat:$lat, long:$long');
List and map patterns let you likewise destructure those respective collection
types (or any other class that implements List
or Map
):
var list = [1, 2, 3]; var [a, b, c] = list; print(a + b + c); // 6. var map = {'first': 1, 'second': 2}; var {'first': a, 'second': b} = map; print(a + b); // 3.
Algebraic datatypes
You often have a family of related types and an operation that needs specific
behavior for each type. In an object-oriented language, the natural way to model
that is by implementing each operation as an instance method on its respective
type:
calculateArea()
operation is supported by all shapes by implementingthe method in each class. This works well for operations that feel closely tied
to the class, but it splits the behavior for the entire operation across many
classes and requires you to be able to add new instance methods to those
classes.
Some behavior is more naturally modeled with the operations for all types kept
together in a single function. Today, you can accomplish that using manual type
tests:
double calculateArea(Shape shape) { if (shape is Square) { return shape.length + shape.length; } else if (shape is Circle) { return math.pi * shape.radius * shape.radius; } else { throw ArgumentError("Unexpected shape."); } }
This works, but is verbose and cumbersome. Functional languages like SML
naturally group operations together like this and use pattern matching over
algebraic datatypes to write these functions. Class hierarchies can already
essentially model an algebraic datatype. This proposal provides the pattern
matching constructs to make working with that style enjoyable:
switch
.
Patterns
The core of this proposal is a new category of language construct called a
pattern. “Expression” and “statement” are both syntactic categories in the
grammar. Patterns form a third category. Like expressions and statements,
patterns are often composed of other subpatterns.
The basic idea with a pattern is that it:
-
Can be tested against some value to determine if the pattern matches. If
not, the pattern refutes the value. Some kinds of patterns, called
“irrefutable patterns” always match. -
If (and only if) a pattern does match, the pattern may bind new variables in
some scope.
Patterns appear inside a number of other constructs in the language. This
proposal extends Dart to allow patterns in:
- Top-level and local variable declarations.
- Static and instance field declarations.
- For loop variable declarations.
- Switch statement cases.
- A new switch expression form’s cases.
Binding and matching patterns
Languages with patterns have to deal with a tricky ambiguity. What does a bare
identifier inside a pattern mean? In some cases, you would like it declare a
variable with that name:
Here, a
and b
declare new variables. In other cases, you would like
identifiers to be references to constants so that you can refer to a constant
value in the pattern:
const a = 1; const b = 2; switch ([1, 2]) { case [a, b]: print("Got 1 and 2"); }
Here, a
and b
are references to constants and the pattern checks to see if
the value being switched on is equivalent to a list containing those two
elements.
This proposal follows Swift and addresses the ambiguity by
dividing patterns into two general categories binding patterns or binders
and matching patterns or matchers. (Binding patterns don’t always
necessarily bind a variable, but “binder” is easier to say than “irrefutable
pattern”.) Binders appear in irrefutable contexts like variable declarations
where the intent is to destructure and bind variables. Matchers appear in
contexts like switch cases where the intent is first to see if the value matches
the pattern or not and where control flow can occur when the pattern doesn’t
match.
Grammar summary
Before walking through them in detail, here is a short summary of the kinds of
patterns and where they can appear. There are three basic entrypoints into the
pattern grammar:
-
matcher
is the refutable patterns. -
binder
is the irrefutable patterns. -
declarationBinder
is a subset of the irrefutable
patterns that make sense as the outermost pattern in a variable declaration.
Subsetting allows reasonable code like:var [a, b] = [1, 2]; // List. var (a, b) = (1, 2); // Record. var {1: a} = {1: 2}; // Map.
While avoiding strange uses like:
var String str = 'redundant'; // Variable. var str as String = 'weird'; // Cast. var definitely! = maybe; // Null-assert.
These entrypoints are wired into the rest of the language like so:
-
A pattern variable declaration contains a
declarationBinder
. -
A switch case contains a
matcher
. -
A
declarationMatcher
pattern embeds a
declarationBinder
inside a refutable pattern. This is
a convenience to let you avoid repeatingvar
orfinal
several times
inside a matching pattern, as in:// List matcher containing three variable matchers: case [var a, var b, var c]: // Declaration matcher containing list binder // containing three variable binders: case var [a, b, c]:
These two cases both do the same thing.
Many kinds of patterns have both matcher (refutable) and binder (irrefutable)
forms. The table below shows examples of every specific pattern type and which
categories it appears in:
Type | Decl Binder? | Binder? | Matcher? | Examples |
---|---|---|---|---|
Record | Yes | Yes | Yes | (subpattern1, subpattern2) (x: subpattern1, y: subpattern2) |
List | Yes | Yes | Yes | [subpattern1, subpattern2] |
Map | Yes | Yes | Yes | {"key": subpattern} |
Wildcard | No | Yes | Yes | _ |
Variable | No | Yes | Yes | foo // Binder syntax. var foo // Matcher syntax. String str // Works in either. |
Cast | No | Yes | No | foo as String |
Null assert | No | Yes | No | subpattern! |
Literal | No | No | Yes | 123 , null , 'string' |
Constant | No | No | Yes | foo , math.pi |
Null check | No | No | Yes | subpattern? |
Extractor | No | No | Yes | SomeClass(subpattern1, x: subpattern2) |
Declaration | N/A | No | Yes | var [foo, bar] // == [var foo, var bar] var (a, b) // == (var a, var b) |
Syntax
This proposal introduces a number different pattern forms and several places in
the language where they can be used.
Going top-down through the grammar, we start with the constructs where patterns
are allowed and then get to the patterns themselves.
Pattern variable declaration
Most places in the language where a variable can be declared are extended to
allow a pattern, like:
var (a, [b, c]) = ("str", [1, 2]);
Dart’s existing C-style variable declaration syntax makes it harder to
incorporate patterns. Variables can be declared just by writing their type, and
a single declaration might declare multiple variables. Fully incorporating
patterns into that could lead to confusing syntax like:
(int, String) (n, s) = (1, "str"); final (a, b) = (1, 2), c = 3, (d, e);
To avoid this weirdness, patterns only occur in variable declarations that begin
with a var
or final
keyword. Also, a variable declaration using a pattern
can only have a single declaration “section”. No comma-separated multiple
declarations like:
Also, declarations with patterns must have an initializer. This is not a
limitation since the point of using a pattern in a variable declaration is to
immediately destructure the initialized value.
Add these new rules:
patternDeclaration ::=
| patternDeclarator declarationBinder '=' expression
patternDeclarator ::= 'late'? ( 'final' | 'var' )
TODO: Should we support destructuring in const
declarations?
And incorporate the new rules into these existing rules:
topLevelDeclaration ::=
| // Existing productions...
| patternDeclaration ';' // New.
localVariableDeclaration ::=
| initializedVariableDeclaration ';' // Existing.
| patternDeclaration ';' // New.
forLoopParts ::=
| // Existing productions...
| ( 'final' | 'var' ) declarationBinder 'in' expression // New.
// Static and instance fields:
declaration ::=
| // Existing productions...
| 'static' patternDeclaration // New.
| 'covariant'? patternDeclaration // New.
Switch statement
We extend switch statements to allow patterns in cases:
switchStatement ::= 'switch' '(' expression ')' '{' switchCase* defaultCase? '}'
switchCase ::= label* caseHead ':' statements
caseHead ::= 'case' matcher caseGuard?
caseGuard ::= 'if' '(' expression ')'
Allowing patterns in cases significantly increases the expressiveness of what
properties a case can verify, including executing arbitrary user-defined code.
This implies that the order that cases are checked is now potentially
user-visible and an implementation must execute the first case that matches.
Guard clauses
We also allow an optional guard clause to appear after a case. This enables
a switch case to evaluate a secondary arbitrary predicate:
This is useful because if the guard evaluates to false then execution proceeds
to the next case, instead of exiting the entire switch like it would if you
had nested an if
statement inside the switch case.
Implicit break
A long-running annoyance with switch statements is the mandatory break
statements at the end of each case body. Dart does not allow fallthrough, so
these break
statements have no real effect. They exist so that Dart code does
not appear to be doing fallthrough to users coming from languages like C that
do allow it. That is a high syntactic tax for limited benefit.
I inspected the 25,014 switch cases in the most recent 1,000 packages on pub
(10,599,303 LOC). 26.40% of the statements in them are break
. 28.960% of the
cases contain only a single statement followed by a break
. This means
break
is a fairly large fraction of the statements in all switches for
marginal benefit.
Therefore, this proposal removes the requirement that each non-empty case body
definitely exit. Instead, a non-empty case body implicitly jumps to the end of
the switch after completion. From the spec, remove:
If s is a non-empty block statement, let s instead be the last statement
of the block statement. It is a compile-time error if s is not abreak
,
continue
,rethrow
orreturn
statement or an expression statement where the
expression is athrow
expression.
This is now valid code that prints “one”:
switch (1) { case 1: print("one"); case 2: print("two"); }
Empty cases continue to fallthrough to the next case as before:
This prints “one or two”:
switch (1) { case 1: case 2: print("one or two"); }
Switch expression
When you want an if
statement in an expression context, you can use a
conditional expression (?:
). There is no expression form for multi-way
branching, so we define a new switch expression. It takes code like this:
Color shiftHue(Color color) { switch (color) { case Color.red: return Color.orange; case Color.orange: return Color.yellow; case Color.yellow: return Color.green; case Color.green: return Color.blue; case Color.blue: return Color.purple; case Color.purple: return Color.red; } }
And turns it into:
cases in a switch statement can share a set of statements. Can we support
that?
Slotting into primary
means it can be used anywhere any expression can appear
even as operands to unary and binary operators. Many of these uses are ugly, but
not any more problematic than using a collection literal in the same context
since a switch
expression is always delimited by a switch
and }
.
Making it high precedence allows useful patterns like:
single return statement or an assignment followed by a break so there is some
evidence this will be useful.
If-case statement
Often you want to conditionally match and destructure some data, but you only
want to test a value against a single pattern. You can use a switch
statement
for that, but it’s pretty verbose:
switch (json) { case [int x, int y]: return Point(x, y); }
We can make simple uses like this a little cleaner by introducing an if-like
form similar to if-case in Swift:
if (case [int x, int y] = json) return Point(x, y);
It may have an else branch as well:
if (case [int x, int y] = json) { print('Was coordinate array $x,$y'); } else { throw FormatException('Invalid JSON.'); }
The grammar is:
ifCaseStatement ::= 'if' '(' 'case' matcher '=' expression ')'
statement ('else' statement)?
The expression
is evaluated and matched against matcher
. If the pattern
matches, then the then branch is executed with any variables the pattern
defines in scope. Otherwise, the else branch is executed if there is one.
Unlike switch
, this form doesn’t allow a guard clause. Guards are important in
switch cases because, unlike nesting an if statement inside the switch case, a
failed guard will continue to try later cases in the switch. That is less
important here since the only other case is the else branch.
TODO: Consider allowing guard clauses here. That probably necessitates
changing guard clauses to use a keyword other than if
since if
nested inside
an if
condition looks pretty strange.
Irrefutable patterns (“binders”)
Binders are the subset of patterns whose aim is to define new variables in some
scope. A binder can never be refuted. To avoid ambiguity with existing variable
declaration syntax, the outermost pattern where a binding pattern is allowed is
somewhat restricted:
declarationBinder ::=
| listBinder
| mapBinder
| recordBinder
TODO: Allow extractBinder patterns here if we support irrefutable user-defined
extractors.
This means that the outer pattern is always some sort of destructuring pattern
that contains subpatterns. Once nested inside a surrounding binder pattern, you
have access to all of the binders:
binder
| declarationBinder
| wildcardBinder
| variableBinder
| castBinder
| nullAssertBinder
binders ::= binder ( ',' binder )* ','?
Type argument binder
Certain places in a pattern where a type argument is expected also allow you to
declare a type parameter variable to destructure and capture a type argument
from the runtime type of the matched object:
The typeOrBinder
rule is similar to the existing type
grammar rule, but also
allows final
followed by an identifier to declare a type variable. It allows
this at the top level of the rule and anywhere a type argument may appear inside
a nested type. For example:
first-class generic function types?
List binder
A list binder extracts elements by position from objects that implement List
.
...
element in order to match suffixes or ignore extraelements. Allow capturing the rest in a variable.
Map binder
A map binder access values by key from objects that implement Map
.
constant expressions. It is a compile-time error if any of the entry key
expressions evaluate to equivalent values.
Record binder
A record pattern destructures fields from a record.
recordBinder ::= '(' recordFieldBinders ')'
recordFieldBinders ::= recordFieldBinder ( ',' recordFieldBinder )* ','?
recordFieldBinder ::= ( identifier ':' )? binder
| identifier ':'
Each field is either a binder which destructures a positional field, or a binder
prefixed with an identifier and :
which destructures a named field.
When destructuring named fields, it’s common to want to bind the resulting value
to a variable with the same name. As a convenience, the binder can be omitted on
a named field. In that case, the field implicitly contains a variable binder
subpattern with the same name. These are equivalent:
var (first: first, second: second) = (first: 1, second: 2); var (first:, second:) = (first: 1, second: 2);
TODO: Allow a ...
element in order to ignore some positional fields while
capturing the suffix.
Wildcard binder
A wildcard binder pattern does nothing.
It’s useful in places where you need a subpattern in order to destructure later
positional values:
var list = [1, 2, 3];
var [_, two, _] = list;
Variable binder
A variable binding pattern matches the value and binds it to a new variable.
These often occur as subpatterns of a destructuring pattern in order to capture
a destructured value.
variableBinder ::= typeWithBinder? identifier
Cast binder
A cast pattern explicitly casts the matched value to the expected type.
castBinder ::= identifier "as" type
This is not a type test that causes a match failure if the value isn’t of the
tested type. This pattern can be used in irrefutable contexts to forcibly assert
the expected type of some destructured value. This isn’t useful as the outermost
pattern in a declaration since you can always move the as
to the initializer
expression:
num n = 1; var i as int = n; // Instead of this... var i = n as int; // ...do this.
But when destructuring, there is no place in the initializer to insert the cast.
This pattern lets you insert the cast as values are being pulled out by the
pattern:
(num, Object) record = (1, "s"); var (i as int, s as String) = record;
Null-assert binder
nullAssertBinder ::= binder '!'
When the type being matched or destructured is nullable and you want to assert
that the value shouldn’t be null, you can use a cast pattern, but that can be
verbose if the underlying type name is long:
pattern forcibly casts the matched value to its non-nullable type. If the value
is null, a runtime exception is thrown:
Refutable patterns determine if the value in question matches or meets some
predicate. This answer is used to select appropriate control flow in the
surrounding construct. Matchers can only appear in a context where control flow
can naturally handle the pattern failing to match.
matcher ::=
| literalMatcher
| constantMatcher
| wildcardMatcher
| listMatcher
| mapMatcher
| recordMatcher
| variableMatcher
| declarationMatcher
| extractMatcher
| nullCheckMatcher
Literal matcher
A literal pattern determines if the value is equivalent to the given literal
value.
literalMatcher ::=
| booleanLiteral
| nullLiteral
| numericLiteral
| stringLiteral
Note that list and map literals are not in here. Instead there are list and map
patterns.
Breaking change: Using matcher patterns in switch cases means that a list or
map literal in a switch case is now interpreted as a list or map pattern which
destructures its elements at runtime. Before, it was simply treated as identity
comparison.
const a = 1; const b = 2; var obj = [1, 2]; // Not const. switch (obj) { case [a, b]: print("match"); break; default: print("no match"); }
In Dart today, this prints “no match”. With this proposal, it changes to
“match”. However, looking at the 22,943 switch cases in 1,000 pub packages
(10,599,303 lines in 34,917 files), I found zero case expressions that were
collection literals.
Constant matcher
Like literals, references to constants determine if the matched value is equal
to the constant’s value.
constantMatcher ::= qualified ( "." identifier )?
The expression is syntactically restricted to be either:
-
A bare identifier. In this case, the identifier must resolve to a
constant declaration in scope. -
A prefixed or qualified identifier. In other words,
a.b
. It must
resolve to either a top level constant imported from a library with a
prefix, a static constant in a class, or an enum case. -
A prefixed qualified identifier. Like
a.B.c
. It must resolve to an
enum case on an enum type that was imported with a prefix.
To avoid ambiguity with wildcard matchers, the identifier cannot be _
.
TODO: Do we want to allow other kinds of constant expressions like 1 + 2
?
Wildcard matcher
A wildcard pattern always matches.
TODO: Consider giving this an optional type annotation to enable matching a
value of a specific type without binding it to a variable.
This is useful in places where a subpattern is required but you always want it
to succeed. It can function as a “default” pattern for the last case in a
pattern matching statement.
List matcher
Matches objects of type List
with the right length and destructures their
elements.
...
element in order to match suffixes or ignore extraelements. Allow capturing the rest in a variable.
Map matcher
Matches objects of type Map
and destructures their entries.
mapMatcher ::= mapTypeArguments? '{' mapMatcherEntries '}'
mapMatcherEntries ::= mapMatcherEntry ( ',' mapMatcherEntry )* ','?
mapMatcherEntry ::= expression ':' matcher
If it is a compile-time error if any of the entry key expressions are not
constant expressions. It is a compile-time error if any of the entry key
expressions evaluate to equivalent values.
Record matcher
Destructures fields from records and objects.
recordMatcher ::= '(' recordFieldMatchers ')'
recordFieldMatchers ::= recordFieldMatcher ( ',' recordFieldMatcher )* ','?
recordFieldMatcher ::= ( identifier ':' )? matcher
| identifier ':'
Each field is either a positional matcher which destructures a positional field,
or a matcher prefixed with an identifier and :
which destructures a named
field.
As with record binders, a named field without a matcher is implicitly treated as
containing a variable matcher with the same name as the field. The variable is
always final
. These cases are equivalent:
switch (obj) { case (first: final first, second: final second): ... case (first:, second:): ... }
TODO: Add a ...
syntax to allow ignoring positional fields?
Variable matcher
A variable matcher lets a matching pattern also perform variable binding.
variableMatcher ::= ( 'final' | 'var' | 'final'? typeWithBinder ) identifier
By using variable matchers as subpatterns of a larger matched pattern, a single
composite pattern can validate some condition and then bind one or more
variables only when that condition holds.
A variable pattern can also have a type annotation in order to only match values
of the specified type.
Declaration matcher
A declaration matcher enables embedding an entire declaration binding pattern
inside a matcher.
declarationMatcher ::= ( "var" | "final" ) declarationBinder
This is essentially a convenience over using multiple variable matchers. It
spares you from having to write var
or final
before every destructured
variable:
switch (obj) { // Instead of: case [var a, var b, var c]: ... // Can use: case var [a, b, c]: ... }
Extractor matcher
An extractor combines a type test and record destructuring. It matches if the
object has the named type. If so, it then uses the following record pattern to
destructure fields on the value as that type. This pattern is particularly
useful for writing code in an algebraic datatype style. For example:
class Rect { final double width, height; Rect(this.width, this.height); } display(Object obj) { switch (obj) { case Rect(var width, var height): print('Rect $width x $height'); case _: print(obj); } }
You can also use an extractor to both match an enum value and destructure
fields from it:
enum Severity { error(1, "Error"), warning(2, "Warning"); Severity(this.level, this.prefix); final int level; final String prefix; } log(Severity severity, String message) { switch (severity) { case Severity.error(_, prefix): print('!! $prefix !! $message'.toUppercase()); case Severity.warning(_, prefix): print('$prefix: $message'); } }
The grammar is:
extractMatcher ::= extractName typeArgumentsOrBinders? "(" recordFieldMatchers ")"
extractName ::= typeIdentifier | qualifiedName
It requires the type to be a named type. If you want to use an extractor with a
function type, you can use a typedef.
It is a compile-time error if extractName
does not refer to a type or enum
value. It is a compile-time error if a type argument list is present and does
not match the arity of the type of extractName
.
Null-check matcher
Similar to the null-assert binder, a null-check matcher provides a nicer syntax
for working with nullable values. Where a null-assert binder throws if the
matched value is null, a null-check matcher simply fails the match. To highlight
the difference, it uses a gentler ?
syntax, like the similar feature in
Swift:
nullCheckMatcher ::= matcher '?'
A null-check pattern matches if the value is not null, and then matches the
inner pattern against that same value. Because of how type inference flows
through patterns, this also provides a terse way to bind a variable whose type
is the non-nullable base type of the nullable value being matched:
String? maybeString = ... if (case var s? = maybeString) { // s has type String here. }
Static semantics
A pattern always appears in the context of some value expression that it is
being matched against. In a switch statement or expression, the value expression
is the value being switched on. In an if-case statement, the value is the result
of the expression to the right of the =
. In a variable declaration, the value
is the initializer:
Here, the (a, b)
pattern is being matched against the expression (1, 2)
.
When a pattern contains subpatterns, each subpattern is matched against a value
destructured from the value that the outer pattern is matched against. Here, a
is matched against 1
and b
is matched against 2
.
When calculating the context type schema or static type of a pattern, any
occurrence of typePattern
in a type is treated as Object?
.
Pattern context type schema
In a non-pattern variable declaration, the variable’s type annotation is used
for downwards inference of the initializer:
schema because there may be holes in the type:
var (a, int b) = ... // Schema is `(?, int)`.
Named fields in type schemas
Named record fields add complexity to type inference:
a
on the matched value. Since itbinds that to a variable of type
int
, ideally, that would fact would flowthrough inference to the right and infer
C()
for the initializer.However, there i
>>>>
Read More


Sign Up to Our Newsletter
Be the first to know the latest updates
Whoops, you're not connected to Mailchimp. You need to enter a valid Mailchimp API key.