-
Notifications
You must be signed in to change notification settings - Fork 205
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
How should default parameter values work with NNBD? #156
Comments
In the last case, this makes y effectively non-nullable. This is problematic as you can't set a nullable parameter to null. For example, with that behavior, it becomes impossible to make a Kotlin like ‘copy’ method (in Dart often called ‘copyWith’) with nullable types. Libraries like redux would be greatly simplified if we can implement such a copy method. See also this issue for a detailed example: I think this behavior would make sense if you declare the variable non-nullable. You can then treat ‘null’ to mean, give me the default value. But if the variable is nullable, it should be possible to give null as value. |
What about introducing an implicit default value for every non-nullable type? For example with a class MyClass {
static MyClass _default;
factory MyClass.default() => _default ??= MyClass._default();
MyClass._default();
}
and then either require a default value being provided for an optional parameter or a |
@zoechi It is quite a easy solution, but I think those are yet other nulls error-prone. |
@Cat-sushi I thought about that as well. |
I like the thought of defaults, but I think it should be opt-in. Otherwise nulls could get lost during API migrations: library a; // using legacy types
import 'library_b.dart' as b;
void delegatesToLibraryB({int a}) => b.someFunction(a: a);
library b; // upgraded to nullable types
void someFunction({int? a}) {
if (a == null) print("Some behavior when a is null");
} If you start using nullable types in library a, the parameter to I do like the idea of default initializers, but I feel like they should be more explicit, like |
We could introduce default values for each type as mentioned here, and that would be helpful in order to handle declarations like But that's a non-trivial exercise, and I think we have a rather natural set of rules already (they're the rules that just "fall out" if you consider the situation):
The point is that we don't need two ways to say that an actual argument was omitted, so developers will need to make a choice: They can use a nullable type for the formal parameter (and no default, if the parameter is optional), and callers may then omit the corresponding actual argument if the parameter is optional, or they can pass null. If the formal has a non-null type then callers can't indicate that the argument is absent, because that concept is simply not supported for this parameter, but if there is a default value then callers may choose to use it by not passing anything. The case where an optional formal parameter has a nullable type and a non-null default value may be a target for a lint. Is it ever justified? It certainly looks like a twisted setup where the designer wanted to allow the parameter to be semantically omitted (that is, to be null), and also wanted to make the syntactic omission of the corresponding actual argument mean something other than "this argument was omitted". One case needs to be considered further: Is it possible for a type to have unknown nullability? We could get such a thing with a declaration like the following: class A<X, Y extends num> {
void foo({X x, Y y}) {}
} We cannot claim that @leafpetersen wrote:
In the given context I think we can conclude otherwise, using the following idea:
I'd recommend that we do this, not by adding more syntax, but simply by noting that a named parameter with no default whose type is non-null is required, and subtyping etc. must treat them as such.
I wouldn't want that to be allowed. It is an anomaly to allow null as an actual argument when the corresponding formal parameter cannot be given the value null. I also don't see a need to proclaim that omitting a parameter and passing null (explicitly or implicitly) is the same thing as not passing anything. It's not consistent with the actual semantics (that the default value is passed if an argument corresponding to an optional parameter is omitted), and it just creates a problem that we don't have to have. Yes, I do recognize that a different semantics could be specified ("passing null means use the default value"), but that semantics clashes with the use of a nullable type (because then we couldn't ever pass null anyway) and actually also with the use of a non-null type (because it's yet another special case and magic effect that we can pass null to a parameter whose type is non-null, at least in the case where it has a default value). It's also a breaking change, of course. |
I think this is mistaken. Omitting the parameter, doesnt have to mean that you want it to be null. Look at the following example: class Todo {
final String body;
final bool completed;
final DateTime dueDate;
Todo({this.body = "", this.completed = false, this.dueDate});
Todo copy({
String body = this.body,
bool completed = this.completed,
DateTime dueDate = this.dueDate,
}) {
return Todo(body: body, completed: completed, dueDate: dueDate);
}
} Note that leaving out the dueDate in the copy method here, doesnt mean, set the dueDate to null, it means, leave it as it is. At this moment, this code doesnt work in Dart, but I heard that Dart may allow non constant default values in the future. Im saying this, because I think it would be unwise to make omitting a variable mean the same as setting it to null, it would make it impossible to implement a copy method like I described above. |
@kasperpeulen wrote:
But that wasn't what I said. We should certainly preserve the ability for an optional parameter to have a default value. If the developer who writes the method/function declaration specifies a default value for a parameter then omitting that parameter means using that default value. No problem, no confusion. The case that I mentioned (and questioned the usefulness of) is the case where we have a (non-null) default value for a parameter and that parameter has a nullable type. The reason why I consider this combination to be (perhaps) too twisted for its own good is that it gives the caller "two ways to omit the parameter": (1) actually omit it, in which case the body will get the specified default value, and (2) pass null (explicitly or via a computation), in which case the body will get null. Does your example actually need the ability for That said, I still want to have support for denoting the default values of callees (such that we can write forwarding functions that preserve the default values without duplicating the code for those values, and such that we can write near-forwarders where some parameters are reordered or renamed, etc). And I'd also be quite happy about generalizing default values to allow dynamic computation. ;-) |
My intuition:
|
(We definitely have cases in Flutter where we want to be able to specify null explicitly, both for required arguments and for arguments with default values where the default isn't null.) |
Indeed, I chose this example as a |
Let me make it clear that I prefer to make passing If we do that, then the function subtyping rules are simple. An optional parameter with a non-nullable type is effectively required. If it has a default value, then the parameter type for the caller becomes nullable, but internally in the function, the parameter is non-nullable. Example: void foo([int x = 42]) {
int z = x; // x not nullable;
}
void Function([int?]) bar = foo; // Allowed because foo *does* accept `null as first argument. Having a default value is effectively equivalent to starting the function out with: int x = x ?? 42; // "Redeclares" x as non-nullable. So, if an optional parameter's type is non-nullable on the outside, then it obviously has no default value. This approach does not require that the type system knows about default values because having a default value is reflected in the parameter type. That leaves the question of what happens to: void foo([int? x = 42]) ... Here What about: void foo<T>([T x]) ... Here we don't know whether It means that you cannot pass For the class Point {
final int x, y;
Point(this.x, this.y);
Point copyWith({int x = this.x, int y = this.y}) => Point(x, y);
/* effectively the same as:
Point copyWith({int? x, int? y} {
x ??= this.x; // and make x non-nullable
y ??= this.y; // and make x non-nullable
return Point(x, y);
}
*/
} That still doesn't work for nullable fields. class Something {
int something;
int? maybe;
Something copyWith({int something = this.something, int? maybe = this.maybe}) =>
Something()..something = something..maybe = maybe;
} Here you can't actually change Damn. So, let's look at the other option: Passing class C<T extends int?> {
void foo({int x, int y = 42, int? z, int? u = 42, T v, T w = 42}) => ...
void doStuff() {
this.foo( .... ); // which arguments are allowed here?
}
} We have to prohibit calling the function without a value for non-nullable non-default-valued parameters. We should probably be disallowed from using For nullable parameters, an omitted default value is just a default value of The type-variable parameters have to be handled as whatever type we think The main problem here is that we might want to encode whether a non-nullable parameter is required in the function type. Somehow. Let's assume we write it as Then I still think the former approach is simpler, both to explain and to use, even if there are some cases that it cannot handle (because there is only one way to represent "nothing"). (Just for the record: A flat |
I don't think so.
If so, we should do so explicitly for nullable x. And I hope the system promote x to non-nullable immediately after it. |
|
The top-level issue is intended to be a proposal both for a specific end-design (this discussion, among others), and for a set of choices around the migration support. This was not especially clear from the text though, so I've updated the header text in the first comment to reflect this. |
I think, the most and only difficult point is the treatment of non-nullable optional parameters without defaults. In context of the migration, functions should migrate with nullable parameters earlier than callers of them. |
It's definitely an option to require non-nullable optional parameters to have a default value, but it's not completely clear how that will work with generics: class C<T> {
void check([T t]) {}
} Is this code valid or invalid. I guess it's invalid, and you would have to write Another issue is that a class with a non-nullable optional parameter like: class D {
count([int number = 0]) {}
} will need a default value, but if a subclass wants to override that with a nullable parameter type, like: class E extends D {
count([int? number]) { if (number != null) super.count(number); }
} then the override becomes invalid because currently it's required to have the same default value. |
I understood the difficulty of generic functions. And, do you mean, class C<T> {
void check([T t]) {}
} ? |
It should be |
@Cat-sushi wrote:
We could do that, but we've had a request for required named parameters for a long time (and the current poor man's version using It is tempting to use this opportunity to get a consistent and language supported notion of required named parameters, and I think it makes sense to use non-null parameters with no default to play that role. |
I don't think this works out very well, unless you treat all non-null parameters as required (in which case, why do you have a default value at all?). The reason is that you need to add the notion of a required named parameter to the type system in order to support this anyway - otherwise you can't tell from the type whether you can elide the parameter or not. typedef F = int Function({int x}); // is x required or not?
void test() {
F f = ({int x = 3}) => x; // Allowed?
f(x:2);
f(); // Error? if so, how do I write the type of function with a non-required, non-nullable, optional parameter?
f = ({int x}) => x; // Allowed?
f(x:2);
f(); //Error?
} It seems reasonable to me to add a notion of a required named parameter. And it might be reasonable to say that in a function/method definition, having a non-nullable parameter with no default value implies required (without having to explicitly write it). But I don't right now see a way to just use nullability to express required-ness without ending up in a weird place. |
Right, we can use function declarations with the current syntax to single out the relevant named parameters, but we would need to add new syntax in function types in order to disambiguate. For instance, the function types could use a It's probably a matter of taste whether we would then require the same modifier in function declarations: NNBD migration might be easier if we don't require it, and code comprehensibility might be better if we do. Sounds like we could allow omitting it in the language, and leave it to a lint to require it in declarations. |
FWIW, I like |
@eernstg @leafpetersen Yes, non-nullable optional named parameters without defaults should be deemed as required. |
I think, nullable required named parameters (without defaults, duh!) are rare, and |
Summarizing my understanding of discussion so far, I see three feasible options (feasible in the sense that I see that they can be worked out).
3a) Treat internal/external types differently
3b) Treat internal/external types differently, and change calling semantics for optional parameters
Note that 3b is hard to make breaking only for opted-in libraries. Does that correctly capture our options at this point? Anything I'm missing? |
Ad. 2: I assume Also, (perhaps too extreme a change):
That requires significant migration, but it's automatable (although we'd might need to remove the requirement that an overriding method must have the same default value in order to keep existing code, where a superclass had no default value, valid). |
Doesn't S2 lead programers to use nullable types just to avoid error? |
I rethought S2 is OK, because in context of migration of |
I can follow the arguments, and adding My only worry is that it's a very verbose syntax, and we are going to be stuck with it for many, many years, so it might be worth it to do something more breaking now, in order to have a better experience in the long run. If we didn't care about the syntax change being breaking, we could do: foo(v1, v2, { v3, v4, [v5, v6]}) so optional arguments are always inside It's very non-backwards-compatible because it changes the existing syntax to mean something else. So, can we introduce a completely new parameter format, which only overlaps with the existing syntax where they agree on semantics. Then you'd have to use the new syntax in get required parameters, but old code would keep working (and we can upgrade incrementally). The only problem is to find such a syntax which does not suck. How about: foo(type x) // positional required parameter
foo(type x = value) // positional optional parameter
foo(type x:) // named required parameter
foo(type x:= value) // named optional parameter That is, a default value Then we'd have methods like: external static Future<Isolate> spawn<T>(
void entryPoint(T message),
T message,
bool paused:= false,
bool errorsAreFatal:= null,
SendPort onExit:= null,
SendPort onError:= null); and
(was: const Test({
Key key,
@required this.foo,
@required this.p1,
@required this.p2,
}) : super(key: key);) Maybe the |
@munificent wrote (regarding option S2b):
Could you expand a bit on the cases where you think this distinction would make a difference? How could you treat The only difference I can see between a required parameter with nullable type and an optional parameter with nullable type is that in the former case you explicitly have to pass
Let's assume that the type determines whether the parameter of a function type is required or not, exactly like it would for function declarations. If we want to mark a parameter as optional, we can simply use Isn't then the only case that we cannot express the case where we want the parameter to be optional but not nullable, which means that every function declaration would be forced to declare a default value? How common is such a case in practice? I'm probably missing something obvious, but so far I'm still having trouble seeing the benefits of an explicit distinction between required/optional and non-nullable/nullable for named parameters. Are there concrete examples that would show the benefits of keeping these notions separate? In my opinion, the difference between S2 and S2b is not just a difference of verbosity. In the case of S2, I would expect a clear mental model to explain the difference between the required-nullable case and the optional-nullable case, since as far as I can tell this is the only real difference in expressiveness between S2 and S2b. S2 can express this distinction, S2b cannot. But I'm not sure what differentiating these two cases buys me; when do I use which one? |
@fkettelhoit wrote:
void foo({int? i = 42}) => print(i);
main() {
foo(); // Prints '42'.
foo(null); // Prints 'null'.
} So if we want to insist that a named parameter is provided, and it is also allowed to be Also, we need the explicit For instance, in the case where the parameter type is a type variable, say @munificent mentioned another situation, where But the alternative interpretation (where |
@eernstg wrote:
But why don't we just omit the default value if we want to insist on a named parameter that can also be null and use Or am I completely missing the point of the example and there is a case that I'm still not seeing?
Good point, I hadn't thought of that. Assuming that all the other issues could be dealt with, wouldn't it be possible to introduce syntax for this case (which I assume would not occur all that often), e.g. in the form of an
But would such a breaking change not be a natural consequence of the introduction of NNBD types and communicate the intent of the function type more clearly? I would expect the function types to need an additional I understand that such a syntax would be a breaking change that requires migration. But isn't everyone who is opting into NNBD types already expecting exactly that? From the perspective of Dart developers outside of Google, Dart in its modern form is still relatively young and the amount of code written is probably relatively small compared to what will be written in Dart in the future. Personally, I would much prefer a simpler and cleaner mental model (where optional/required is determined by the presence/absence of default values + nullability of the type) and gladly accept comparatively large migration costs – not because I want to type less (that's just an added bonus), but because I want to think less about the subtle difference between these notions when it doesn't matter for the large majority of day-to-day use cases. |
The main point is probably what you are talking about later on: If a nullable named parameter is supposed to be required then we can't express that when requiredness is encoded in the type; so we just couldn't that named nullable parameter required, and your point would be that this is better because it is simpler. That makes sense, simplicity is actually valuable. The trade-off is, of course, that then we just can't have a named parameter which is required and can be null. Such a thing would serve to force call sites to explicitly indicate such a
If we were to start from scratch then it wouldn't matter so much whether
It might be ever so natural and still a little bit too inconvenient, if it breaks a lot of existing code and we could have avoided it. ;-)
I was tempted by that idea at first as well, but I agreed with Leaf that we do need an explicit marker for function types. Also, the ability to express (potentially) nullable types with a syntax that does not include // X could be nullable, or it could be non-null. So is `x` required?
X foo<X>({X x}) => x;
main() {
int? i = foo(); // OK, inferred type argument is `int?`.
int j = foo(); // Error, inferred type argument is `int`.
} I'm not convinced that we could make this work well (that is, we can't let the requiredness of a parameter depend on an actual type argument at each call site). So I'd prefer to say that an optional parameter with a potentially non-null type must have a default value (we can't do that for |
I really appreciate all this thoughtful discussion. Given some parameter, a user could potentially express:
(There is also whether it is named versus positional, but let's assume we want to keep those separate and ignore that for now.) There is a high correlation between these:
So I think what we're all feeling is some intuition that we don't need to give users three levers to control these independently. It's tedious and redundant. I felt the same way too. However, I'm not convinced that even though there are correlations, they aren't 100%. If we shackle two of these levers together, we'll leave users unable to express things they may need to express. In particular:
It's unfortunate, but this leads me to conclude we do need notation for each of the three things. We could potentially collapse them to two inside function declarations, but then it makes function types and function declarations diverge which causes its own problems. @lrhn, I empathize with your desire to reboot the syntax entirely. I've never liked the current syntax. But I'm personally not sold that your suggestion is worth the massive amount of churn to do this. If anything, I'd rather wait until some larger hypothetical "Dart syntax 2.0" where we could also reconsider getter/setter declaration syntax, types on the right, optional semicolons, |
Thank you all for the detailed explanations! These points are all valid, and I can see that coupling the type and the required/optional distinction would become too complex and unpredictable in some of the cases you mentioned, so I guess I'm finally convinced that S2b is not a good idea after all. @munificent wrote:
Just as a last thought: If we cannot couple type and the required/optional distinction, could we perhaps couple presence/absence of default value and required/optional? This would allow to express the distinction between required nullable parameters and optional nullable parameters; in the latter case the default value int? foo({int? i = null}) => i; // optional
int? foo({int? i}) => i; // required The semantics seem quite clean to me, but of course this would also be a massive breaking change for all the existing named parameters without default value and still doesn't solve the syntax problem for function types. Maybe someone else has a better idea of how to couple required/optional with the absence/presence of default values, but if not, I have to agree that S2 is the best we can get for now. |
That's the second point in my last bullet list. It's actually something I really really wanted to do for a long time until someone pointed out the problem: function types. In a function type, you can't specify a default value. You can't do, for example: typedef Callback = Function({int i = 3}); This is because default values are only a property of an actual declaration. You can think of a default value as implicitly being an assignment that happens at the top of the body of the function. But function types don't have bodies, so it would be confusing to allow a default value. We could say that you could just do a bare typedef Callback = Function({int mandatory, int optional =}); We could then also allow that inside function declarations as a more terse way of saying const Test({
Key key,
this.foo =,
this.p1 =,
this.p2 =,
}) : super(key: key);) As far as I can tell, this would work. But it does feel a little magical to me, and I don't know if I'm sold on the syntax. |
@munificent wrote:
I get that, which is why I wrote that it "still doesn't solve the syntax problem for function types". I merely wanted to point out that while the coupling of required/optional with the type seems to be complex and semantically confusing, the coupling of required/optional with the absence/presence of default values is semantically quite clean, it's "only" a syntax problem.
I agree, it feels magical and to my eyes at least worse than an explicit @tatumizer wrote:
If |
I feel the problem of "nullable required parameters" is a groundless fear. |
I'm inclined to agree, though it is a little scary to declare that to be true by giving users no way to create one at all. There is also still the type parametric case where you don't know if a parameter is nullable or not: class C<T> {
foo({T t}) {} // Is t required or optional?
}
You say that about every situation. :) In practice, what our users strongly prefer is familiar, incremental, in-the-box solutions whenever possible. Most people lack the luxury of time so want to solution that requires as little new learning as feasible. |
Depends on who is asking, and what If I have a It sounds a little weird that the same function parameter can be both optional and non-optional, but it really just means that it is nullable or non-nullable, and we have no problem with that. Omitting a nullable parameter works because you can just pass |
@munificent I think you are right about the importance of the order of named parameters. You want to be able to group related functionality (e.g. put |
I'm still relatively new to Dart and Flutter. And I am more or less able to follow the above discussion, at least the broad strokes. I'm someone who uses a lot of named parameters because, being newb-ish, they help me ensure the I'm passing the correct things and aid in my ability to trace values when I'm debugging. And while I know there are ways for me to ensure values are passed when I need a parameter to be required, I always pause when I create named parameters knowing that they are by default optional. So, at the risk of embarrassing myself, I wanted to ask if one of the solutions proposed above, or a variation on one of those proposed solutions, could, while awaiting a permanent solution, work as a package that one could import to provide this additional functionality. That way it would be, um... optional, not required. 😬 I guess I'm wondering if there is a way to enhance the default functionality that doesn't impact compatibility with code that doesn't use it. Perhaps an annotation that, under the hood, functions like a built in assert. Not sure if I'm completely making sense, but hopefully the general idea I'm trying to put forward is discernible. |
Today, there is a |
This is resolved in favor of having first class |
We need to resolve how default parameter values should work with NNBD, for both nullably and non-nullably typed parameters.
Consider:
Questions:
x
inf
an error?x
is effectively requiredf
is a subtype of the type ofg
, then thex
parameter ofg
must also be treated as required, which means there's no point in giving it a default.null
to thex
parameter ofg
allowed (and interpreted the same as not passing anything)?void Function({int x})
andvoid Function({int? x})
? You can passnull
to both of them.null
to they
argument ofg
bindy
tonull
(current behavior) or to1
.cc @munificent @lrhn @eernstg
The text was updated successfully, but these errors were encountered: