dart-lang / language

Design of the Dart language
Other
2.67k stars 205 forks source link

It is inconvenient that the default value of a formal parameter is constant #140

Open eernstg opened 5 years ago

eernstg commented 5 years ago

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.

eernstg commented 5 years ago

One starting point for addressing this request is the reference to Kotlin given in #137.

kealjones-wk commented 4 years ago

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.

eernstg commented 4 years ago

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.

eernstg commented 4 years ago

Note that https://github.com/dart-lang/language/issues/951 makes a similar request.

lrhn commented 4 years ago

And https://github.com/dart-lang/language/issues/429. My comments there also apply to #951.

clragon commented 12 months ago

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.

lrhn commented 11 months ago

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.

clragon commented 7 months ago

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.

lrhn commented 7 months ago

Let's give a concrete design for non-constant default values.

Proposal

Async

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.

Consequences

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.

clragon commented 4 months ago

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.

munificent commented 4 months ago

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,
  );
}
lrhn commented 4 months ago

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

munificent commented 4 months ago

Oh, yes, agreed that ? is probably not the right syntax.