Description
An aspiration with pattern matching is to help when working with nullable types. A pattern can test an expression to see if it's not null
and, if not, bind the result to a variable whose type is now non-nullable.
Solving that problem well requires nice syntax.
Null-check patterns
The current proposal addresses this using null-check patterns. When the subpattern is a variable, it gives you a concise way to test for null
and bind the non-null
value to a variable:
int? maybeInt() => ...
switch (maybeInt()) {
case var n?:
print(n + 1); // "n" is non-nullable.
}
Or inside an if
:
if (maybeInt() case var n?) {
print(n + 1); // "n" is non-nullable.
}
The postfix ?
syntax is terse, but that seems to be the only thing it has going for it. I don't think anyone has liked it or understood it the first time they encountered it.
Null-check subpatterns
The powerful thing about null-check patterns is that you can have any kind of subpattern inside, not just a variable. But, in practice, this extra power is useless. All of the other kinds of patterns either do a type test or type assertion, so the null check becomes redundant if you wrap it around anything other than a variable.
<pattern> != null
patterns
@leafpetersen proposes we instead allow subpatterns on the left hand side of ==
patterns. That would look like:
if (maybeInt() case var n != null) {
print(n + 1); // "n" is non-nullable.
}
I think that's more approachable, but seeing a variable declaration inside an infix expression feels kind of strange to me. The precedence looks weird, almost like it's missing an =
after the variable name or something.
Let
Here's a strawman for another approach (mostly borrowed from Swift) that might work or might be a bad idea. It also might help in other ways:
- Provides a shorter syntax for declaring final local variables.
- Provides a shorter possibly more intuitive syntax for the "check if an expression is null" if-statement like form.
- Make it clearer in code where null might appear.
We introduce let
as another (contextual) keyword for declaring a variable with an inferred type. Like final
, the variable can't be assigned. Unlike final
and var
, the variable's inferred type is the non-null type of the initializer.
Let variable patterns
This means it can only be used in contexts where the initializer evaluating to null
can be gracefully be handled. The first obvious place is as another kind of variable pattern. Like other variable patterns, it matches when the incoming value has the variable's type. Since the variable's type is inferred as the non-null type of that value, it effectively does a null check and binds otherwise.
So the first example becomes:
switch (maybeInt()) {
case let n:
print(n + 1); // "n" is non-nullable.
}
You can read "let" as sort of "permissive" here. It means "if we actually did get a value".
Let in if
You could also use a let pattern in an if-case
statement, of course:
if (maybeInt() case let n) {
print(n + 1); // "n" is non-nullable.
}
This is very similar to Swift's if-let-case
. Like Swift, we could also support a more direct form of using let
inside the if
:
if (let n = maybeInt()) {
print(n + 1); // "n" is non-nullable.
}
Note that here we don't have any sort of general pattern on the left of the =
. It's a special form that is just let <var> = <expr>
.
Guard let
Also like Swift, we could support an inverted form where you have an else clause which must exit which is run when the expression is null. Maybe something like:
let n = maybeInt() else {
throw 'Oh no!';
}
Let variables
Since let
is shorter than final
, users who prefer single-assignment variables might want to use it for normal local variable declarations.
main() {
let hello = 'hi';
print(hello);
}
As long as the initializer's type is non-nullable, this can't fail, so we can safely allow it. And, in fact, now this code tells you something useful. By using let
, you know the variable is non-nullable even though you didn't write a type annotation. This can make it clearer where null
might flow through code using type inference on local variables.
In code that consistently uses let
for all non-nullable variables, then seeing var
or final
sends a signal that the variable may hold null
.
I'm hesitant to introduce a third keyword for variables, but maybe there's something to this? Thoughts? @mit-mit @lrhn @stereotype441 @natebosch @jakemac53 @kallentu