-
Notifications
You must be signed in to change notification settings - Fork 207
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
Allow collection if
in argument lists
#2316
Comments
I believe this is asking for something similar to the second example in dart-lang/sdk#57203. |
Yep, if you solved that you would solve what I'm asking for in the example code. Technically this is asking for something broader. That other issue makes a good point that while we have existing syntax for representing the above example code, it requires duplicating or naming the default value which isn't ideal. Widget makeWidget(bool isFoo) =>
Bar(
a: 1,
b: 2,
c: isFoo ? 3 : defaultValueDuplicated,
); I was thinking of keyword parameters when I wrote this but it's general enough to cover Widget makeWidget(bool isFoo) =>
Bar(
if (isFoo) {
1
}, // <-- Compilation error for signature mismatch in some cases.
2,
3,
);
Widget makeWidget(bool isFoo) =>
Bar(
if (isFoo) {
1
} else {
2
}, // <-- This is fine.
2,
3,
);
|
With #2269, we could do this: Widget makeWidget(bool isFoo) =>
Bar(
a: 1,
b: 2,
c: isFoo ? 3 : default,
);
This is more powerful than changing the language such that null works exactly like not passing the argument, e.g., forwarding can tailor the placement of parameters in the parameter list: void foo([int x = 15, int y = 231]) {...}
// Forward to `foo`, but swap the argument order.
void oof([int y = foo.default[1], int x = foo.default[0]]) => foo(x, y);
void main() {
oof(null, null); // Compile-time error: Nullable types do not occur anywhere.
oof(default, -15); // Allows us to "omit" the first optional positional parameter.
} Note that we don't introduce any nullable parameter types at all, and hence we preserve the static type checking that prevents nulls at call sites. We are relying on default values that are (1) different from null, and (2) preserved faithfully in the forwarding call, without any duplication of code. |
I prefer dart-lang/linter#2232 |
@Cat-sushi, do you have an idea how you could then maintain null safety for optional parameters? That is, how would you help developers avoiding to pass null by accident, because they didn't remember that the actual argument is a nullable expression, not because they wanted to "omit that argument" by passing null? |
The advantage of the foo(
if (cond) x: 3,
y: expr,
if (!cond) x: "value",
) Here you seem to have the same named argument twice. Because So, you must only ever have one argument for each parameter, even a conditional one. The For positional arguments, we definitely do not want to move positions of arguments if an earlier one is missing. My proposal for something like this syntax would be to allow foo(
1,
if (cond) 42,
3,
x: if (cond) "bop"
); Here the values of the second positional and the This allows one new thing: Omitting a non-trailing positional argument. Grammar: argument ::= (identifier `:`)? argumentExpression
argumentExpression ::=
expression
| `if` `(` expression `)` argumentExpression (`else` argumentExpression)? |
You could disallow optional arguments from showing up in more than one argument's expression. foo(
if (cond) x: 3,
y: expr,
if (!cond) x: "value", // <-- Error "x:" is assigned in 2 separate expressions.
) foo(
if (cond)
x: 3
else
x: "value, // <-- Good.
y: expr,
)
That sounds reasonable. My only criticism is that it isn't as close to the |
Just to compare, assuming dart-lang/sdk#58240. Here is a tricky example from this comment: foo(
1,
if (cond) 42,
3,
x: if (cond) "bop"
); This would allow us to omit the second optional positional argument even though we're passing the third one (presumably by passing the default value or null), and it makes passing the named parameter foo(
1,
cond ? 42 : default,
3,
x: cond ? "bop" : default,
); This approach may be slightly more verbose than approaches where we actually introduce a mechanism that will omit the parameter under certain circumstances, but I think it does have a simpler semantics, and hence it may be more readable. And this one from here: foo(
if (cond) x: 3,
y: expr,
if (!cond) x: "value", // <-- Error "x:" is assigned in 2 separate expressions.
)
// OR
foo(
if (cond)
x: 3
else
x: "value", // <-- Good.
y: expr,
) This would use foo(
x: cond ? 3 : "value",
y: expr,
) |
My issue with The reason you can omit an entire entry in a map literal is that the map's type does not depend on the presence or absence of keys. Argument lists, and records, do. var r = (1, 2, x: 4, if (cond) y: 5); because the type of Argument lists are not records, they either have a "context type" (the function's parameter list) or it's a dynamic call anyway. In the former case, we know the parameters that are not getting an argument. |
I don't see a problem in that:
There's no limitation on the placement of an expression of the form
This is exactly the reason why I think the approach where "passing or not passing an argument" is using So that would be an argument in favor of using |
IMO, How would void f([int? a, String? b, bool? c]) {
print('$a, $b, $c');
}
// called like so
f(if (cond1) 5, if (cond2) 'A', if (cond3) true); I assume the parameters would be assigned |
collection if
in parameter listscollection if
in argument lists
I'm starting to lean very strongly towards this feature, a collection-if like absence of a value, as the preferred way to handle passing or not passing arguments. We can use (A way to extract the default value of a function parameter, as an expressible value, simply doesn't work. You don't actually know that the default value has a type that is assignable to the static type of the function's parameter, as you know it.) So, proposal: DesignWe introduce a conditional argument, using a collection-if like syntax. Example: foo(if (v != null) v); // Positional argument
bar(baz: if (v != null) v); // Named argument These calls will either pass a non-null value as first positional/ (We also want to introduce a null-aware element, If an argument-element doesn't evaluate to a value, either because its An argument value being elided differs slightly from having no argument. Also, calls like Because of that, we might as well allow unconditionally eliding positional arguments, A Generally, you can only elide values for optional parameters. You can elide arguments in dynamic invocations, in which case the argument list may have holes, GrammarThe current argument grammar, <arguments> ::= `(' (<argumentList> `,'?)? `)'
<argumentList> ::= <argument> (`,' <argument>)*
<argument> ::= <label>? <expression> becomes <arguments> ::= `(' <argumentList> `)'
<argumentList> ::= (<argument>? `,')* <argument>?
<argument> ::= <identifier> `:' <argumentElement> | <argumentElement>
<argumentElement> ::=
<expression>
|`if' `(' <expression> `)' <argumentElement> (`else' <argumentElement>)?
| `?` <expression> The Static semanticsWe infer types for each argument position as usual. If there is a corresponding parameter type in the static function type being invoked, we use that parameter type as context type for the The static type of an The static type of an Type inference of
where we perform element type inference on an
Effectively we introduce a new kind of bottom-type, representing no value instead of non-termination. Runtime semanticsAn runtime argument list can now contain elided values, which the implementations can implement however they want. Calling with an elided value triggers a default value, exactly as passing no argument at all. A dynamic invocation will throw an error if an elided value is passed to a non-optional parameter, or if the function does not have any parameter at that position. A Later workIf it's possible to control the arguments in a function call to this degree, Without the ability to easily control whether an argument is passed or not, forwarding arguments can be prohibitive if the called function can check whether each argument was passed or not. Possible approaches could be:
|
Interesting! I think it's definitely worth considering how we could enable provision/non-provision of arguments to optional parameters in a programmatic manner. On the other hand, this idea does seem to impose a non-trivial amount of complexity on the meaning of call sites.
Well, if we have support for // This is what we have.
void f([int x = 42, int y = 314]) {...}
// But someone wants a callback where the parameters are swapped.
void g([int y = f.default[1], int x = f.default[0]]) => f(x, y);
// Or they want to use named parameters.
void h({int x = f.default[0], int y = f.default[1]}) => f(x, y); So I don't think it's fair to say that it doesn't buy us anything to use an explicit denotation of a default value.
I commented on this topic here, which seems to be a more relevant context. |
Allowing a sole Allowing a |
Of course, it isn't difficult to reach the conclusion that "this approach doesn't contribute anything of value" if you start by implicitly redefining "this approach" in such a way that it contributes absolutely nothing.
No, that's exactly the kind of feature that I proposed in dart-lang/sdk#58240 and referred to several times in this issue, starting here. This confirms very directly that you are using a straw man argument. Never mind, here's a real argument:
There could be several reasons why that perspective is suboptimal: A default value which is declared in the way we use today, considered as part of the API, ...
It is of course highly error prone to declare a default value for a parameter in an instance method declaration, and then declare a different default value for the "same" parameter in an overriding declaration. I created dart-lang/sdk#59293 in order to maintain that there should be support for detecting this situation, and acting on it.
It clearly has to be a well-documented property of an instance method declaration what the semantics of omitting a given actual argument is. Saying "you can pass this argument or omit it, but it is none of your business what happens if you omit it" is not very helpful. Sure, you can write a long story about said semantics in a DartDoc comment. I'm just noting that a plain expression in the declaration of the optional parameter may be a useful, readable, and concise way to document this behavior. The fact that it is possible to write error-prone code where a given default value is overridden by a different value is unfortunate. But (1) it's easy to fix, just enable that lint and correct any discrepancies, and (2) the approach where the default value is considered to be an implementation detail does not eliminate this problem, it just turns the problem into a mistake that we won't get any help at all to avoid. |
It's inconsistent that Dart allows conditional expressions in collection literals when building Lists, but not in parameter lists. Parameters are just collections that are used to invoke a procedure.
I propose we fill out
collection if
's implementation to support parameter lists.example:
I realize there are existing ways to express this, my proposal is solely based on making the language consistent. I've written constructs like above only to remind myself that
collection if
only works in certain contexts. It is more glaring in Flutter applications where the tree of your application is represented in a large statement composed of constructors and collections andcollection if
only works sometimes.The text was updated successfully, but these errors were encountered: