-
Notifications
You must be signed in to change notification settings - Fork 209
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Swift-like guard statement #1548
Comments
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');
} |
My experience is that the analyzer can't detect when you have a class or chains with nullable types, like this:
If we had
Or check for more complex conditions / chains like this:
|
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. |
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. |
As a layman it would be interesting to know why the compiler can't prove that the field won't be null though. |
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.
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 |
@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); |
Maybe this guard solution could be related to destructuring or pattern matching. |
@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? 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 |
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. |
@lrhn wrote:
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 // 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();
} |
I would like to emphasize not only the use of // This is the current way of writing it
if !context.mounted {
return;
}
// This way is more readable
guard context.mounted else {
return;
} |
I would just say 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? 😄 |
I think that void hello() {
guard (context.mounted) else {
return;
}
// This is being "guarded"
// ...
}
I'd argue final map = <String, Object?>{
'name': 'John',
};
guard (map case {'name': final String name}) else {
return;
}
// 'name' is usable here...
print(name); // 'John' |
There's only one use case for guard-like construct that makes sense to me: when the condition contains declarations of variables ( If Examples: foo() {
if! (obj case A(:int x)) {
print("no match");
return;
}
print ("match");
use(x); // available here
}
bar() {
if! (obj case A(:int x)) {
print("no match");
return;
} else {
print ("match");
use(x); // available here
}
use(x); // error, x is not defined here
} |
And a class is "a body of students meeting regularly to study the same subject" :P While I agree |
I looked into the wikipedia article. The term "guard" is defined loosely as a condition that "guards" the entrance to some execution branch.
So it's the same type of "guard" that dart uses for similar purposes - see https://dart.dev/language/branches#guard-clause
|
I would love to have |
I don't mind if the language feature with the same meaning as |
There's another good keyword: on (x == null) return;
on !(obj case A(:int x)) {
print("no match");
return;
} The problem is that "on" is not quite a keyword (It can be used as an identifier). |
I came from #3865 and, if you ask me, it feels like this issue should be rewritten as "let AFAIK what OP's asking for is almost obtainable via pattern match, but he's asking for some syntactic sugar, whereas in #3865 we'd like to obtain a negatable match. Say we have abstract class X {
int get a;
int get b;
int get c;
} And then, maybe, we want a particular condition to return prematurely as OP asked, while also capturing a "valid value". X? something = ...;
if (something case! !=null && X(a: > 9, b: < 10, c: 2)) {
throw AssertionError('this should *not* happen');
}
// use `something` which is now non-nullable and it's also been "validated" This would be even more flexible and readable if X? something = ...;
final isValid = something case !=null && X(a: > 9, b: < 10, c: 2);
if (!isValid) throw AssertionError('this should *not* happen'); So OP's problem is solved: Of course I have no clue if this is even possible, e.g. would this usage of |
I just stumbled on another usecase for negating a pattern. Say we want to validate a The three errors are semantically different and should be handled accordingly. void parse(Map<String, Object?> jsonResponse) {
final token = jsonResponse['token'];
if (token == null) throw Exception("invalid token, either not sent or null");
final result = switch(jsonResponse) {
{'some': final shape} => shape,
_ => throw CheckedFromJsonException(...),
};
return result;
} Turns out (AFAIK!) there's no "easy" way to do this with patterns. switch (jsonResponse) {
case {'token': != null}: // happy path
print("token field has been sent and it's not null");
print("in here I can further match on jsonResponse's shape");
case {'token': null}:
print('token field has been sent, but has been explicitly set to null');
throw Exception("invalid token, it's null");
case final map when map['token'] == null:
print('token field has *not* been sent at all');
throw Exception("invalid token, not sent");
default:
throw CheckedFromJsonException(...)
} With a negatable pattern I could just write: if (jsonResponse case! {'token': != null}) {
throw Exception('invalid token');
} And I'd be good to go towards parsing. |
I'll have to think more about negatable patterns, but as far as your example goes, I think you could write it like: Object? parse(Map<String, Object?> jsonResponse) {
return switch (jsonResponse) {
_ when !jsonResponse.containsKey('token') =>
throw Exception("invalid token, not sent"),
{'token': null} =>
throw Exception("invalid token, it's null"),
{'some': final shape} => shape,
_ => throw CheckedFromJsonException(...),
}
} |
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.
Here are some more examples of how this works in Swift.
The text was updated successfully, but these errors were encountered: