-
Notifications
You must be signed in to change notification settings - Fork 205
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
[proposal] non-nullable named parameters required by default #878
Comments
One issue with that is that it might get confusing when you use a type variable. T foo<T>({T value}) => value; would you then expect that parameter to be required when Another thing is that default values also make a difference. It's possible to have a required nullable parameter and an optional non-nullable parameter (if it has a default value). That again makes things harder to read if we make too many things implicit. So, this is a case of favoring explicit over implicit because it makes the code easier to read and maintain. |
This is a readability/writability trade-off. It would not be hard to define and implement a rule that a named parameter of a function or a concrete method is required in the case where it has a potentially non-nullable type and no default value. This makes it a bit easier to write, but a reader of the code would have to compute the requiredness of the parameter based on various properties. However, that computation isn't entirely trivial, and one case couldn't even be expressed any more. An abstract method declaration can have such a parameter (potentially nullable, no default) which is not required. That is useful because an abstract method doesn't ever require a default value for any parameters, and there can be a default value specified in each implementation of the method, or the parameter could have a nullable type in some implementations: abstract class A<X> { void foo({required X x}); }
class B1 extends A<int> { void foo({x = 42}) {}}
class B2 extends A<double> { void foo({x = 1.61803}) {}}
class B3<X> extends A<X?> { void foo({x}) {}} The overall point is that even though the |
If the concern is about readability vs writability, I wouldn't worry too much about the former. This could be the job of the IDE instead of the language to fix the readability issue, as shown with the auto-generated closing tags and the UI guides I can see a world where the |
I agree with @rrousselGit. This is another use-case where we need to over-write the class User {
final String id;
final String email;
final String userName;
final String address;
final String phoneNumber;
final String name;
final String? avatarUrl;
User({
required this.id,
required this.email,
required this.userName,
required this.address,
required this.phoneNumber,
required this.name,
this.avatarUrl,
});
} We can think of this feature as:
|
There is a lot of discussion of this question on this issue. Some comments that might help clarify: 1, 2, 3. |
To be clear, the existence of It, however, has everything to do with not being able to select a default value due to type constraints. Those constraints are... non-nullable types. A nullable parameter can be required. A non-nullable parameter can be not required. Argument lists today already have a virtual Being explicit about what's required makes future language developments possible, like being able to specify the default value for a type, enabling you to have the same behavior as null does for nullable types. Not being explicit means that adding a default value mechanism and having it mesh well with required parameters turns into a breaking change for anyone (Dart SDK libraries, or 3rd party libraries) to add a default, making it only useful to have for newly created classes. Also. I firmly disagree that languages should depend on IDE tooling to be readable. Code review happens outside of IDEs, you're pushing the IDE's work onto them during review-time. The goal should be to provide readable code without IDE intervention. |
The problem is not about having the keyword or not. Its presence is necessary for many reasons. A metaphor: Types have a type inference for the obvious situations. The {required int a} is not more readable than {int a} Adding MyClass value = MyClass();
// vs
var value = MyClass();
MyClass value = null;
// vs
MyClass value We could simply keep the keyword, and have it inferred when it is obvious. This would allow: // required
({ int value })
({ required int value })
({ required int? value })
<T>({ required T value })
<T extends Object>({ T value }) // T is non-nullable, so equivalent to {int value}
// optional
({ int? value })
({ int value = 0 })
<T>({ T? value })
<T extends Object?>({ T value }) // T is nullable, so equivalent to {int? value} And it would not allow: <T>({T value}) // T can be both nullable and non-nullable. Inference fails |
Sorry I didnt get it. Why I would have a required parameter that can be null |
I wouldn't necessarily recommend that, but it's definitely possible. You can also run into the situation with generic classes. class Box<T> {
T value;
fill({required this.value}) { // Because you *like* to write `box.fill(value: x)`
this.value = value;
}
} then a (I personally never got the idea of required named parameters at all, but other people seem to like them, mostly for readability reasons, but also for ordering). |
This is a fair metaphor, but we have to be really careful about swinging the inference hammer. Inferring X based on the presence of Y works well when almost all readers expect that implicit magic to happen. But for any reader who doesn't expect that, they will be very surprised if they write Y and an unasked X's semantics appear in their code. Even when they do expect that to happen, they may be very unpleasantly surprised if they move X into a slightly different context where inference no longer kicks in and the Y they expected gets dropped on the floor. Type inference is relatively safe because all users of statically-typed languages are familiar the basic mechanism of type propagation. If they write If we do "required inference", we don't have the luxury of any pre-existing user intuition we can build on. This is a relatively novel features with completely novel syntax. If we add inference, we will have to laboriously teach every Dart user about it, because they have no reason to expect it would be inferred. I personally don't think the brevity benefit of inferring |
Would it be worth making a poll about it? I do have the impression that most people are surprised that the required keyword is required. It'd be great to have numbers backing (or contradicting) that. After all, popular languages like Typescript don't have that |
Can you elaborate? My understanding (which could be wrong, I'm not a Typescript expert) is that
So I don't really see the connection. It's true they don't have required named parameters (and hence don't have a For what it's worth, we did consider marking optional parameters instead of the required ones. I believe @munificent did a corpus survey of Flutter code and found that the vast majority of named parameters were optional (correct me if I'm misremembering, Bob). |
There is technically no "named parameters", but typescript has structures and destructuring, which the community uses for it Here's some Dart code: void example(int a, {required String b}) {}
void example2({int? a, required String b}) {}
void example3({int a = 42}) {} With the typescript equivalent: function example(a: number, { b }: { b: String }) { }
function example2({ a, b }: { a: number?, b: String }) { }
function example3({ a = 42 } = {}) {} Which allows: example(42, {b: '42'})
example2({a: 42, b: '42'});
example3()
example3({a: 21}); The behavior is pretty close to Dart in that "named" parameters are always named and with a syntax fairly similar if we removed types. |
Common Lisp, although a great language, also has When dealing with parameters, "normal" parameters are required by default, and named parameters (aka keyword parameters) are all optional by default, may receive a default value and optionally a binding that says if the parameter has been given. The design works flawlessly and I don't think the usual programmer expects a named parameter to be required. However, Common Lisp has no null-safe types, and it really don't make sense to expect a non-nullable type to be optional. A non-required parameter implies that, if I try to access this parameter from within the function I will get a default value. In this context, I can think on some possibilities:
Considering this, I think that (a) named parameters on non-nullable types should be required unless a default value is provided or (b) named parameters on non-nullable types should be required and a default value should be obligatorily provided. |
You're correct. The relevant numbers are in this comment. I found only about 20% of named parameters were required. |
I get some syntax errors from your code above if I try it out, but here's a fixed example What I think I see from your example is that TypeScript makes you to mark optional named parameters, whereas in Dart we're proposing to make you mark the required parameters. This choice was made in part because of the corpus analysis from @munificent referenced above. As far as I can tell, Typescript doesn't do the kind of inference you're proposing here. Required vs optional is orthogonal to nullable/non-nullable. You can have a nullable required named parameter Am I misunderstanding something here? |
The code was valid. The Technically, if we wanted to have the |
If I paste your example into the TypeScript playground, I get the following error on the
I don't know if this is relevant though? My main point was my takeaway, which is that TypeScript does not do the inference you are proposing. The only difference in TypeScript that I can see is that they make you mark the optional parameters instead of the required parameters. Do we agree on this, or am I misunderstanding (very possible, again, I don't program in TypeScript so I'm just exploring it by reading and experimenting). If so, can you give an example? |
Ah, my bad, I fixed it. I placed the
Yes, because it doesn't have the
Yes, but Dart makes us mark optional parameters too:
The difference is that Dart makes us also mark required parameters. whereas Typescript does not. |
Another way of formulating my concern would be: the
Also, random thought: void example({ int! a }) {} That's a lot easier to refactor/write/read. And it matches with |
I'm still really missing something. I do understand that:
Is your point just that you prefer one of the two choices above? Because otherwise, the TypeScript approach is exactly identical to the Dart approach. In both Dart and TypeScript, there are four possible combinations: required/optional and nullable/non-nullable. In both Dart and TypeScript all four combinations can be expressed. The only difference is whether required or optional is the default, and whether you use a keyword or a piece of syntax. It is true that in Dart the optional/non-nullable corner of the matrix requires you to put a default value on the argument, since Dart doesn't have undefined, but that really doesn't seem relevant to me here. |
I've just noticed that there's a difference in typescript between You're right then, especially about the:
I think I've better formulated my concerns about that difference on my second message #878 (comment) |
Ack.
We did really consider it. I believe for all of us involved (but certainly for me) the deciding factor is the fact that it doesn't work for function types - only function definitions. That means that you have the same syntax in a function definition meaning a different thing in a function type. typedef F = void Function({int x}); // This is the type of a function with an optional named parameter
void f({int x}); // This is a function with a required named parameter
F g = f; // This is a type error, even though the types "look" identical. WAT? That's just weird, and surprising. It also means that you still need to have typedef F = void Function({required int x}); // This is the type of a function with a required named parameter
void f({int x}); // This is a function with a required named parameter
F g = f; // Now we're ok. Ok, so that means we still need to have an explicit syntax for function types. But it turns out, we also need it for function definitions, because there are use cases for required nullable parameters (Flutter Framework has APIs of this form). So now we have:
Two different ways to write the same thing, and the same thing meaning different things in two different places. Yuck.
This was definitely kicked around at one point, I forget whether as |
I admit I only skimmed this thread but with NNBD do we need Although in some cases like Flutter's I'm trying to think of a situation where you'd need |
Same here. If I don't provide a value to a non nullable the analyzer/compiler surely will tell me. A required keyword only makes sense to me if I want to enforce something that the compiler doesn't enforce anyway. Curious, is the |
Required named parameters are not necessary for null safety, it is a new feature added along with null safety. We could have chosen to allow only optional named parameters even with null safety. It would just be less convenient for those situations where authors actually want the parameter to always be passed, where they currently use As for whether the |
I'll admit that I'm still quite fearful about the impact of this Named required parameters are needed only for improving the invocation readability, but this keyword decreases the readability/writability of the declaration. At the very least, with |
@rrousselGit If you previously wrote If you want a parameter with a non-nullable type, then you need either to make the parameter required or give it a default value. All in all, I think things are better with null safety. Unless your code was not null-safe before, then it hurts, but that's what null safety is all about: Making non-null-safe code hurt to write 😁 |
I know there is probably no likelihood of this happening but I just wanted to mention that for me personally it’s starting to get more annoying over time. On top of having the keyword everywhere, the code generated by code actions is usually wrong. |
I'd definitely suggest filing issues for these. cc @bwilkerson @scheglov
I hear this, but I'm very hesitant to have a practice of leaving issues open that are not actionable for us, since it makes the issue tracker grow increasingly cluttered over time. @lrhn thoughts on this? |
I would love to see this reconsidered for Dart 3. 90% of my parameters are named and required. 1% of my named parameters are nullable and required. It's a worthy change. |
There is a distinction between Flutter (widgets) and non-Flutter classes in the analysis server. @mateusfccp do you want this only for Flutter? Mostly for Flutter? For non-Flutter classes just as useful as for Flutter? |
@scheglov This is a very welcome improvement, thanks! This issue appears for me not only for Flutter, but for domain classes too. I usually will have named parameters unless the function/method has a single parameter or it's extremely simple (ie. a |
@scheglov I think this should act the same on all classes, not just Widgets |
Hello, great developers. May I write my tiny ideas here ? 😁
and here are already some keyword to controll them.
Let's change them for one-to-one 😆
My code will be like this,
in summary,
I think both Compilers and Humans are happy with simple rule😍 |
and like this
|
Just leaving my two cents here. I understand that the current system is an evolution of Dart lang's origins, and has matured over time. However, the usage of the language has also largely shifted. I don't have numbers on this, but I feel like most people use Dart predominantly for Flutter applications. Additionally, because named required parameters are (subjectively) awesome for clarity and consistency, I myself and others use them way more often relative to positionals or optionals. I think an aspect that might confuse newcomers, and adds cognitive overhead to advanced users alike, is the arguable inconsistency in how the syntax rules for function arguments are set up in Dart in its current state.
I feel it's weird that the default rule for what is required and what isn't flips back and forth like this. Again, I might be in the minority here, and admit this is quite subjective. 😄 However, considering that (anecdotally) named parameters are generally preferred over positionals and these arguments are commonly set to required anyway, I believe it would make sense to have named arguments be required by default (just like positionals), and be manually set to optional using some token/keyword (like the system @rbdog proposal using |
We don't need anecdotes since I analyzed the entire Flutter codebase itself. More than half of parameters are positional. Of the named ones, only about 21% are required. |
@munificent including private functions and methods? |
You analyzed the codebase of a UI framework though, where widgets are highly customizable by design and expose many optional parameters. That's not the typical widget in a flutter app. I think it's a missed opportunity to not have included some app codebases (from the partner program for instance) in the analysis. |
I agree with others that Flutter is not the correct target for analysis on this matter. I would add that this is a self fulfilling prophecy. People are less likely to use named required parameters if named required parameters are less convenient to write So the result is biased on multiple levels IMO |
Yes, if I recall right.
When I did the analysis, all named parameters in Dart were considered optional. To determine which ones were intended to be considered required, I looked for the presence of the I can redo the analysis today now that we have null safety and
So positional parameters are still more than twice as common as named, and optional named are still more than twice as common as required named. In case you're curious, the most common parameter signatures look like:
Here, |
But null-safety hasn't solved the bias issue. These syntaxes are not equal in terms of usability at the moment. Named required parameters could very well be less popular than named optional because of the In a world where we instead had to write: { int namedRequired, optional int? namedOptional } then the result would likely be different. After all, a non-negligible number of developers want to type as few characters as possible. |
But by how much, and how would we determine that? |
I'm not too sure what would be a good way of collecting data on that matter. But if we look at the metrics you reported, a case could be made that the syntaxes are sorted in how easy/difficult they are to type (with positional optional being too impractical to use) Also, the fact that we have in the top 4 of method prototype both (p), (p, p), and (p, p, p) is to me an indicator that people don't care that much about optional vs required. Maybe one path to explore would be to see how those top 4 functions are invoked. Such as:
|
Edit: I'll defer to Remi's answer: Old post: I wonder if it's worth considering making Consider the following syntax: void someFunction({
int a, // implicitly required because it's non-nullable
int b = 1, // not required because it has a default value
int? c, // not required because it's nullable
required int? d, // required because explicitely specified
}) {
// ...
} Not making Moreover, this change would be non-breaking. |
'null' is used for many different messages.
To force users to specify it's null or not, From another angle, these 2 JSON objects are different.
So, To keep consistency over language,
required c, but not d. |
I'd say we copy typescript, the whole |
For example, in the code bellow I need to tell the compiler that these optional parameters are required.
This code won't work in the current NNBD implementation, I need to use the
required
annotation.Would be cool if the language consider every non-nullable param as required!?
So I think this:
Is better than this:
The text was updated successfully, but these errors were encountered: