Skip to content

Commit 9a31f3f

Browse files
committed
[patterns] Add null-assert and null-check patterns.
1 parent 5c43fbd commit 9a31f3f

File tree

1 file changed

+98
-3
lines changed

1 file changed

+98
-3
lines changed

working/0546-patterns/patterns-feature-specification.md

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,7 @@ binder
513513
| wildcardBinder
514514
| variableBinder
515515
| castBinder
516+
| nullAssertBinder
516517
517518
binders ::= binder ( ',' binder )* ','?
518519
```
@@ -681,6 +682,30 @@ pattern:
681682
var (i as int, s as String) = record;
682683
```
683684

685+
#### Null-assert binder
686+
687+
```
688+
nullAssertBinder ::= binder '!'
689+
```
690+
691+
When the type being matched or destructured is nullable and you want to assert
692+
that the value shouldn't be null, you can use a cast pattern, but that can be
693+
verbose if the underlying type name is long:
694+
695+
```dart
696+
(String, Map<String, List<DateTime>?>) data = ...
697+
var (name, timeStamps as Map<String, List<DateTime>>) = data;
698+
```
699+
700+
To make that easier, similar to the null-assert expression, a null-assert binder
701+
pattern forcibly casts the matched value to its non-nullable type. If the value
702+
is null, a runtime exception is thrown:
703+
704+
```dart
705+
(String, Map<String, List<DateTime>?>) data = ...
706+
var (name, timeStamps!) = data;
707+
```
708+
684709
### Refutable patterns ("matchers")
685710

686711
Refutable patterns determine if the value in question matches or meets some
@@ -699,6 +724,7 @@ matcher ::=
699724
| variableMatcher
700725
| declarationMatcher
701726
| extractMatcher
727+
| nullCheckMatcher
702728
```
703729

704730
#### Literal matcher
@@ -846,6 +872,9 @@ By using variable matchers as subpatterns of a larger matched pattern, a single
846872
composite pattern can validate some condition and then bind one or more
847873
variables only when that condition holds.
848874

875+
A variable pattern can also have a type annotation in order to only match values
876+
of the specified type.
877+
849878
#### Declaration matcher
850879

851880
A declaration matcher enables embedding an entire declaration binding pattern
@@ -930,7 +959,31 @@ It is a compile-time error if `extractName` does not refer to a type or enum
930959
value. It is a compile-time error if a type argument list is present and does
931960
not match the arity of the type of `extractName`.
932961

933-
**TODO: Some kind of terse null-check pattern that matches a non-null value?**
962+
#### Null-check matcher
963+
964+
Similar to the null-assert binder, a null-check matcher provides a nicer syntax
965+
for working with nullable values. Where a null-assert binder *throws* if the
966+
matched value is null, a null-check matcher simply fails the match. To highlight
967+
the difference, it uses a gentler `?` syntax, like the [similar feature in
968+
Swift][swift null check]:
969+
970+
[swift null check]: https://docs.swift.org/swift-book/ReferenceManual/Patterns.html#ID520
971+
972+
```
973+
nullCheckMatcher ::= matcher '?'
974+
```
975+
976+
A null-check pattern matches if the value is not null, and then matches the
977+
inner pattern against that same value. Because of how type inference flows
978+
through patterns, this also provides a terse way to bind a variable whose type
979+
is the non-nullable base type of the nullable value being matched:
980+
981+
```dart
982+
String? maybeString = ...
983+
if (case var s? = maybeString) {
984+
// s has type String here.
985+
}
986+
```
934987

935988
## Static semantics
936989

@@ -1036,12 +1089,19 @@ The context type schema for a pattern `p` is:
10361089
be used to downcast from any other type.*
10371090
* Else it is `?`.
10381091

1039-
* **Cast binder**, **wildcard matcher**, or **extractor matcher**: The
1040-
context type schema is `Object?`.
1092+
* **Cast binder**, **wildcard matcher**, or **extractor matcher**: The context
1093+
type schema is `Object?`.
10411094

10421095
**TODO: Should type arguments on an extractor create a type argument
10431096
constraint?**
10441097

1098+
* **Null-assert binder** or **null-check matcher**: A type schema `E?` where
1099+
`E` is the type schema of the inner pattern. *For example:*
1100+
1101+
```dart
1102+
var [[int x]!] = [[]]; // Infers List<List<int>?> for the list literal.
1103+
```
1104+
10451105
* **Literal matcher** or **constant matcher**: The context type schema is the
10461106
static type of the pattern's constant value expression.
10471107
@@ -1218,6 +1278,22 @@ The static type of a pattern `p` being matched against a value of type `M` is:
12181278
The static type of `p` is `Object?`. *Wildcards accept all types. Casts and
12191279
extractors exist to check types at runtime, so statically accept all types.*
12201280
1281+
* **Null-assert binder** or **null-check matcher**:
1282+
1283+
1. If `M` is `N?` for some type `N` then calculate the static type `q` of
1284+
the inner pattern using `N` as the matched value type. Otherwise,
1285+
calculate `q` using `M` as the matched value type. *A null-assert or
1286+
null-check pattern removes the nullability of the type it matches
1287+
against.*
1288+
1289+
```dart
1290+
var [x!] = <int?>[]; // x is int.
1291+
```
1292+
1293+
2. The static type of `p` is `q?`. *The intent of `!` and `?` is only to
1294+
remove nullability and not cast from an arbitrary type, so they accept a
1295+
value of its nullable base type, and not simply `Object?`.*
1296+
12211297
* **Literal matcher** or **constant matcher**: The static type of `p` is the
12221298
static type of the pattern's value expression.
12231299
@@ -1265,6 +1341,9 @@ patterns binds depend on what kind of pattern it is:
12651341
declaration or declaration matcher has a `final` modifier. The variable is
12661342
late if it is inside a pattern variable declaration marked `late`.
12671343
1344+
* **Null-assert binder** or **null-check matcher**: Introduces all of the
1345+
variables of its subpattern.
1346+
12681347
* **Declaration matcher**: The `final` or `var` keyword establishes whether
12691348
the binders nested inside this create final or assignable variables and
12701349
then introduces those variables.
@@ -1550,6 +1629,14 @@ To match a pattern `p` against a value `v`:
15501629
2. Otherwise, bind the variable's identifier to `v`. The match always
15511630
succeeds (if it didn't throw).
15521631

1632+
* **Null-assert binder**:
1633+
1634+
1. If `v` is null then throw a runtime exception. *Note that we throw even
1635+
if this appears in a refutable context. The intent of this pattern is to
1636+
assert that a value *must* not be null.*
1637+
1638+
2. Otherwise, match the inner pattern against `v`.
1639+
15531640
* **Literal matcher** or **constant matcher**: The pattern matches if `o == v`
15541641
evaluates to `true` where `o` is the pattern's value.
15551642

@@ -1571,6 +1658,12 @@ To match a pattern `p` against a value `v`:
15711658
3. Otherwise, match `v` against the subpatterns of `p` as if it were a
15721659
record pattern.
15731660

1661+
* **Null-check matcher**:
1662+
1663+
1. If `v` is null then the match fails.
1664+
1665+
2. Otherwise, match the inner pattern against `v`.
1666+
15741667
**TODO: Update to specify that the result of operations can be cached across
15751668
cases. See: https://github.com/dart-lang/language/issues/2107**
15761669

@@ -1611,6 +1704,8 @@ main() {
16111704

16121705
- Allow extractor patterns to match enum values.
16131706

1707+
- Add null-assert binder `!` and null-check `?` matcher patterns.
1708+
16141709
### 1.1
16151710

16161711
- Copy editing and clean up.

0 commit comments

Comments
 (0)