-
Notifications
You must be signed in to change notification settings - Fork 213
Completer.complete() signature is not null-safe-friendly #1299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
cc @natebosch @lrhn @munificent this is pretty painful. Unfortunately, I imagine this would be massively breaking to fix by making the argument required. What if we made the argument type
|
Yes it would be.
This would at least warn about |
fwiw this was the actual case I ran into. The example above was a distillation of that. |
We have a number of functions which allow you to omit the argument and default to I'll look into whether using a default sentinel value makes sense in some cases. The completer example here seems to work because the sentinel can implement |
What if this:
actually behaved differently depending on whether |
@mraleph That would be a nice language feature. We have the problem that this function is perfectly valid already because it's abstract. It doesn't say how it plans to implement that (likely a default value, but you never know). We can't see from the abstract function signature whether the invocation is allowed or not because we can't see whether it has a default value. I suggested (somewhere) to allow a (generic or in a generic interface) function type to declare a default value type, and allow potentially unsafe default values and default values types, so you'd write: void complete([FutureOr<T> value = Null]); // abstract functions have default value types
/// and, having that type,
void Complete([FutureOr<T> value = null]) { ... } // concrete functions have concrete default values and then an invocation of a generic function with an unsafe default value type for an omitted argument would only be valid if the type argument makes the default value valid. That also allows you to have unsound defaults other than |
This particular case is actually simpler than it looks. We can just change the signature of Even if you tear off the function, you can't tell the difference because it's already covariant by generic, so the run-time type is Looking at this actually suggests to me that there is a use-case for having an optional argument with a tighter bound than the what the default value requires. So, a abstract class C {
factory C() = D;
void foo([int x]);
}
class _D implements C {
void foo([int? x = null]) { ... }
} Maybe we should be able to do that in a single declaration as well. Say, if you can set the internal type of a parameter using class C {
void foo([int x as int? = null]) { ... }
} (Then we could allow the |
Yes, it would be handy to be able to express "this argument is optional, but if it is specified, it needs be type X (e.g. FutureOr)" |
@tvolkert You can have optional and non-nullable parameters now, but only if you can provide a default value within that type. It's not possible to have a concrete method with an optional parameter with a restricted type, and a default value outside of that type. (And if that type happens to be a type variable, there is no way to create a sentinel value within the type). In some situations, you can cheat. |
Yep. I was saying that a way in the language to differentiate between an argument being specified vs unspecified could be handy, but the cognitive overhead it would cause is probably not worth it. |
About detecting whether an optional parameter was passed as an argument or not ... we tried that, and it made forwarding arguments more complicated. It needs to be combined with a way to specify "no argument" conditionally, so you can do foo({int x}) => bar(x: x::wasPassed ? x : __nothing__); (or some much better syntax) in order to be able to wrap functions and forward arguments without having an exponential number of cases in the number of optional parameters). My idea of allowing We could take a hint from list literals and allow: |
As a comeback to this, would it be possible to instead deprecate Especially now that we have |
It's such a good name! I'd rather have the option to "deprecate optionality", maybe just Maybe we should have other deprecation-for-change markers, instead of just the current "will be removed" variant. |
That sounds like a good addition. And it could be applied to many things, like |
This just bit someone internally via
|
(Change the parameter and argument passing system so that nullable parameters are optional, and vice versa, and change |
Outside of language level changes, this is my preference. If we have the same foot-gun in other APIs we could wrap them up in one lint. Maybe we could add a lint for now, and if we get new language features to express this directly we could switch to that. If we do think it's plausible we'll get new language features in this area I'd prefer not to make the breaking change. |
I think it will always be in principle breaking, since the goal is to turn current (potential) runtime errors into static errors. So for example, making the parameter type optional but non-nullable would cause a static error in cases that are currently potential (but not actual) runtime errors, because the value passed is never null. |
True. I should have used a more loose definition like non-breaking for code which didn't trigger the proposed lint. What I hope to avoid if we can, is requiring that all |
@leafpetersen: are you proposing this for Looking way up-thread, I really resonate w/ @tvolkert's observation:
Not being able to express this has bitten us a lot. If the proposal is for |
Unfolding an idea which has nearly been explored above (maybe I just missed it): Conditional default values. Any optional formal parameter can have a conditional default value, An invocation where a formal parameter with an optional default value is omitted is an error, unless the static type of the default value is assignable to the type of the parameter. In the case where the enclosing function declaration is generic, this means that some actual type arguments will cause the parameter to have a default value, others will cause it to not have a default value (and in that case it is an error to omit the parameter). A conditional default value is associated with a dynamic check (such that there is no doubt about the soundness), generally of the form For example: void f<X>([X x ?= null]) {}
X g<X extends num>([X x ?= 1.5]) => x;
// Conditional default values have no context type, so we may need to specify type arguments.
X h<X>(List<X> xs ?= const <String>['Hello!']) => xs[0];
void main() {
f(); // `f<dynamic>(null)`, OK.
f<int>(); // Compile-time error, `Null` is not assignable to `int`.
f<int?>(); // OK.
(f as Function)<int>(); // Run-time error.
(f as Function)<int?>(); // OK.
g(5); // OK.
g(); // OK, `g<num>(1.5)`.
int i = g(); // Compile-time error.
void local<S extends num>() {
S s = g(); // Compile-time error, `double` not assignable to `S`.
}
h(); // OK, `h<String>(const <String>['Hello!'])`.
List<Object> os = h(); // OK, `h<Object>(const <String>['Hello!'])`.
} Of course, it is tempting to say that we can have multiple conditional default values, and then we'll select the first one that has an assignable type for statically checked invocations; but it is not obvious how to deal with dynamic invocations, and it's probably too twisted to carry its own weight. A named optional parameter with a conditional default value just turns into a required named parameter when the typing does not match up. A positional optional parameter with a conditional default value will force all earlier positional parameters to be required when the typing does not match up. I think it could work. |
@eernstg I remember specifying something like this too, so I definitely think it can work. You can drop the It requires including the default value's type in the function type as well, so something like; void Function<X extend Foo>([X x ?= Bar]) is a function type for a function which takes a type argument, and which has a default value of type A Since the feature affects the type system, it's probably a "big" feature. |
Yes, If we decide that this feature is really, really desirable, it would be useful to take a look at it, in order to see how big it actually is. |
Are there any really compelling use cases for anything other than |
More broadly, for some definition of broadly. The example that came up internally was with |
Consider the following definition of
Completer.complete()
:Now look at the following code:
When running in sound null-safety, that code will produce no static analysis errors, compile cleanly, and throw a runtime error when it hits the call to
completer.complete(null)
. This is highly counterintuitive and confusing to developers (at least it was to me when I ran into it).Upon inspection, it's clear that this is clean to analysis because the completion value argument is optional. While this signature made sense in a pre-null-safety world, it's just going to be a source of errors when running with null safety.
I think we should consider a language change to fix this.
The text was updated successfully, but these errors were encountered: