-
Notifications
You must be signed in to change notification settings - Fork 213
NNBD, non-nullable named parameters with defaults, and wrapping. #577
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
There are three additional arguments that I see against this approach. First, it makes removing the default value on the original function a breaking change. Second, it forces you to make the wrapping function arguments nullable. Third, if the original function arguments were nullable, then the wrapping function behaves differently from the original function. original({int? a : 0}) {
print(a);
}
wrapper({int? a}) => original(a: a ?? default);
void main() {
original(a: null); // prints "null"
wrapper(a: null); // prints "0"
} |
Fair point, that would usually be breaking anyways but it would move to a compile time error instead of a runtime one. That could be good or bad, depending who you ask :D.
Yep - this was the con I listed :). A more in depth feature could clean this up, but within the context of the wrapper function it really is nullable so I think it actually makes sense. Otherwise the defaulting would have to actually happen when you call the
Good point - it is a weird edge case but totally valid. |
I agree that this is an annoying problem with the language. It was a problem before NNBD and it continues to be a problem with NNBD.
Yeah, to me, this is one of the real problems with the proposed solution. I think the cleaner solution is to just keep using the solution you use before NNBD: assign the default value in the body: void original({int? a, int? b}) {
a ??= 0;
b ??= 1;
}
void wrappedWithDefault({int? a, int? b}) => original(a: a, b: b); This does mean that the parameters are nullable in |
Yes, I agree that barring any changes here nullable named parameters are probably all I will ever use (when I have a default at least). That matches how we do this always today, and this will help enforce the defaulting so it would be a small win still. It is a little bit sad to end up there, but acceptable. |
There is no reason to restrict this feature to named parameters, it should work for any parameter with a default value. This feature even allows you to leak the default value of a function argument: foo([Object x = const _SecretToken()]) { ... };
...
Object leaker() {
Object o;
try {
foo(((o = default) == null && false) || throw o);
} on Object { }
return o;
} Maybe There have been earlier proposals about have suggested a new "special value" which, when uses as an argment, is equivalent to not passing an argument. Then it would be up to the receiver to fill in the default value when called instead of letting the call-site see the value. That's probably safer, but has some interesting edge cases like |
Correct, this should also be extended to support positional parameters with defaults.
Ya, this likely removes a lot of potential unintended uses of this feature, I would be perfectly fine with that.
Right, that is a little bit less flexible but I would be in favor of it, I don't think apis that treat Null as a meaningful value are idiomatic Dart anyways. |
What about introducing an ‘undefined’ value? |
Here's the proposal that I usually mention when this comes up. ;-) If we wish to enable consistent and maintainable propagation of default values for formal parameters in forwarding functions, those default values should be denotable. Let's assume the syntax Then we could use (We don't need It is a compile-time error to refer to a default value that does not exist (in particular, when there is no named parameter with that name, or when the parameter exists but has a non-nullable type and no declared default value). The original example would then be expressed as follows: void original({int a: 0, int b: 1}) {}
void wrappedWithCopiedDefaults(
{int a: original.default[#a], int b: original.default[#b]}) =>
original(a: a, b: b); It's a bit verbose, and it would break if the named parameters are renamed, but that's breaking anyway; or if a default value is removed, but that would require all call sites to be updated if they omit said parameter, so why wouldn't the forwarding call site need to be revisited as well? Besides, we could let void wrappedWithCopiedDefaults({int a: default, int b: default}) =>
original(a: a, b: b); This mechanism doesn't force the parameter types to have any particular properties (they can be nullable or non-nullable as you wish, as long as the forwarding call can pass the type checks). Next, this design avoids the clash between "null as a dynamic request for a default value, and null as a proper argument", which always comes up in these discussions. Here is the example from @leafpetersen, rewritten to use explicitly denotable default values: original({int? a : 0}) {
print(a);
}
wrapper({int? a = default}) => original(a: a);
void main() {
original(a: null); // prints "null".
wrapper(a: null); // prints "null", so the discrepancy is gone.
} With respect to the original goals, we have the following: Pros:
For the cons, the situation differs:
Finally there was the issue about leaking default values. We can already leak defaults of instance methods: // LIBRARY 'widely_used_stuff.dart'.
// This is the argument type, available to the public.
class A {}
class _Secret implements A {
const _Secret();
}
const _secret = _Secret();
class B {
void f([A a = _secret]) {
// Here, we want to use `a == _secret` as evidence that the
// actual argument for `a` was omitted.
}
}
// LIBRARY 'my_program.dart'.
// Let's get access to `B.f.default[0]`.
class Leaker implements B {
noSuchMethod(Invocation i) => i.positionalArguments[0];
}
main() {
A mySecret = Leaker().f() as dynamic;
print(mySecret.runtimeType); // Prints '_Secret'.
} However, I'm not convinced that leaking a secret object which is used as a default in order to indicate that "this argument was omitted" is a huge problem. You can pass it explicitly in order to cheat and pretend that the argument was omitted, and you can forward it (implicitly, if you receive it in an invocation of the forwarder). But where's the exploit? |
If we look only at the "non-nullable with default value" case. // User write:
void original({int a = 0, int b = 1}) {
//...
}
// Automatically becomes:
void original({int? a, int? b}) {
int a = a ?? 0; // shadows the parameter
int b = b ?? 1;
//...
} The parameter becomes nullable from the outside and null means "use the default value". Since the parameter is intentionally declared as non-nullable, there is never a conflict that null could be a "proper argument". It changes the signature of the function but as a developer it seems totally understandable to me. |
I think the desugaring to In particular, we couldn't use an approach that requires the parameter type to be non-nullable if the parameter type is a type variable |
Is it possible for a parameter to be of a type variable without bound but have a default value? void original<T>({T frobnicator = ???}) {} |
That's right, we cannot construct a constant expression with type However, we might want to allow abstract methods to omit such an "impossible" default value (cf. #655), which would allow for abstract instance method declarations like |
We won't have this for the NNBD release, but leaving it open for future consideration. |
will this be in Dart 3? :-) |
No current plans to change this for Dart 3. (Read: Not a chance, we're too late in the 3.0 process to add more features.) |
Problem statement
With NNBD we will now have static checking that non-nullable named parameters can never contain null, which is great generally 🎉.
This does present a problem however when trying to wrap a function with non-nullable optional named parameters that have default values, especially if you want those same named parameters in your own function.
Here are a couple examples of how you could do this with NNBD as it stands now:
Neither of these are satisfactory:
Proposed solution
Credit to @jodinathan for the original idea here (from gitter).
Allow
default
as a value for named arguments. This would be compile time syntactic sugar only and would translate to copying the value of the default from the underlying named parameter. If the default value is not statically known then it would be a static error to usedefault
.default
is already a reserved word (so it can be used in switch statements) so there should be no concern with using that name.Example usage:
Pros:
Cons:
The text was updated successfully, but these errors were encountered: