-
Notifications
You must be signed in to change notification settings - Fork 213
Yet another proposal for the problem of default values #1492
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
Neat! If I'm understanding right, in typical copyWith you would have something like this?
In this case, user can pass in a value, null, or not pass at all, and we get the 3 behaviors we want. That seems really nice to read, and feels familiar as well due to it's similarities to the constructor. The one drawback is obviously boilerplate. Take a class with 8 properties, create fields, constructor, defaults and copyWith body, and you end up with quite the boatload of repeated code. I wonder if this could be made more succinct on the callee side somehow. In this example what is really 8 lines of intent, becomes 45 or so lines of derived code. Meta-programming would obviously make this a non-issue though :) |
A quick example with 6 vars, if I'm understanding right. Granted I guess this is only ever going to be 25% more lines than the existing approach, so it's no major difference.
|
I don't like it, because it works only for methods. The issue with default values applies to static functions and constructors too, especially in the context of "composition". I'm still convinced that what we want is union types: class Undefined {
const Undefined();
}
void fn({int? | Undefined param = const Undefined()}) {
if (param is Undefined) print("default")
else print(param)
}
fn() // default
fn(param: null) // null
fn(param: 42) // 42 |
I have a strong preference for the union types approach. It feels more natural to me, it's concise, and it's similar to other languages. |
For me at least union types are not at all natural. Maybe they are more natural if you are used to them from other languages, but in terms of Dart, the Also if Undefined is not a standard thing, but instead re-created in every single project, I think the cure might be worse than the disease really. |
Still, this proposal does not solve function/class composition, which is a very important part of the issue with default values. We need to have a way for anything that can take parameters to differentiate null from no parameter, not just methods. A common use-case would be for composing TextField for example, which default to a max lines of 1, but has the ability to set the length to infinity (without relying on double) With unions, we can do: TextField({int|"infinity" maxLines ??=1}) (??= is just syntax sugar for default value that update the parameter if the parameter is null) This means doing: class MyTextField {
int? | "infinity" maxLines;
build() => TextField(maxLines: maxLines);
} Will behave like: MyTextField() // 1 line
MyTextField(maxLines: 42) // 42 lines
MyTextField(maxLines: null) // 1 lines
MyTextField(maxLines: "infinity") // infinite lines This solves the composition Issue. we are now able to compose TextField into a MyTextField, without losing the built-in behavior or "by default, maxLines is 1" or having to replicate the default value of TextField into MyTextField |
Composition. It's a custom-made widget, potentially for theming purposes. Here's a more realistic example class MyTextField extends StatelessWidget {
MyTextField({Key key, this.maxLines}): super(key: key);
final int? | "infinity" maxLines;
@overrides
Widget build(context) {
return TextField(
style: TextStyle(color: Colors.blue),
maxLines: maxLines,
);
}
}
MyTextField() // 1 line
MyTextField(maxLines: 42) // 42 lines
MyTextField(maxLines: null) // 1 lines
MyTextField(maxLines: "infinity") // infinite lines
No, this is not a mistake. It is a side-effect of the That's just syntax sugar for: void foo({int? count}) {
count ??= 42;
} |
This is a very heavily used pattern in Flutter, where you routinely wrap some more complex MaterialWidget, providing some default values of your own, but then letting the rest pass through. Seems like we already have double.infinity tho, so seems like there is an existing workaround for this, which is fairly readable? |
Compositon is definitely not an edge case. I would argue that in the context of default values, composition is a more important scenario than copyWith. To begin with, copyWith could be implemented without default values: a spread operator If we can have a spread operator like with collections but for classes, that would remove the need for writing copyWith methods, removing the default value issue completely. |
There would be no copyWith class Person {
Person({this.name, this.age});
final String? name;
final int? age;
}
void main() {
final john = Person(name: 'John');
final copy = Person(...john, age: 18);
} Such spread operator could inject the properties of an object in the named parameters of a constructor/function |
Spread operator is different from manually passing named parameters. With spread operator, the following is valid: var first = {'key': 42, 'a': 'a'};
var second = {'key': 21, 'b': 'b'};
var merge = {...first, ...second} // {'a': 'a', 'key': 21, 'b': 'b'} The same logic applies to object spread: the last one takes over.
Yes, because there should not be a difference in behavior between properties and getters, as this would otherwise make refactoring properties into getters a breaking change.
Why would the compiler not know?
I don't see the issue. This use-case is obviously useless, but the ability to spread multiple objects is definitely useful. |
The spread operator is statically analyzable. So if the constructor doesn't take a parameter, the spread isn't passing it. |
It does seem like it could get messy really quickly when you look at it like that. It would work, but it has too much implicit behavior. It wouldn't support refactoring well, as you're coupling an implicit field name to an implicit method param, change either and the link quietly severs. The default method seems more focused/explicit and less open to abuse, and no issues with refactoring mysteriously breaking things. |
I think it's better to move all possible solutions into the replies and only keep the problems in the first post. People would thumb it without distraction and the dart team will put a higher priority on it. We could discuss the solutions and rate ideas on different replies. |
My opinion is to have a value for the absent value condition. It should work like Some examples: void test({int? arg}) {
if (arg == null) { print('null'); } // null situation
else if (arg == undefined) { print('undefined'); } // the arg is undefined, so we could assign default value here.
else { print('$arg'); } //do normal stuff
}
void main() {
test(); // "null", Currently, if no default initializer, the value is null, not undefined.
test(arg: null); // "null"
test(arg: undefined); // "undefined", a new way to tell the function that the argument's value is undefined.
test(arg: 1); // "1"
var a = undefined; //ok, a is dynamic type, and the value is undefined
var b = a; //ok
test(arg: b); //ok, works like test(arg: undefined)
var c; // c = null, by default, not undefined.
} To keep the current default behavior, the undefined value should only be used explicitly. Or it will fall to a |
@tatumizer The specific problem would be most elegantly solved in the following way: class A {
final int? x;
final String y;
A({this.x, this.y});
A copyWith({int? x = this.x, String y = this.y}) => A(x: x, y: y);
} This is also how Kotlin allows for a copy method. See this issue: I agree with @rrousselGit, that if you want more control than just allowing for nonconstant default parameter values, then probably union types are the way to go. However, I doubt if there are many common problems that could not be solved by allowing non-constant default parameter values. Do you have more examples? Because I can not think of a time where I needed this in Kotlin. |
See also: |
Problem description (copied from another thread)
In short, the problem is twofold:
Suppose you have a function
foo(int i=42)
, but in your program, there's a nullable variable x, and you want to say:foo(x !=null ? x : USE_DEFAULT)
- but how are you going to say "USE_DEFAULT"? There's no way to do so.How are you going to find out whether the caller passed
x
ory
or not? Well, fory
you can change the type toString?
and check for null (which, by the way, is against NNBD principles), but what aboutx
? For,x
is nullable, it can't be made even more nullable to check the condition.Proposed solution:
foo(if (cond) expr);
. We may, or may not, want to support a special case forfoo(if (x != null) x)
in the form offoo(?x)
or equivalent. This point is not important for the proposal. What is important is that there's SOME way to pass an optional parameter conditionally.The meaning of
if (cond) expr
is: ifcond
is true then passexpr
else USE DEFAULT. It's important to avoid formulating it as "ifcond
is true then passexpr
else pass nothing". No, the whole point of it is to avoid using the word "nothing", otherwise "nothing" itself turns into "something" which simply has the label "nothing" attached to it, and we are entering a rabbit hole of increasing degrees of nothingness. We don't want that. But how to find out on the callee side whether the parameter was passed or not without mentioning the word "nothing" directly or indirectly? Here's where the second part comes into play(For a constructor, "default" clause should precede the initializer list).
Note that we never mentioned "nothing"! That was the whole point! :-)
EDIT: Another problem was raised in the discussion (by @rrousselGit)
Possible solution: support
late
declaration for a parameter. Example:When you declare your parameter "late", the compiler doesn't complain until you are trying to access the variable inside g, which you never do (other than in forwarding).
The text was updated successfully, but these errors were encountered: