Open eernstg opened 5 years ago
One starting point for addressing this request is the reference to Kotlin given in #137.
I am curious if this issue has been reconsidered/revisited due to the incoming non-nullable types, default values of parameters that are non-nullable would require that any user created classes/types would have to have a const value just to be able to default it. which may not always be possible.
We have made some changes to the rules about default values for parameters.
In particular, named parameters can be required
, in which case there is no need and no permission to specify a default value. This mechanism is a language mechanism, it's taken into account when determining whether one function is a subtype of another one, and hence it is enforced (so it's quite different from the @required
metadata which has been around for several years).
Moreover, abstract methods do not have to have a default value for an optional parameter, even when its type is non-nullable, because the run-time semantics will never rely on such default values anyway.
However, this actually doesn't make much difference here: This issue is essentially about allowing default values to be normal expressions in some scope (maybe: the instance or static scope of the enclosing class, or the library scope, depending on where the parameter is declared). Those expressions would then be evaluated at each invocation where the corresponding parameter is omitted.
The requirement that a default value must be assignable to the type of the corresponding parameter remains unchanged, and the requirement that a default value must be specified iff the dynamic semantics may use it also remains unchanged. So the changes that we're introducing along with non-nullable types will just carry over directly to the kind of defaults that this issue is targeting.
Note that https://github.com/dart-lang/language/issues/951 makes a similar request.
And https://github.com/dart-lang/language/issues/429. My comments there also apply to #951.
You can tell whether a parameter was passed or not by having side effects
But assuming we care a lot about allowing a copyWith
method that does actually allow setting a null
-able variable to null
without the extensible overhead of fairly obscure code generation, how would we ever resolve this?
I personally think it is incredibly desirable to create such copyWith
methods and the benefits would be felt throughout the entire language because writing models will become much, much easier.
This is especially important in libraries which do not wish to use other code generating libraries or places where its currently simply impossible to set a null
-able variable to null
with copyWith
like in the Uri
class.
So given A: "we wish to default to non-constants" B: "we do not wish to expose information on whether a prameter is passed"
Is there generally a solution that would satisfy both A and B or is the current conclusion that we are simply trading B for A? I believe giving up B for A if we cannot have both would be the better outcome.
No, there is no design which satisfies both A and B. Trivially, if the default value expression is not constant, so A, it can have side effects, which means it's possible to detect whether it has run, which precludes B.
I don't have a problem with allowing you to programmatically detect whether an argument was given or not, if you also have a way to programmatically decide whether to pass an argument or not, without requiring a combinatorial explosion of individual call expressions.
Having the former without the latter is what makes forwarding optional parameters nigh impossible to do precisely. With both, it can (and should) be a one-liner.
Thats a really good point.
I have seen there was some proposal about Records being able to be spread into parameter lists, which I think might have also included some kind of syntax to conditionally do so? Maybe that could be broadend and combined with this?
Unfortunately I dont recall where this discussion happened.
Let's give a concrete design for non-constant default values.
Allow non-constant expressions as default value expressions. (That is, remove the current requirement that they are constant expressions.)
The lexical scope of the expression is the parameter scope.
It's a compile-time error if the default value expression of a parameter refers to a parameter which is not declared earlier in the parameter list. Cannot refer to later variables, cannot refer to itself.
It's a compile-time error if the default value expression contains an assignement to any parameter of the function.
The static type of a default expression must be assignable to the declared type of the parameter.
It's a compile-time error if a default value expression of a const
constructor is not a potentially constant expression, or if its assignable only using a non potentially constant coercion.
During function invocation, while binding actuals to formals, parameters are processed in source order, each parameter variable being bound to its value in that order, until all parameter variables are bound.
If an optional parameter has no corresponding argument, its default value expression is evaluated to a value in the parameter scope (where all prior parameters have now been bound to a value), and then the parameter variable is bound to the value of that evaluation, or coerced if it its static type was assignable to, but not a subtype of, the parameter's type. If it throws, the invocation throws.
We can allow an asynchronous function to have await
in the default-value expressions.
We probably should allow that, otherwise it'll feel non-orthogonal.
That does mean that an invocation of an asynchronous function can introduce an asynchronous gap before it even reaches the function body.
That's not particularly new or worrisome. If the first line of the body had been arg ??= await something()
, it would behave exactly the same.
Since constructors cannot be async
, at least we won't have asynchronous interruptions in the middle of creating and initializing an object.
That will trivially allow a copyWith
function like:
class Point
final int x, int y;
final Color? color;
Point(this.x, this.y, {this.color});
Point copyWith({int x = this.x, int y = this.y, Color? color = this.color}) =>
Point(x, y, color: color);
}
Because of the "no assignment" rule, the compiler doesn't need to have the incoming values stored in any particular place. It can probably be loosened, but it makes it much easier to read when one initializer cannot change a previously initialized variable.
However, because expressions can have side effects, this feature allows determining whether a parameter was passed, even if it has the same value as the eventual default value.
bool __wasPassed = true;
// Can be read only once, then it resets.
// Should be read to reset it, before the next call to `foo`.
bool get _wasPassed {
var result = __wasPassed;
__wasPassed = true;
return result;
}
Object? _notPassed() {
_wasPassed = false;
return null;
}
void foo([Object? arg = _notPassed()]) {
if (_wasPassed) {
print("Passed: $arg");
} else {
assert(arg == null);
print("Not passed");
}
}
void main() {
foo(); // Not passed
foo(null); // Passed: null
foo(); // Not passed
foo(null); // Passed: null
}
That's so cumbersome it's probably not going to be used much, which is why I don't necessarily think we need to allow computationally deciding whether to pass an argument. It'll be an anti-pattern to rely on distinguishing a passed argument from a default value, because it makes it harder for callers to choose the behavior they want.
This is fantastic, and exactly what I need.
Would this be a non-breaking change? it does seem to me like that, but perhaps it isnt.
if you also have a way to programmatically decide whether to pass an argument or not, without requiring a combinatorial explosion of individual call expressions.
Control flow in argument lists would do that:
forward({
String? a,
String? b,
String? c,
String? d,
String? e,
String? f}) {
original(
if (?a) a: a,
if (?b) b: b,
if (?c) c: c,
if (?d) d: d,
if (?e) e: e,
if (?f) f: f,
);
}
Control flow in argument lists would do that
"Parameter elements" was absolutely what I was hinting at when writing that sentence.
(But ?x
may not be the best syntax for checking if a parameter is set when we also use ?e
as a null-aware element.
It makes ? x ? v1 : v2
ambiguous. At least one of the meanings will need parentheses.)
Oh, yes, agreed that ?
is probably not the right syntax.
Currently, default values are constant expressions, which implies that it is impossible for a function declaration to use the default value mechanism to specify a non-trivial computation which fills in an actual argument value when it is omitted at the call site.