Skip to content
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

Closed
Tracked by #110
leafpetersen opened this issue Dec 22, 2018 · 47 comments
Closed
Tracked by #110

How should default parameter values work with NNBD? #156

leafpetersen opened this issue Dec 22, 2018 · 47 comments
Assignees
Labels
nnbd NNBD related issues

Comments

@leafpetersen
Copy link
Member

We need to resolve how default parameter values should work with NNBD, for both nullably and non-nullably typed parameters.

Consider:

  void f({int x; int? y}) {...}
  void g({int x = 0; int? y = 1}) { }

Questions:

  • Is the declaration of x in f an error?
    • If not, then x is effectively required
      • This in turn means that if the type of f is a subtype of the type of g, then the x parameter of g must also be treated as required, which means there's no point in giving it a default.
      • So this effectively means that there are no optional non-nullable variables
      • Unless we separately added the notion of required named parameters that are part of the type
    • If so, then optional non-nullable parameters must always have a default value
  • Is passing null to the x parameter of g allowed (and interpreted the same as not passing anything)?
    • If so, then behaviorally, what's the difference between void Function({int x}) and void Function({int? x})? You can pass null to both of them.
  • Does passing null to the y argument of g bind y to null (current behavior) or to 1.
    • Changing this would be a breaking change.
    • This change has been advocated for independently, since it makes forwarding work better.

cc @munificent @lrhn @eernstg

@kasperpeulen
Copy link

kasperpeulen commented Dec 22, 2018

Does passing null to the y argument of g bind y to null (current behavior) or to 1.

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:
#137

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.

@zoechi
Copy link

zoechi commented Dec 23, 2018

What about introducing an implicit default value for every non-nullable type?

For example with a default constructor

class MyClass {
  static MyClass _default;
  factory MyClass.default() => _default ??= MyClass._default();
  MyClass._default();
}
  • int: 0
  • double: 0.0
  • num: 0
  • String: ''
  • bool: false

and then either require a default value being provided for an optional parameter or a default constructor to exist for the parameter type.

@Cat-sushi
Copy link

@zoechi It is quite a easy solution, but I think those are yet other nulls error-prone.
I mean, I wonder if 0, 0.0, '' and false couldn't be sources of bugs because of mis-initialization as well as null.

@zoechi
Copy link

zoechi commented Dec 24, 2018

@Cat-sushi I thought about that as well.
There is some danger but I think it's quite convenient and usually the desired behavior, especially in UI code.

@ds84182
Copy link

ds84182 commented Dec 24, 2018

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 delegatesToLibraryB would become default-initialized to 0 instead of null. Now we just changed the behavior of all libraries that depend on library a. From just library a's perspective, this isn't an issue.

I do like the idea of default initializers, but I feel like they should be more explicit, like void func({int a: default}). This opens another can of worms for generic functions (what should void func<T>({T value: default}) do?)

@eernstg
Copy link
Member

eernstg commented Dec 28, 2018

We could introduce default values for each type as mentioned here, and that would be helpful in order to handle declarations like T t; where T is a non-null type.

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):

  • It is a compile-time error for an optional positional parameter to have a non-null type and no default value.
  • When a named parameter has a non-null type and no default value, it is required. That is, the rules for subtyping and for correct overrides are adjusted such that they cannot be omitted in a supertype; and the rules for invocations are adjusted such that it is an error to omit actual arguments for them.

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 X is a non-null type if we support instantiations like A<int?, int>, and we cannot claim that it is a nullable type because we would certainly support instantiations like A<int, int>. But we could make the decision that a type whose nullability is unknown cannot be the type annotation of a named parameter with no default value (so that would be a compile-time error). In that case the developer would have to make the decision explicitly by using X? or X!, whose nullability is obviously known. Note that y is a non-problem, and it's a required named parameter, because Y is guaranteed to be a non-null type.

@leafpetersen wrote:

there are no optional non-nullable variables

In the given context I think we can conclude otherwise, using the following idea:

required named parameters that are part of the type

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.

Is passing null to the x parameter of g allowed (and interpreted
the same as not passing anything)?

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.

@kasperpeulen
Copy link

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".

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.

@eernstg
Copy link
Member

eernstg commented Dec 30, 2018

@kasperpeulen wrote:

Omitting the parameter, doesnt have to mean that you want it to be null

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 dueDate to be null? If you really want that then it could be a very useful example to have in mind; I'm just noting that "two ways to omit the parameter" is more complex than "one way" (which means omitting it syntactically, which means using the default value), and I'm not quite convinced that it is helpful to have it. It would probably be addressed by a lint rather than a language error/warning, though—it may be twisted, but it's still well-defined.

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. ;-)

@Hixie
Copy link

Hixie commented Dec 31, 2018

My intuition:

  • passing null should mean passing null, it's not the same as omission. Passing null to a non-nullable parameter should fail.

  • optional non-nullable parameters should be required to specify their default, since the default default, null, isn't valid for them.

  • requiredness is orthogonal and should continue to be specified using @required.

@Hixie
Copy link

Hixie commented Dec 31, 2018

(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.)

@kasperpeulen
Copy link

Does your example actually need the ability for dueDate to be null? If you really want that then it could be a very useful example to have in mind.

Indeed, I chose this example as a dueDate is a typical nullable property. Of course, the exact same problems will arise with any other nullable property and a similar implementation of the copy method.

@lrhn
Copy link
Member

lrhn commented Jan 2, 2019

Let me make it clear that I prefer to make passing null equivalent to not passing an argument, so omitting an argument is just a shorthand for passing null. It's a much simpler model to work.

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 works well with subtyping because any super-type of void Function([int]) must also have a non-nullable type as first argument.

This approach does not require that the type system knows about default values because having a default value is reflected in the parameter type.
(We also don't even need optional positional parameters any more, we should be able to omit any trailing nullable positional parameter, whether declared optional or not!)

That leaves the question of what happens to:

void foo([int? x = 42]) ...

Here x is nullable on the inside and nullable on the outside, so what happens if I pass null?
I'd say that it is still replaced by the default value.

What about:

void foo<T>([T x]) ...

Here we don't know whether T is non-nullable or not without knowing whether the type argument is.
The invocation may know, but doesn't necessarily, in case the type argument is itself a type variable with a nullable bound. There will have to be a run-time check in that case, but it can still be at the call-site.

It means that you cannot pass null to a function with a default value and get a different effect than not passing an argument.
That might be a problem ... existing code does depend on this (no matter how ill-advised the design may be in my opinion).

For the copyWith methods, I'd like to make default values non-constant, so you can just write:

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 maybe to null.

Damn.


So, let's look at the other option: Passing null is not the same as not passing an argument.

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 null as an argument to a non-nullable parameter, even if it has a default value. It's omittable, not nullable.

For nullable parameters, an omitted default value is just a default value of null. Passing null does not trigger the default value (like now).

The type-variable parameters have to be handled as whatever type we think T is.
Our covariant generics may cause us to make mistakes that are caught at run-time (the T-parameters are covariant by generics), and we'll have to assume worst-case behavior otherwise. So, in doStuff, you can't pass null to v or w because T is not a super-type of Null (it's a sub-type of int?, and that is all we know).

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 void Function({int x!}) (which we probably won't).

Then void Function({int? x}) <: void Function({int x}) <: void Function({int x!}).
This complicates all sub-typing and override rules. It effectively means that if a super-class method has a default value, then a sub-class override of the method must also have a default value (I'd still change the spec to allow it to be a different default value, though).

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 null is a bad idea entirely. We should have had an Option which can nest instead).

@Cat-sushi
Copy link

Cat-sushi commented Jan 2, 2019

Let me make it clear that I prefer to make passing null equivalent to not passing an argument, so omitting an argument is just a shorthand for passing null. It's a much simpler model to work.

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.

I don't think so.
It's very unintuitive that parameter can be nullable for callers and non-nullable for the body at the same time.

Having a default value is effectively equivalent to starting the function out with:
int x = x ?? 42; // "Redeclares" x as non-nullable.

If so, we should do so explicitly for nullable x.
More precisely,
x ??= 42;

And I hope the system promote x to non-nullable immediately after it.

@Cat-sushi
Copy link

Cat-sushi commented Jan 2, 2019

By the way, this is a sub-issue of the migration issue.
Passing null to non-nullable optional parameter isn't a solution of migration.

@leafpetersen
Copy link
Member Author

By the way, this is a sub-issue of the migration issue.

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.

@Cat-sushi
Copy link

Cat-sushi commented Jan 4, 2019

I think, the most and only difficult point is the treatment of non-nullable optional parameters without defaults.
And, I suspect there are no perfect solution of it.
So, I think non-nullable parameters without default should be simply prohibited.

In context of the migration, functions should migrate with nullable parameters earlier than callers of them.
Said that, only if the callers migrate earlier than the functions, parameters without marked ? should be deemed as nullable for the callers.

@lrhn
Copy link
Member

lrhn commented Jan 4, 2019

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 void check([T? t]) {}.
You can't write a default value.

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.
We should probably remove that requirement. If a sub-class extends the parameter type, it's annoying that you have to keep using the original default value, and that becomes more prevalent if super-types are required to have default values.

@Cat-sushi
Copy link

I understood the difficulty of generic functions.
Now, subtypes could have default value being non-nullable like those of the super.
I don't think, methods of subtypes want to have different defaults null.

And, do you mean,

class C<T> {
  void check([T t]) {}
}

?
I think, t should be T?, if it doesn't have a defaults.

@lrhn
Copy link
Member

lrhn commented Jan 4, 2019

It should be [T t] yes. Fixed. Thanks!

@eernstg
Copy link
Member

eernstg commented Jan 4, 2019

@Cat-sushi wrote:

So, I think non-nullable parameters without default should be simply prohibited.

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 @required has loopholes because it isn't part of the type).

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.

@leafpetersen
Copy link
Member Author

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.

@eernstg
Copy link
Member

eernstg commented Jan 4, 2019

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 required modifier (probably a bult-in identifier) to indicate that any given named parameter is required.

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.

@Hixie
Copy link

Hixie commented Jan 4, 2019

FWIW, I like @required (over required) precisely because its loopholes mean that sometimes you can get your job done even if it means stomping all over the pretty semantics of the API you're using. We've definitely been slowly losing that aspect of Dart in the Dart 1->2 transition. It's becoming much more of a straight-jacket language where you can't just tell the compiler to trust you.

@Cat-sushi
Copy link

@eernstg @leafpetersen Yes, non-nullable optional named parameters without defaults should be deemed as required.

@Cat-sushi
Copy link

I think, nullable required named parameters (without defaults, duh!) are rare, and @required can be preserved for them.

@leafpetersen
Copy link
Member Author

leafpetersen commented Feb 5, 2019

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).

  1. No optional non-nullable parameters
  • {int x} means x is required
  • [int x] is probably an error
  • default values are not allowed for non-nullable variables
  • there is no way to make a nullable named parameter required
  1. Add required named parameters
  • {int x} implicitly means x is required
    • required-ness goes into the function type: int Function({required int x})
  • {required int? x} is allowed
    • means that something must be passed
    • passing null is allowed
  • {int x = 3} is allowed
    • x is optional
    • passing null to it is an error
    • passing nothing to it results in it getting the default value
  • [int x] is probably an error
  • [int x = 3] is allowed

3a) Treat internal/external types differently

  • {int x} means that x is required
  • {int? x} means that x is optional
  • {int x = 3} is treated as:
    • {int x} in the function body
    • {int? x} externally (in the type of the function)
    • passing null to a non-nullable variable with no-default gives the default value

3b) Treat internal/external types differently, and change calling semantics for optional parameters

  • All the same things as 3a
  • plus passing null to any optional parameter gives the default value (if any)

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?

@lrhn
Copy link
Member

lrhn commented Feb 5, 2019

Ad. 2: I assume {required int? x = 2} is an error. No default values on required parameters.

Also, (perhaps too extreme a change):

  1. Make all named parameters with a default value optional, all other named parameters required
  • {int x} is required.
  • {int x = 2} is optional.
  • {int? x} is required.
  • {int? x = null} is optional
    Maybe even do the same thing for positional parameters, and drop the [ and ].

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).

@Cat-sushi
Copy link

Doesn't S2 lead programers to use nullable types just to avoid error?
If so, I think that it lowers the value of NNBD.

@Cat-sushi
Copy link

Cat-sushi commented Mar 30, 2019

I rethought S2 is OK, because in context of migration of f({int i}), we should expricitly select f({int? i}) or f({required int i}), anyway. It's not the problem of error in migration.

@lrhn
Copy link
Member

lrhn commented Apr 1, 2019

I can follow the arguments, and adding required as a keyword is the simplest completely backwards compatible syntax.

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 [..], even if named.

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 = means the parameter is optional, a : after the name means that it's named. There is no [..] or {...}, so the only overlap with existing parameter syntax is for required positional parameter. Should the : be a prefix instead of a suffix? It seems more readable, but doesn't work with initializing formals.

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

const Test({
    Key key,
    this.foo:,
    this.p1:,
    this.p2:,
  }) : super(key: key);

(was: 

const Test({
      Key key,
      @required this.foo,
      @required this.p1,
      @required this.p2,
    }) : super(key: key);)

Maybe the : is not visible enough, but I think the idea is worth considering.

@fkettelhoit
Copy link

fkettelhoit commented Apr 1, 2019

@munificent wrote (regarding option S2b):

We probably will still need an explicit syntax. We can't only rely on this inference to determine which parameters are required because some APIs want to have required parameters of nullable types. This is because they treat null as meaning something distinct from "no".

Could you expand a bit on the cases where you think this distinction would make a difference? How could you treat null as different from "no" in the presence of this distinction?

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 null, whereas in the latter you can either explicitly pass null or pass nothing at all, but neither case allows you to differentiate between null as an explicit value and no value, since even the nothing-explicitly-passed case for an optional parameter with nullable type would still set the parameter value to null, correct? So how would the function body even tell the difference between required-nullable and optional-nullable?

This doesn't work in function types. Default values are a property of a function declaration. They only come into affect inside the body of a member. Thus, a function type can't specify a default value. But a default-less named parameter in a function type can still be optional. We need a syntax to express that, even if the parameter's type is non-nullable. This is a valid function type:
dart typedef MaybeTakeInt({int i});

And it's satisfied by this member:
dart function({int i = 1}) {}

So even if we infer required in function declarations, we still need a syntax for making it explicit in function types. Worse, the same syntax in a declaration now means something different in a function type. If you take the parameter signature in this function:
dart function({int i}) {}

And paste it into a typedef:
dart typedef FunctionType({int i});

You have changed i from being required to optional. I think this is a serious problem. We already have enough confusion around our parameter syntax and function types. I'm really hesitant to have the same syntax be valid but mean something else in different places.

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 int? instead of int.

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?

@eernstg
Copy link
Member

eernstg commented Apr 1, 2019

@fkettelhoit wrote:

Could you expand a bit on the cases where you think this distinction would make a difference?

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 null, then we must allow developers to specify (separately from the nullability of the parameter type) that this parameter is required.

Also, we need the explicit required flag on parameters in a function type.

For instance, in the case where the parameter type is a type variable, say X. If we wish that such a parameter be required then we must ensure that it is known that X is non-null. We can do that by using a bound which is not nullable (like X extends Object as opposed to the default bound Object?). But if we wish to allow the parameter to be optional then we can't express that: We would have to specify that X must be a supertype of Null, and that would be an entirely new concept (a lower bound on a type variable). Lower bounds on type variables have frequently been connected with undecidability, so we wouldn't go that way unless we've explored the implications very carefully first.

@munificent mentioned another situation, where typedef FunctionType({int i}) would change i from being required to optional (which is certainly not appropriate if we want to let that type denote a tear-off of a method whose named i argument is required).

But the alternative interpretation (where i is required, because the type is non-null int) would be massively breaking: There is a lot of existing code where there are named parameters which are actually expected to be optional, and we probably don't want to add ? to the type of all named parameters in function types. The community would then have to refactor all code using function types with named parameters such that all invocations pass all named parameters with a non-null type (because they just can't be optional any more). But even that wouldn't suffice because there is no subtype relationship among function types with different sets of required named parameters, so we'd need to refactor code until no such subtype relationships are used any more. So I definitely think it would not be realistic to say that function types can use the rule "a named parameter is optional iff its type is nullable".

@fkettelhoit
Copy link

@eernstg 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 null, then we must allow developers to specify (separately from the nullability of the parameter type) that this parameter is required.

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 void foo({int? i}) => print(i); in that case? If a parameter is non-nullable, then the presence/absence of a default value should control whether the parameter is optional/required. If a parameter is nullable, then either there is no default value and no difference for the function body between passing null or omitting it (and thus no harm in not insisting on passing something explicitly) or there is a default value so it doesn't make sense to insist on passing something (otherwise what's the point of the default value?).

Or am I completely missing the point of the example and there is a case that I'm still not seeing?

Also, we need the explicit required flag on parameters in a function type.

For instance, in the case where the parameter type is a type variable, say X. If we wish that such a parameter be required then we must ensure that it is known that X is non-null. We can do that by using a bound which is not nullable (like X extends Object as opposed to the default bound Object?). But if we wish to allow the parameter to be optional then we can't express that: We would have to specify that X must be a supertype of Null, and that would be an entirely new concept (a lower bound on a type variable). Lower bounds on type variables have frequently been connected with undecidability, so we wouldn't go that way unless we've explored the implications very carefully first.

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 optional keyword just for function types? It seems extreme that all function declarations should pay the price if only function types are affected.

But the alternative interpretation (where i is required, because the type is non-null int) would be massively breaking: There is a lot of existing code where there are named parameters which are actually expected to be optional, and we probably don't want to add ? to the type of all named parameters in function types. The community would then have to refactor all code using function types with named parameters such that all invocations pass all named parameters with a non-null type (because they just can't be optional any more). But even that wouldn't suffice because there is no subtype relationship among function types with different sets of required named parameters, so we'd need to refactor code until no such subtype relationships are used any more. So I definitely think it would not be realistic to say that function types can use the rule "a named parameter is optional iff its type is nullable".

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 ? if they indeed accept optional named parameters.

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.

@eernstg
Copy link
Member

eernstg commented Apr 1, 2019

But why don't we just omit the default value if we want to insist
on a named parameter that can also be null

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 null, which may or may not be a reasonable choice in a given application context.

an optional keyword just for function types?

If we were to start from scratch then it wouldn't matter so much whether optional or required would have to be specified. But with the current installed base (where there are a lot of function types with named parameters which may or may not be passed at each call site), I'm afraid that it would be rather seriously breaking to require an optional on all those function types, just so that existing code could remain working.

But would such a breaking change not be a natural consequence
of the introduction of NNBD types

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. ;-)

[prefer a model where] optional/required is determined by the
presence/absence of default values + nullability of the type

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 ? makes it a bit less readable if required holds if and only if the type is potentially non-null:

// 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 x above, though), and a required named parameter must be marked as such (we can do that for x above, and then we can also ensure that all invocations will provide an x). And then the developer can use ({X? x}) in order to enforce that the parameter type is nullable, such that x can be optional.

@munificent
Copy link
Member

I really appreciate all this thoughtful discussion.

Given some parameter, a user could potentially express:

  • Whether or not it has a nullable type.
  • Whether or not it has a default value.
  • Whether or not it is optional.

(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:

  • Most nullable-typed parameters are optional.
  • Any require parameter in a declaration must have a default value.

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:

  • If a parameter's type is itself some type parameter, then we don't know if it's nullable or not. That means that if we infer required/optional from that, we don't know what to infer. In practice, users will likely need to be able express both cases explicitly.

  • A non-optional parameter in a function type does not need a default value, and in fact the syntax prohibits them. So you still need a way to express required/optional independent of default/no-default inside function types.

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, let, etc. As it is now, I think the current syntax is adequate and it's probably best to just make an incremental change with required even though the result isn't very elegant.

@fkettelhoit
Copy link

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:

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.

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 null would have to be used:

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.

@munificent
Copy link
Member

could we perhaps couple presence/absence of default value and required/optional?

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 = to specify an optional named parameter in a function type:

typedef Callback = Function({int mandatory, int optional =});

We could then also allow that inside function declarations as a more terse way of saying = null. Lasse's example would look like:

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.

@fkettelhoit
Copy link

fkettelhoit commented Apr 2, 2019

@munificent wrote:

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.

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.

We could say that you could just do a bare = to specify an optional named parameter in a function type:

typedef Callback = Function({int mandatory, int optional =});

We could then also allow that inside function declarations as a more terse way of saying = null. Lasse's example would look like:

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.

I agree, it feels magical and to my eyes at least worse than an explicit required. For function declarations with a hypothetical syntax for default-values-as-optional, I would personally prefer an explicit foo = null, which is a bit more verbose, but clearly communicates the presence of a default value. But of course that doesn't solve the problem for function types and I can't think of a syntax that wouldn't feel a lot worse and more magical in the context of function types than the explicit required.

@tatumizer wrote:

If you decide to explicitly mark required parameters, then why not keep @required as annotation?

If required really becomes an integral part of Dart's NNBD system that is needed to express the required/optional distinction, it must in my opinion also become a proper keyword. An annotation is normally used for something on a higher level than the core language itself and not strictly necessary for the semantics of the language. Making the new required an annotation would blur that line.

@Cat-sushi
Copy link

I feel the problem of "nullable required parameters" is a groundless fear.
I also feel the correlations between nullability and requiredness are almost 100%.
The only problems are parameters in function types and parameter typed parameters.
In those contexts, we could introduce keyword of required and/ or optional.

@munificent
Copy link
Member

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?
}

I think the situation is in dire need of original, out-of-the-box thinking.

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.

@lrhn
Copy link
Member

lrhn commented Apr 3, 2019

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?
}

Depends on who is asking, and what T is to them.

If I have a C<int> then its foo method has one required parameter. If I have a C<int? then its foo method has one optional parameter. If I am inside C, and all I can see is the abstract T, then I must assume that the parameter is required, because it might be required, and my code must work for every valid instantiation of T.

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 null. Omitting a non-nullable parameter doesn't work because there is no value to pass.

@yjbanov
Copy link

yjbanov commented Apr 3, 2019

@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 decorationColor and decorationStyle next to each other in TextStyle) as well as sort those groups (as in your header, body, footer example). So I think S2 and S3 are better than S1.

@secondstreetmedia
Copy link

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.

@munificent
Copy link
Member

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.

Today, there is a @required annotation that you can place on named parameters. The analyzer will give you some static checking to ensure that you pass arguments to those, but it's not integrated into the language fully, so code that omits them can still be run.

@leafpetersen
Copy link
Member Author

This is resolved in favor of having first class required parameters.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
nnbd NNBD related issues
Projects
None yet
Development

No branches or pull requests