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:
(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:
Here, the calculateArea() operation is supported by all shapes by implementing
the 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:
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:
As you can see, it also adds an expression form for 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:
declarationBinder is a subset of the irrefutable
patterns that make sense as the outermost pattern in a variable declaration.
Subsetting allows reasonable code like:
A declarationMatcher pattern embeds a declarationBinder inside a refutable pattern. This is
a convenience to let you avoid repeating var or final 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:casevar [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:
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.
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 a break, continue, rethrow or return statement or an expression statement where the
expression is a throw expression.
Empty cases continue to fallthrough to the next case as before:
This prints “one or two”:
switch (1) {
case1:case2: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:
TODO: This does not allow multiple cases to share an expression like empty
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:
Over half of the switch cases in a large corpus of packages contain either a
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]:returnPoint(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) returnPoint(x, y);
It may have an else branch as well:
if (case [int x, int y] = json) {
print('Was coordinate array $x,$y');
} else {
throwFormatException('Invalid JSON.');
}
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:
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:
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:
TODO: Can type patterns have bounds?
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:
TODO: Do we want to support function types? If so, how do we handle
first-class generic function types?
List binder
A list binder extracts elements by position from objects that implement List.
TODO: Allow a ... element in order to match suffixes or ignore extra
elements. Allow capturing the rest in a variable.
Map binder
A map binder access values by key from objects that implement Map.
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 binder
A record pattern destructures fields from a record.
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 asint= n; // Instead of this...var i = n asint; // ...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 asint, s asString) = 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:
To make that easier, similar to the null-assert expression, a null-assert binder
pattern forcibly casts the matched value to its non-nullable type. If the value
is null, a runtime exception is thrown:
Refutable patterns (“matchers”)
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.
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.
TODO: Allow a ... element in order to match suffixes or ignore extra
elements. Allow capturing the rest in a variable.
Map matcher
Matches objects of type Map and destructures their entries.
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.
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.
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.
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:casevar [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:
classRect {
finaldouble width, height;
Rect(this.width, this.height);
}
display(Object obj) {
switch (obj) {
caseRect(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:
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 (casevar 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:
Patterns extend this behavior:
To support this, every pattern has a context type schema. This is a type 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:
Here, the pattern is destructuring field a on the matched value. Since it
binds that to a variable of type int, ideally, that would fact would flow
through inference to the right and infer C() for the initializer.
However, there i
Whoops, you're not connected to Mailchimp. You need to enter a valid Mailchimp API key.
Our site uses cookies. Learn more about our use of cookies: cookie policyACCEPTREJECT
Privacy & Cookies Policy
Privacy Overview
This website uses cookies to improve your experience while you navigate through the website. Out of these cookies, the cookies that are categorized as necessary are stored on your browser as they are essential for the working of basic functionalities of the website. We also use third-party cookies that help us analyze and understand how you use this website. These cookies will be stored in your browser only with your consent. You also have the option to opt-out of these cookies. But opting out of some of these cookies may have an effect on your browsing experience.