-
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
Problem: Syntax for optional parameters and required named parameters is verbose and unfamiliar #15
Comments
Have we seen any problems with people facing this syntax? I don't recall anyone even blinking at this syntax in the Flutter usability studies, but it's been a while. cc @InMatrix |
@Hixie I think it obvious that if you come from any other language, and have no experience with Dart, that you are not going to guess this syntax yourself. Or find it by trial and error. You have to learn this syntax by reading documentation or googling it. Personally, I have done a lot of development with Dart in the past. Not done much the last 2 years. And I'm now actively learning Flutter (and relearning the latest changes to Dart). I have googled "how the do the required named parameter in Dart" at least 2 times when learning Flutter. Before remembering, "Oh wait I needed to import a package for that." |
I see current behavior of named parameters as a really nice feature actually, they scale really well when API evolves. Also proposal in #16 turns the problem 180 degrees and enforces verbosity for "new optional named arguments" by requiring to always specify default value. Re: optional args - even though square brackets are not often used in those languages it's how they are documented in many of the above mentioned languages (maybe even most), ex.: php, nodejs. So current syntax is familiar in this regard. I'd be surprised if a developer coming from a language like JS or PHP would be confused by this syntax. For me it seems like Dart is just following the idea of code being the best documentation. Personally I like current syntax as it's a lot easier to parse and it does look familiar to me as a developer coming from other language(s). |
To note, though I'll add more on #16, |
@Hixie We had study participants (new Dart users) call methods with optional parameters and named parameters, and no obvious issues were observed. However, we have few observations of new Dart users constructing methods with those types of parameters. |
This is the bit that bothers me most here - if one of our major platforms is going to the trouble to use an annotation to get this minor feature, it's a pretty strong signal that this is something missing. I'd like to see us incorporate this into the language. |
Agreed @leafpetersen. And that's not even counting the issues with forwarding today with named arguments with default parameters; if I remember correctly @munificent is quite aware of the issues here: void method1({a = 1}) => print(a);
void method2({a}) => method1(a: a:);
void main() {
method1(); // "1"
method2(); // "null"
} |
In practice, package:meta is part of the language. Whether people put an |
It's perfectly possible to write code without using That is, in part, why it's so successful. It can add restrictions that are too strict for everybody, something we would never do at the language level. As for "required", I think it is a good idea, but that it is probably not really worth doing by itself. With non-nullable types, we could just make any nullable typed parameter with no default, and any parameter with a default, optional. Then: void foo(int x, int? y, int z = 42, {int u, int? v, int w = 37}) I'd like that. |
I don't think this works out well for positional parameters for all of the subtyping and call reasons we've been working through with Bob's UI as code proposal. For example: void f(int? x, int? y) {print(x);};
f(3); // prints 3
void Function(int? x, int y) g = f;
g(3); // prints null |
Yeah, I don't think we should make nullable positional parameters implicitly optional because it affects all later parameters in ways the user likely doesn't intend. But for named parameters, where each parameter is independent of the others, I think it might work (though I haven't put a ton of thought into it). I would like to support required named parameters but so far haven't come up with a syntax I like. |
What's wrong with what we have today? |
The language doesn't have required named parameters, so if you are not using the analyzer, you won't even get a warning if you don't pass a value for a If it were to become a language feature, then using an annotation is not good design. It's also problematic that even if we did that, we'd have to add a |
I think we should tackle this problem after we have nullability in the language, since the two seem to interact in ways that might require that we look into real world usage patterns. |
I don't think this matches our experience or the experience of our developers (that is, people using Flutter). The analyzer is an integral part of the Flutter/Dart development experience, in the same way that debug mode asserts are an integral part of the development experience. You can certainly artificially limit your experience by not using the analyzer or not using debug mode, but I don't think it's worth optimising for people who do that because they're artificially making life harder on themselves. It's not clear to me there's anything to be gained by making apps crash if you omit the "onPressed" handler for a button, say (that's one of the arguments we have marked
Flutter re-exports
From the perspective of our developers, the difference is a single Based on our experience when we went from the |
As a general principle, I don't like delegating core language features to annotations. If something is broadly used as an annotation, that's a strong signal that there's something missing from the language. Sometimes there's not a good way to incorporate something cleanly into the language, or sometimes the feature is too niche to adopt. But something like this seems very clear to me. It's widely requested, widely used via an annotation, it's something a compiler could use to generate better calling sequences, and it slots naturally into existing features (we have required and optional positional parameters in the language, why not required named?). There's certainly a valid point in the language design space which is basically Lisp + annotations, but I don't think Dart should aim to be that language. |
The example of I am a little concerned about letting a framework (any framework, including mine) dictate the definition of the language by adding new annotations and then treating them as part of the language, but at the same time I do wish we had more static analysis checks than we have now and understand annotations are the only way to do them cleanly and non-breaking. |
I agree that any annotations added as analyzer extensions would not be considered part of the language. The ones I listed above are all supported natively by the analyzer, though, and I think it's fine to consider the analyzer to be part of Dart. (We will hopefully one day also add our own annotations and an analyzer plugin, but we're not there yet. I would not consider those part of Dart in any meaningful sense, only part of Flutter.) My original point was just that we already have required arguments, with a syntax that works, and which people demonstrably understand and use, so it would make sense IMHO to keep using it (or something very close to it, e.g. just removing the |
+1.
We've gotten a lot of feedback over the years from many users that hints, lints, metadata annotations and other pseudo-language features cause a lot of confusion and fragmentation. One of the things users tell us they like most about Dart is that it's opinionated — there is usually a single canonical blessed way of doing something. I think we should have an opinion about whether required named parameters are worth doing or not. If they are, lets do them right and give them good syntax full integrated into all of our tools and type system. (Personally, I think we should have them, but I think it might be best to integrate them with non-nullable types.)
+1. The last thing I want to do to parameter lists is add more punctuation. The If I had to pick a syntax today, I would suggest just |
Does it make sense to have a required parameter that is nullable? Also adding a default value would automatically require the parameter to be optional, so a default value could also be used to distinguish required from not-required fields. (update - just saw that's discussed in #16) This way a new keyword wouldn't be required. If something like allowing all parameters to be used like named or positional parameters (removing the distinction between positional and named) or rest parameters are considered, (and perhaps others I forgot) this should also be taken into consideration when changes to parameter syntax are discussed. |
FlatButton.onPressed is required and nullable. |
@Hixie right, I think I saw you mention this example already. |
We use "null" to indicate there's no behaviour and so the button should be disabled. |
As I have not yet seen this idea, and as one of the many things I like about Dart is that it isn't as verbose as some other languages and still easily readable, how about: void f(int a, int b, required {int c, int d}); It's less verbose than adding the keyword to every parameter (or setting every optional parameter to null like in #16) you want to make required and named and it doesn't change the meaning of void f(int a, int b, {required int c, int e, required int d}); and will have to do this instead: void f(int a, int b, required {int c, int d}, {int e}); Furthermore (in my opinion), this declaration would also clearly state that those parameters are required when overriding a method. No idea whether anyone would want to have it any other way. Possible other syntax instead of |
Putting |
Having I'd imagine that if required is the intention for >30% of cases, it'd be a better default. Then we could hopefully allow explicit optional:
and make a lint for implicit optional, such that we could switch the default for maybe dart 4. |
When I last looked at the Flutter repo, the wide majority of named arguments were not required. Other repos aren't a good signal since most users don't know I do generally think optional will be the norm for named args even if we put required named arguments into the language. A big part of why you want to be able to pass by name (which is more verbose) instead of position is to be able to pick and choose which you pass. If they're mandatory, it starts to make more sense to make them positional. |
I think it much more than that. Named arguments provide certain flexibility that positional arguments do not. For example, a named argument that is required today may become optional in the future without breaking the API. Many (if not most) of Flutter named arguments map onto class properties with the same names, and many of them share types (e.g. TextStyle). Argument names make the code much more readable by telling you which property is being set without you having to deduce from type, which is inexact, or memorize which property corresponds to which position in the argument list. I kinda like @denniskaselow's suggestion. Another variant might be to allow making positional arguments nameable via a
But @denniskaselow's suggestion composes better with the existing notion that everything inside |
I quite like the annotation the biggest issue IMO, is the fact that the compiler does not break if the param is missing - hence no feedback to the developer. Positional param Annotated Params at the moment for my apis to enforce the required state |
Note that required named parameters will be introduced as a language mechanism with NNBD (cf. this section for the syntax). This means that it will be a compile-time error (so there's no need to enable a lint, and it cannot be ignored) to omit such a parameter in an invocation, and the function type subtype rules will ensure that this property is not "forgotten" during an up- or downcast. |
@eernstg But wait, what does it mean, if you have a non nullable parameter without a default value, which is not annotated to be required? So foo({int bar}) for an example. If I now call foo(), do I get a compile time error, as bar is non nullable? I guess it must be. Is it a different error then when I write foo({required int bar})? |
That's a compile-time error in the declaration of the parameter, so there's no need to worry about how it would work for invocations. A declaration like |
@eernstg Hmm.. what about inferring it to be a required parameter? You could safely do that. |
Indeed, and we discussed that option, too. It is a trade-off between making the code concise and well-documented (for instance, a type variable |
Can't we have both? void foo({int a});
foo() // compile error, a is required
void bar({int? a});
bar() // valid
void baz({required int? a});
baz() // compile error, a is required
baz(a: null) //valid
void qux({int a = 42});
qux() // valid
qux(a: null) // compile error And then for generics: void foo<T>({T a});
foo<int>() // compile error
foo<int>(a: 42) // valid
foo<int?>() // valid
void bar<T>({T? a});
bar<int>() // valid
void baz<T>({required T? a});
baz<int>() // compile error
baz<int?>() // compile error |
@rrousselGit I would really like this. In practice this would mean that I can skip the required annotation allmost everywhere I use it. |
That's an interesting idea for the generic case. It basically means that the parameter But One example is an instance method: abstract class C<X> {
void foo({X x});
}
main() {
C<int?> c = Random().nextBool() ? C<int>() : C<int?>();
c.foo(); // Compile-time error, could be a `C<int>`.
} Another difficulty is that the ability to recognize that the named parameter is sometimes-optional disappears when we assign a function to a function-typed variable or parameter: void foo<X>({X x}) {}
void Function<X>({X x}) f1 = foo; // Error!
// This is OK, but forces `x` to be always-required, no matter which
// `X` we have at a call site.
void Function<X>({required X x}) f2 = foo; Function types in Dart do not support declarations of default values, so whenever we pass a function as a first class value we need to represent the requiredness of any given named parameter explicitly (hence So the initialization of On the other hand, the initialization of So we do get a little bit of extra flexibility, but that flexibility tends to evaporate as soon as we combine this idea with other language mechanisms. |
We discussed this option, but decided against it because it makes things confusing when you interact with function types, for which you would need to write
|
To be fair, it's default values that are confusing, not that suggestion: void foo({ int x = 42}) => print(x);
foo(); // 42
foo(x: null); // null There's a difference in behavior between both calls, but the type definition doesn't suggest that there's any difference at all. With non-nullable types, I think we could include that behavior inside the type definition itself: typedef NullableOptional = void Function({ int? x });
typedef NonNullableOptional = void Function({ int x = }); Which translates into: NullableOptional a;
a(); // valid
a(x: null); // valid
NonNullableOptional b;
b(); // valid
b(x: null); // compile error |
That's a fair point. In that case, an alternative is to disable the inference for generics such that: void foo({ int a }); compiles by being inferred to: void foo({ required int a }); but: void baz<T>({ T a}); doesn't compile at all. |
Agreed, we could do some or all of those things. At the same time, I think this illustrates that this would introduce additional complexity for any reader of Dart code, mainly because we gave such a high priority for writers to have the option to omit the word |
Right. So having to write this I'm not too sure how to have real stats backing that statement though. |
There is one device which could help with the verbosity while keeping requiredness explicit: void foo(required {T1 n1, T2 n2}) {...} // All named parameters are required. |
I believe that using |
I'm not sure where I wrote up the results, but I did some digging into use of From that, I think it implies we don't want a syntax that forces all required named parameters to be separated from the optional ones. |
There's no need to force anything, |
Yes, in other words, a named parameter with no default value is required iff it is not possible to give it implicitly the default value of About your example: abstract class C<X> {
void foo({X x});
}
main() {
C<int?> c = Random().nextBool() ? C<int>() : C<int?>();
c.foo(); // Compile-time error, could be a `C<int>`.
} What would happen in this example with the current NNBD/required proposal? According to the type definition the parameter x is optional, but if I would do: var c = C<int>();
c.foo();
// error, int can not be null It would follow that in this instance the parameter So I would already expect either a compile time error in the declaration of the parameter that abstract class C<X> {
void foo({X x});
} If you want the parameter abstract class C<X> {
void foo({X? x});
} This is also how it works in Kotlin, if you want a parameter of type generic X to be optional, you should make it nullable: |
@kasperpeulen Example: class Foo {
int foo({int x = 42}) => x;
}
int Function({int x}) f = Foo().foo;
f(); // Allowed? If not, which type *should* f have to make it allowed? It means that the type system for functions needs a way to recognized optional parameters, even if they are non-nullable. |
IMO, in a context where int Function({int x}) f = Foo().foo; As I said here #15 (comment), I think it's important to include default values as part of the function definition. Doing so allows optional non-nullable optional parameters: int foo({int x = 42}) => x;
foo() // valid
foo(x: null) // compile error |
@kasperpeulen wrote:
Exactly, which is the reason why that whole concept (of making
Right, we could rely strictly on the type, but this means that it would be impossible to specify that a parameter has a non-nullable type and may still be omitted (because there is a default value). And, who knows, it might even be inconvenient that you also can't specify that a parameter has a nullable type and is required. For instance, the only way we can allow a function f of type |
The current syntax for optional positional parameters is:
The current syntax for required named parameters is:
Both syntaxes are not used in any other mainstream language that I know of and are unnecessary verbose compared to possible alternatives.
The text was updated successfully, but these errors were encountered: