Skip to content

Let #2703

Open
Open
Let#2703
@munificent

Description

@munificent

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    patternsIssues related to pattern matching.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions