-
Notifications
You must be signed in to change notification settings - Fork 213
Late parameters, late-init-query operator, parameter element #3680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Definitely agreed that we need both, and I really like that the latter one also helps with default values. I don't think it fully fixes the default value issue because often it is actually an override, and you aren't just calling |
This proposal to me is a nice solution that avoid adding something like |
Why not allow The class Point {
final int x;
Point(this.x);
Point copyWith({late int x = this.x}) => Point(x);
} |
Late variables are evaluated lazily, so the presence of default value won't prohibit the use of f({late x=1, late y=2}) {
print(x); // x gets initialized here
print(?? x); // true - variable is already initialized
print(?? y); // false - variable is not initialized yet: asking the question doesn't count as an "access"
} The example of "print(x)" shows that the confusion between "passed" and "initialized" might be common. For that reason, maybe it's better to adopt a narrow definition: '?? a` means "parameter was passed" regardless of whether it is initialized or not at the moment. Not sure. |
I think the ?? prefix operator could be generalised to all late variables - not just parameters - so having it use a parameter-specific term like "passed" might not work great in the future. I still wouldn't want it to become common practise to have late final variables that have their initialisation checked with |
I'm less enthusiastic about the "adding more places a variable can be uninitialised" part of this proposal though. My opinion is that uninitialised variables are dangerous, with better alternatives available. Not having to pass a value for a parameter is fine, but it should only be allowed for parameters that can already be omitted (optional positional and non-required named parameters). For the named parameters, it's completely useless as you can just not pass the named parameter. There should probably also be a lint encouraging people to use named parameters instead of optional positional ones if they do find themselves having to omit a value to "skip" a positional parameter. The |
Maybe you just want #140, then? |
+1 it is annoying today that you can't check the initialized status of late variables, and it means you can't use them in places you might like to. I think extending this proposal to all late variables helps solve multiple problems, which is always a good sign for any proposal. |
If we have both late parameters and non-constant default values, then I think you should allow late parameters to have default values also. This could allow you to for example assign a late parameter to the result of an expensive function call that may or may not get executed due to lazy evaluation. EDIT: Correct me if I am wrong, but I assume a bool and(bool a, late bool b) => a && b;
bool or(bool a, late bool b) => a || b;
bool truePrint(String s) {
print(s);
return true;
}
void main() {
and(true, truePrint('A')); // side effect, prints A
and(false, truePrint('B')); // no side effect, second argument never evaluated
or(true, truePrint('C')); // no side effect, second argument never evaluated
or(false, truePrint('D')); // side effect, prints D
} |
A workaround I use for "the void main() {
final x = Foo('some', 1);
final y = x.withBar('y');
final z = x.withBaz(null).withBar('another');
print('$x\n$y\n$z');
}
class Foo {
final String bar;
final int? baz;
Foo(this.bar, this.baz);
Foo withBar(String newBar) => Foo(newBar, baz);
Foo withBaz(int? newBaz) => Foo(bar, newBaz);
@override
String toString() => 'Foo(baz=$bar, baz=$baz)';
} The intermediate objects obviously aren't ideal performance-wise but IMO in practice it's pretty uncommon that you're updating a large number of fields at once, and IIUC Dart's garbage collector is optimized for large numbers of short-lived objects. If it started to become a problem I can imagine a few optimizations for chaining multiple |
As an alternative to the (defun foo (a b &optional (c 3 c-supplied-p))
(list a b c c-supplied-p)) In Dart, we could reuse some token to indicate this (strawman using class Point
Point(this.x, this.y, {this.color});
final int x, int y;
final Color? color;
Point copyWith({
late int x +hasX,
late int y +hasY,
late Color? color +hasColor,
}) {
return Point(
hasX ? x : this.x,
hasY ? y : this.y,
color: hasColor ? color : this.color,
);
} |
I see some relation with #877. |
I think |
Suppose we introduced a method late int foo;
print(foo.isInitialized) The problem is that methods are always defined for types. Does What will change if we replace Given the rigor around types maintained by dart, it would be more consistent to create an honest type representing the omitted parameters. This can be done without introducing "another null". Please give some consideration to the idea of |
Nothing would make What we could do is make Since being late is not part of a class interface, or member signature at all, such an operator can probably only work for local variables, maybe static/top-level variables inside the same library, and maybe even instance variables when read through |
If it's not about types, then I don't understand your earlier objections to |
The difference is not typing, it's whether the operation looks like a function, but behaves like a special operator. If it's the latter, I want a special operator for it, so I don't have to look at Dart originally defined We did the opposite with That is why I'd prefer And same for Similar things should look similar, distinct things should look different. That's the cornerstone of experience-based readability. We can teach people to recognize patterns better than lots of individual rules. |
Probably off-topic in this thread, but anyway... |
It's never too late to invent entirely new features! Using Not sure it's worth it, we don't plan to add a lot of "really language features, not functions". But maybe it's just what we need for inline assembly operations for FFI |
This is an argument against the proposed syntax Another (unrelated) argument could be that Despite that, I admit I kinda like the proposed operator |
At least you guys considered encapsulation, while all I wanted to do was just check the initialization state of the late final fields... I'm cracking up, honestly, I really don't care much about code quality...#4029 |
I'm not a fan of magically telling if something was passed in. I do like the idea of using This could be using final class Bar {
Bar() {
if (something) { // some flag. could be debug stuff we want private access to.
final foo = _Foo();
foo._doPreliminaryWork(this);
this.foo = foo;
}
}
// if foo is never interacted with, its never initalized.
late final Foo foo;
void doRareThingWithFoo() {
if (late? foo) { // that debug stuff was never used, and no external tool set it.
// foo was never set
print('foo is still late, which means it still isnt here.'); // did it get caught in traffic?
// psudo-promotion. its safe to set.
foo = Foo();
}
}
}
// we could have
if (late? bar.foo) bar.foo = SpecialExternalFoo();
bar.foo.somethingNeat(); I'm actually liking that one - the word that writes the contract is the same one that checks it. beautiful. That can also be a great way of making it so that knowledge of initialization is opted into only when desired (ie by using the It also ensures that non-const defaults arent called if we don't want them to be, since honestly i'd be fine if non-late defaults were still const, since it would imply the creation and immediate removal of objects at runtime. at least with non-late const defaults, they were canonicalized since the start. |
I like the idea of using final foo = late _foo ? _foo : _bar;
if (_bar case late var bar?) { // ? - additional null check
// bar ...
} |
Not sure what the pattern syntax would look like. |
(Because I'm sure I've written this before, but it's probably in a comment of another issue, here is a stand-alone version.)
The
copyWith
problem is that it's impossible to distinguish an omitted argument from an explicit passing of the default value.That's generally considered a good thing, but it makes forwarding arguments to another function awkward.
Dart once had a "was argument passed" operator,
?x
which was true ifx
had an argument passed, andfalse
if it got its default value implicitly. It was intended to make it easier to forward arguments to other functions, but since those other functions could now also behave differently depending on whether an argument was passed or not, it actually made it harder. It was quickly removed again.I propose that we add two things, both a way to see if an argument was passed, and a way to easily pass or not pass an argument in constant code size.
Proposal
Allow optional parameters to be declared
late
. A late parameter need not be initialized, so it can be optional and not have a default value. (It's not allowed to have a default value, because if it has one, it doesn't need to be late.)Add a prefix operator
??
(strawman syntax) which applies to a late local variable, and which evaluates to abool
value that istrue
if the variable is initialized, andfalse
if it's not. (This value affects "definite assignment" analysis, so it's possible to writeif (??x) { return x; } else { x = 42; }
no matter what the assignment analysis said about the late variablex
before.)(See also #1028. Maybe
??
should only apply to late parameters, but it can be extended to any other late variable.)Finally, allow argument elements, which are arguments which can evaluate to no value.
Anywhere we currently allow an
<expression>
as an argument, we now use<argument>
, which allows omitting a value for that argument.if
can have noelse
branch.switch
needs not be exhaustive (unless it's an always-exhaust type, as usual).? <expression>
is a non-value if the expression isnull
.<argument>
itself can be omitted (otherwise?null
would be the shortest "no value" syntax).(The element-switch and null-aware element
?e
should both be added as collection elements as well.)That allows a call of
foo(, if (false) 0, switch (0) { case 1: 0 }, ?null, 42)
to callfoo
with the first four arguments being omitted.It's the same as
foo(,,,, 42)
. It's an error if a parameter is not optional, and its argument element can evaluate to no value (more precisely: an<argumentElement>?
may evaluate to no value if it's empty, if it's anif
element that has noelse
branch, or where the argument element of either branch may evaluate to no value It's empty, if it's a switch element which is not exhaustive, or where any case element may evaluate to no value, or if it's an?
-element).It's new that non-trailing optional positional arguments can be omitted, and later parameters can be passed.
There is no good way to avoid that property while allowing conditionally passing an argument, not without completely disallowing that for positional arguments. Then we'd disallow
late
positional parameters too. That's an annoying asymmetry with named parameters, so it's better to just allow non-trailing positional parameters to be omitted. The majority of functions can't tell the difference (yet, without the??
feature), they'll just get their default values.(A few functions may deliberately use private sentinel default values to check whether an argument was passed. That's not generally possible, which is why it doesn't solve the
callWith
problem, but if the parameter type isObject?
anyway, any sentinel value can be used. Calling those functions incorrectly may give you a different result than you expected (well, what did you expect?). So don't do that. The functions will probably be rewritten to use??parameter
instead ofidentical(parameter, _sentinel)
soon enough.)Semantics
The semantics are straight-forward.
late
parameter with no argument value passed is uninitialized after "binding actuals to formals", behaving just like any other uninitialized locallate
variable. If an argument was passed, it's initialized to that value. The variable starts as potentially assigned, which means it's not a static error to read or write it, but may be a runtime error. Reading it will throw, if unassigned, writing will succeed if unassigned or non-final
. Static type is the declared type. (For acovariant
parameter, the parameter's local variable's type is the declared type, even if the function parameter's runtime type isObject?
. Same as always.)??parameterVariable
has static typebool
, the operator checks whether the late parameter is initialized or not. If used in a conditional position, the operator promotes the variable to definitle assigned on thetrue
branch and definitely unassigned on thefalse
branch. It's a compile-time error to use it on anything other than alate
variable name.if
withfalse
value and no else branch, non-exhaustiveswitch
with no matching case,?e
with anull
value, or just an omitted<argument>
), then the argument list has no value at that position (or at that name, ifnamed
, an omitted<argument>
is always counted as a positional argument). This is new, argument lists now need a way to represent "no value" at any position or name, in a way that can be acted on when binding actuals to formals. See first item.Consequences
It's now possible to write a
copyWith
:(I'm quite certain that the first request we'll get is to allow
??
as infix, treating an unassigned late variable the same as anull
-valued nullable non-late
variable. ThencopyWith
would be:That's ambiguous when a variable is both nullable and late, since reading the variable is allowed (it only might throw).
More likely the
??
will be saved for special cases, and you can still usenull
where it's not a valid value:Still, expect this request.)
It's also now possible to forward any known argument list, without knowing the default values:
or from
noSuchMethod
without an exponential blowup in the number of optional named parameters:The forwarding is more signficant than just
copyWith
. IfcopyWith
was the only problem to solve, allowing non-constant default values is enough:(Which we should just do. #140! But that does allow seeing whether an argument was passed using side-effects through external variables, so we might still want argument elements to counteract that ability.)
The text was updated successfully, but these errors were encountered: