Skip to content

Swift-like guard statement #1548

Open
Open
@erf

Description

@erf

I would like a better way to handle optional/null values. I think Swift does this well using the guard statement.

I especially like this feature in order to unwrap optional values, and return early if null.

E.g.

String? name;
guard final nonNullName = name else {
    print("name is null. Cannot process");
    return;
}
print('name is $nonNullName');

Here are some more examples of how this works in Swift.

Activity

added
featureProposed language feature that solves one or more problems
on Mar 26, 2021
weenzeel

weenzeel commented on Mar 26, 2021

@weenzeel

I'm no expert and there may be edge cases, but do we need this in Dart? I think the way that the Dart compiler is able to analyze the code paths and promote nullable types to proved non-nullable types is quite elegant.

void doStuff([String? someString]) {

  if (someString == null) {
    return;
  }
  
  // OK to use nullable someString here.
  // Compiler knows we won't get here unless someString isn't null.
  
  print(someString.substring(0,10));
  // ...
  
}

void main() {

  doStuff('12345678901234567890');
  
}
erf

erf commented on Mar 26, 2021

@erf
Author

My experience is that the analyzer can't detect when you have a class or chains with nullable types, like this:

class MyClass {
  String? someString;
}

void testNull(MyClass myObj) {
  if (myObj.someString == null) {
    return;
  }
  // Error: Analyzer don't know that someString is not null
  print(myObj.someString.substring(0, 10));
}

If we had guard we could do:

void testNull(MyClass myObj) {
  guard final someString = myObj.someString else {
    return;
  }
  print(someString.substring(0, 10));
}

Or check for more complex conditions / chains like this:

void testNull(MyClass? myObj) {
  guard final someString = myObj?.someVariable?.someString else {
    return;
  }
  print(someString.substring(0, 10));
}
weenzeel

weenzeel commented on Mar 26, 2021

@weenzeel

You are correct that nullable fields aren't promoted. The reason given in the documentation is that the compiler can't prove that the field won't change its value between the time we check it and the time we use it.

The way to handle this today would be:

class MyClass {
  String? someString;
}

void testNull(MyClass myObj) {
  var myGuardedValue = myObj.someString;
  if (myGuardedValue == null) {
    return;
  }
  // Analyzer do know that my guarded value can't be null.
  print(myGuardedValue.substring(0, 10));
}

void main() {
  testNull(MyClass());
}

This is just a different syntax compared to Swift isn't it? I think the functionality is the same. I tried a small sample in Swift and I'm not allowed to do stuff with the real field without added ceremony there either. All I'm allowed to touch is a promoted local variable, just like in Dart.

erf

erf commented on Mar 26, 2021

@erf
Author

That works, but i rather not have to declare a new variable, on a new line to have the analyzer conclude it is not null. With a guard statement you could do it in a one-liner.

Not sure what you mean with the Swift example (please provide an example), but with the guard statement you would check in run-time and not only analyze the code before hand.

weenzeel

weenzeel commented on Mar 26, 2021

@weenzeel

As a layman it would be interesting to know why the compiler can't prove that the field won't be null though.

mateusfccp

mateusfccp commented on Mar 26, 2021

@mateusfccp
Contributor

That works, but i rather not have to declare a new variable, on a new line to have the analyzer conclude it is not null. With a guard statement you could do it in a one-liner.

As far as I could understand your proposal, what you are proposing is similar to #1201 and wouldn't bring any benefit in relation to it.

As a layman it would be interesting to know why the compiler can't prove that the field won't be null though.

Consider the following:

class A {
  final num? amount = 5;
}

class B implements A {
  var _returnInt = false;

  @override
  num? get amount {
     // Evil, but this is a simplistic example.
    _returnInt = !_returnInt;
    return _returnInt ? 5 : null;
  }
}

void main() {
  A a = B();
  if (a.amount == null) {
    print(a.amount.runtimeType);
  }
}

If the compiler deems a.amount as non-nullable in if (a.amount == null) it will be clearly incorrect. This doesn't happen only with null-safety but any kind of promotion. You may want to refer to field promotion label.

erf

erf commented on Mar 26, 2021

@erf
Author

@mateusfccp I was not aware of that proposal, and it looks like it could solve the simplest cases, but not sure if you be able to unwrap a chain of nullable types like this? Also it seem you must reuse the same variable name, so you can't asign a member of another type to that local variable.

  guard final myNonNullStr = myObj?.someVar?.someString else {
    // was not able to declare myNonNullStr, something was null
    return;
  }
  print(myNonNullStr);
erf

erf commented on Mar 26, 2021

@erf
Author

Maybe this guard solution could be related to destructuring or pattern matching.

lsegal

lsegal commented on Apr 10, 2021

@lsegal

As a layman it would be interesting to know why the compiler can't prove that the field won't be null though.

If the compiler deems a.amount as non-nullable in if (a.amount == null) it will be clearly incorrect.

@mateusfccp this seems just a little orthogonal to the original question which was specifically about fields. The example above is demonstrating the compiler's inability to determine nullness of a function. Perhaps my nomenclature is a bit off, but based on my understanding, "getter" functions sit atop the actual fields themselves, which means getters are not considered fields-- and presumably the compiler knows this?

If so, surely the compiler can detect when a function is accessed vs. a bare field, at which point presumably we should be able to promote fields without introducing any new syntaxes? Am I wrong about the distinction between fields/accessors?

It just seems pretty inconsistent to me for promotion to only work on variables in a local scope. Even global variables (which are by no means considered fields) do not get promoted?

image

This behavior breaks some fairly intuitive expectations around what is a variable and what is not. If we were legitimately dealing with method calls, sure, but we "know" (both intuitively and ideally provably so in Dart's AST) that x y and z above all return values in the exact same way.

lrhn

lrhn commented on Apr 11, 2021

@lrhn
Member

Dart getters are "functions" in the sense that they can do and return anything. Non-local variable declarations introduce getters which just return the content of the field.

The compiler might be able to detect that some getters won't actually change value between calls, and that it's therefore sound to promote a later access based on an earlier check. However, that is breaking the abstraction. It means that if you ever change any of the implementation details that the compiler used to derive this, it will stop promoting. Any such change becomes a breaking change.

Since you should always be able to change a non-local variable to a getter (and possibly setter) and vice versa, the only safe approach is to not promote non-local variables. It's not sound towards future supposedly non-breaking changes.

eernstg

eernstg commented on Apr 12, 2021

@eernstg
Member

@lrhn wrote:

Since you should always be able to change a non-local variable to a getter (and
possibly setter) and vice versa, the only safe approach is to not promote ...

I agree that we should support the encapsulation of properties (such that a non-local variable can be changed to a getter-&-maybe-a-setter). This ensures that the implementer of the property has a certain amount of freedom.

However, I don't see a problem in supporting a different contract with a different trade-off as well: The designer of the property could decide that the property is stable (#1518), which means that the associated getter must return the same value each time it is invoked. The implementer now has less freedom, but clients have more guarantees (in particular, such getters can be promoted). The loss of flexibility only affects variables/getters declared as stable, so it's fully controlled by the designer of each declaration, and there is no penalty for non-stable variables/getters.

// Assuming #1518.

import 'dart:math';

stable late int? x;

class Foo {
  stable late int? y = null;
  void run() {
    int? z = b ? 5 : null;
    if (x == null || y == null || z == null) return;
    print('${x + 1}, ${y + 1}, ${z + 1}'); // OK.
  }
}

bool get b => Random().nextBool();

void main() {
  x = b ? 2 : null;
  var foo = Foo();
  foo.y = b ? 3 : null;
  foo.run();
}
yuukiw00w

yuukiw00w commented on May 22, 2024

@yuukiw00w

I would like to emphasize not only the use of guard as a syntax for unwrapping null values but also the readability aspect of the code, where it is guaranteed that the scope will be exited if the condition is false, simply by seeing guard.
For instance, in Flutter, a common condition check is context.mounted.
If we could write it as guard context.mounted else {}, it would make it easier to recognize that context.mounted is true below the guard statement, which is a significant readability advantage.

// This is the current way of writing it
if !context.mounted {
  return;
}

// This way is more readable
guard context.mounted else {
  return;
}
ghost

ghost commented on May 22, 2024

@ghost

I would just say if (!context.mounted) return;. Looks quite readable to me.

To be honest, I don't understand the meaning of the verb "guard" here:

guard context.mounted else {
  return;
}

According to Webster, "to guard" means "to protect against damage or harm". What kind of harm? Who is protected by whom? Against what threat? And what "else" means in this context? 😄

17 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureProposed language feature that solves one or more problems

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @lsegal@munificent@erf@Albert221@mateusfccp

        Issue actions

          Swift-like guard statement · Issue #1548 · dart-lang/language