Skip to content
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

Closed
pedromassango opened this issue Mar 10, 2020 · 119 comments
Closed

[proposal] non-nullable named parameters required by default #878

pedromassango opened this issue Mar 10, 2020 · 119 comments
Labels
feature Proposed language feature that solves one or more problems nnbd NNBD related issues

Comments

@pedromassango
Copy link

pedromassango commented Mar 10, 2020

For example, in the code bellow I need to tell the compiler that these optional parameters are required.

  bool run({
    String taskId,
    String command,
    List<String>? args,
  }) {
    //...
  }

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:

  bool run({
    String taskId,
    String command,
    List<String>? args,
  }) {
    //...
  }

Is better than this:

  bool run({
    required String taskId,
    required String command,
    List<String>? args,
  }) {
    //...
  }
@pedromassango pedromassango added the feature Proposed language feature that solves one or more problems label Mar 10, 2020
@lrhn
Copy link
Member

lrhn commented Mar 10, 2020

One issue with that is that it might get confusing when you use a type variable.
If you write:

T foo<T>({T value}) => value;

would you then expect that parameter to be required when T is a nullable type or not?
It would probably be required as long as the type variable is potentially non-nullable, but that still makes it harder to read the code.

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.

@eernstg
Copy link
Member

eernstg commented Mar 10, 2020

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 required modifier seems to be a forced choice, it is not always entirely simple to determine whether it is required, and there are even cases where you can have it, and you can omit it. So we decided to make it explicit in all cases, to improve the documentation of code properties for the readers of the code.

@rrousselGit
Copy link

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 required keyword would be added by the IDE.

@pedromassango
Copy link
Author

pedromassango commented Mar 10, 2020

I agree with @rrousselGit.

This is another use-case where we need to over-write the required tag even the property is not nullable. Note that the avatarUrl property is nullable so it can be optional.

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:

  • Named and nullable parameter can be optional
  • Named and non-nullable parameter must be specified

@munificent
Copy link
Member

There is a lot of discussion of this question on this issue. Some comments that might help clarify: 1, 2, 3.

@ds84182
Copy link

ds84182 commented Mar 11, 2020

To be clear, the existence of required has nearly nothing to do about non-nullable parameters.

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 = null on the end of each named parameter. Tagging these as required removes the default initialization, and turns them into a different class of parameter (which we don't have today), which are required named parameters.

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.

@rrousselGit
Copy link

rrousselGit commented Mar 11, 2020

The problem is not about having the keyword or not. Its presence is necessary for many reasons.

A metaphor: required is like types. In a typed language, we should type everything.
But that does not mean we should write the type all the time.

Types have a type inference for the obvious situations. Therequired keyword needs some form of inference too.
In its current state, it only adds a huge amount of boilerplate and decreases readability.

{required int a}

is not more readable than

{int a}

Adding required in this situation only adds duplicate information, and is basically equivalent to:

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

@pedromassango
Copy link
Author

A nullable parameter can be required. A non-nullable parameter can be not required.

Sorry I didnt get it. Why I would have a required parameter that can be null

@lrhn
Copy link
Member

lrhn commented Mar 11, 2020

@pedromassango

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.
There can be situations where you want a parameter to be required because it is very important to the readability of the call, but you also want to allow the user to to specify a "none" value. Rather than invent a special value for "no effect", you can choose to use null for that.

You can also run into the situation with generic classes.
If you have:

class Box<T> {
  T value;
  fill({required this.value}) { // Because you *like* to write `box.fill(value: x)`
     this.value = value;
  }
}

then a Box<int?>.fill will have a required nullable parameter.

(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).

@munificent
Copy link
Member

A metaphor: required is like types. In a typed language, we should type everything.
But that does not mean we should write the type all the time.

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 foo(bar()), they expect that the return type of bar() is implicitly checked against the parameter type of foo() even though the type flowing between those two functions is never written down. All other type inference basically builds on that intuition. (Even so, there are corners where type inference does things that unpleasantly surprise users—it's not a panacea.)

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 required outweighs that cognitive cost.

@rrousselGit
Copy link

Would it be worth making a poll about it?
I can make one on Twitter, or the Dart team can make a large scale one too.

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 required keyword.

@leafpetersen
Copy link
Member

After all, popular languages like Typescript don't have that required keyword.

Can you elaborate? My understanding (which could be wrong, I'm not a Typescript expert) is that

  • Typescript has no named parameters
  • For positional parameters, Typescript took a similar approach to Dart in that all parameters are required unless they are marked optional
  • Unlike Dart, Typescript marks optional parameters on a per parameter basis rather than on a section.

So I don't really see the connection. It's true they don't have required named parameters (and hence don't have a required keyword). They also don't have optional named parameters. Am I missing or mis-stating a Typescript feature?

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).

@rrousselGit
Copy link

rrousselGit commented Mar 24, 2020

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.

@mateusfccp
Copy link
Contributor

mateusfccp commented Mar 24, 2020

Common Lisp, although a great language, also has null (or NIL, in it's case).

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:

  • The default value is null. However, obviously, this can't be the case for non-nullable types.
  • The default value is related to the type itself, like in C#. For example, the default int value is 0, the default bool value is false, etc.
    • This doesn't seem to be a reasonable approach, as a type default value is totally arbitrary.
    • What is a default value for a custom object (class)? C# has not non-nullable references until C# 8, when non-nullable references does not prevent compilation, only throw warnings.
  • The default value is provided by the developer. This way, the behavior of a function when a given optional named argument is not passed to the function is explicit and clear to the developer.

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.

@munificent
Copy link
Member

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).

You're correct. The relevant numbers are in this comment. I found only about 20% of named parameters were required.

@leafpetersen
Copy link
Member

The behavior is pretty close to Dart in that "named" parameters are always named and with a syntax fairly similar if we removed types.

@rrousselGit

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 {a : String | null}, or a non-nullable optional named parameter {a? : String}. The former must be passed an argument (which can be null), and the latter can have the argument elided, even though the type is non-nullable.

Am I misunderstanding something here?

@rrousselGit
Copy link

I get some syntax errors from your code above if I try it out, but here's a fixed example

The code was valid.
The difference is that JS has null vs undefined. As such, instead of having T? be T | null, they made it T | undefined.

The null keyword is pretty much unused in TS's non-nullable world

Technically, if we wanted to have the required T behavior, we could do T | null like you did. But I have yet to see people do that outside of compatibility with legacy

@leafpetersen
Copy link
Member

The code was valid.

If I paste your example into the TypeScript playground, I get the following error on the ? in your example2:

JSDoc types can only be used inside documentation comments.(8020)

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?

@rrousselGit
Copy link

Ah, my bad, I fixed it. I placed the ? in the wrong location

My main point was my takeaway, which is that TypeScript does not do the inference you are proposing

Yes, because it doesn't have the required keyword and doesn't need it, while still achieving named required parameters and named optional parameters.

The only difference in TypeScript that I can see is that they make you mark the optional parameters instead of the required parameters.

Yes, but Dart makes us mark optional parameters too:

{ int a } does not mean that a is optional.
We either have to write { int? a } or {int a = 42}, so we do mark optional parameters.

The difference is that Dart makes us also mark required parameters. whereas Typescript does not.

@rrousselGit
Copy link

rrousselGit commented Mar 24, 2020

Another way of formulating my concern would be: the required keyword is very verbose.

  • Refactoring from { required int a } to { int? a } is particularly painful whereas int a vs int? a is a single character
  • I don't get why we would want to reject the { int a } syntax. It currently does not compile anyway, so the slot is free.
  • the required keyword takes a lot of visual place. required is larger than int a itself, when the type and variable name are more useful. This decreases readability and hinders the 80 character limit

Also, random thought:
What about ! instead of required?

void example({ int! a }) {}

That's a lot easier to refactor/write/read. And it matches with foo!.bar.

@leafpetersen
Copy link
Member

Yes, because it doesn't have the required keyword and doesn't need it, while still achieving named required parameters and named optional parameters.

I'm still really missing something. I do understand that:

  • TypeScript has chosen to mark optional parameters instead of required parameters
  • TypeScript has chosen to use syntax ({a? : type}) instead of a keyword ({optional a : type}).

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.

@rrousselGit
Copy link

I've just noticed that there's a difference in typescript between { a?: number } and { a: number | undefined } (the former is optional, the second is required), so I stand corrected

You're right then, especially about the:

  • TypeScript has chosen to use syntax ({a? : type}) instead of a keyword ({optional a : type}).

I think I've better formulated my concerns about that difference on my second message #878 (comment)

@leafpetersen
Copy link
Member

Another way of formulating my concern would be: the required keyword is very verbose.

  • Refactoring from { required int a } to { int? a } is particularly painful whereas int a vs int? a is a single character

Ack.

  • I don't get why we would want to reject the { int a } syntax. It currently does not compile anyway, so the slot is free.

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 required around. You need to be able to write the type of f above, so that means that you 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:

  • {int x} means optional in function types, but required in function definitions
  • {required int x} means required, and can be used in types or definitions

Two different ways to write the same thing, and the same thing meaning different things in two different places. Yuck.

  • the required keyword takes a lot of visual place. required is larger than int a itself, when the type and variable name are more useful. This decreases readability and hinders the 80 character limit

Also, random thought:
What about ! instead of required?

This was definitely kicked around at one point, I forget whether as {int! x} or as {int x!}. I don't have strong feelings. I tend to rely more on the judgement of @munificent and @lrhn when it comes to syntax. It's just not an area where I feel that I have good intuition (or at least, intuition that lines up with what our users want).

@lukepighetti
Copy link

lukepighetti commented Sep 29, 2020

I admit I only skimmed this thread but with NNBD do we need required at all? If a type is nullable, and it is not provided, it will be null which meets all the needs of the type and thus the consuming code. @required seemed to act as a shim for items that couldn't be null.

Although in some cases like Flutter's RaisedButton.onPressed handler it was more like "yo, you should probably have something here", but in my opinion that is unnecessary. Seems like this could be replaced with a @recommended decorator that gave some IDE sugar for filling out commonly used constructor arguments, although that is well outside the scope of this issue. Suffice to say that required doesn't seem to have much more value than making IDE recommendations.

I'm trying to think of a situation where you'd need required in any scenario with NNBD and I'm coming up short.

@escamoteur
Copy link

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.
I didn't need to write @required in front of a positional parameter because it would have been redundant.

Curious, is the required keyword necessary for the compiler to correctly treat nonnullables or is it just a new language feature to 'enhance' readability?

@lrhn
Copy link
Member

lrhn commented Sep 29, 2020

Required named parameters are not necessary for null safety, it is a new feature added along with null safety.
It was added because it improves code that people are already writing.

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 @required.
Those parameters would then either need a default value (which is silly when the parameter is required), or they would need to be nullable, which is annoying because the function body would have to check for null anyway, and users might think passing null is reasonable.
By adding required named parameters, that code can now happily expect that there is a non-null argument value.

As for whether the required syntax (or some other marker) is necessary, that has been covered above. It is.
(I agree that you probably shouldn't have a required nullable parameter, but it's fine to have an optional non-nullable parameter.)

@rrousselGit
Copy link

rrousselGit commented Sep 29, 2020

I'll admit that I'm still quite fearful about the impact of this required keyword on the community.

Named required parameters are needed only for improving the invocation readability, but this keyword decreases the readability/writability of the declaration.
I am certain that some will give-up on named required parameters because required is too verbose, and use required positional parameters.

At the very least, with @required + assert(x != null), the annotation was optional. So people used named parameters but without annotation/assertion.

@lrhn
Copy link
Member

lrhn commented Sep 29, 2020

@rrousselGit
You can keep using optional named parameters the same way you did before (with nullable types, like they used to be). Nothing changed about that.

If you previously wrote @required, you can now write required and actually have the requirement checked by the compiler, not just the analyzer.

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.
People used to write either write @required (one character more, and didn't actually ensure anything) or provide a default value, and would then still have to check for a deliberately passed null in both cases.

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 😁

@Kavantix
Copy link

Kavantix commented Jan 7, 2022

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.
So I’d like to see this issue reopened even if it’s not on the road map, that will also make it easier for people to find this issue instead of creating a new one.

On top of having the keyword everywhere, the code generated by code actions is usually wrong.
For instance, executing the constructor for final fields action on a widget leaves a bunch of errors that I need to fix.
When I then try to quickfix those errors it often makes the parameter nullable instead of required.
(Not sure if an issue exists for this yet on the sdk repo btw)

@leafpetersen
Copy link
Member

For instance, executing the constructor for final fields action on a widget leaves a bunch of errors that I need to fix.
When I then try to quickfix those errors it often makes the parameter nullable instead of required.

I'd definitely suggest filing issues for these. cc @bwilkerson @scheglov

So I’d like to see this issue reopened even if it’s not on the road map, that will also make it easier for people to find this issue instead of creating a new one.

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?

@mateusfccp
Copy link
Contributor

mateusfccp commented Jan 7, 2022

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.
So I’d like to see this issue reopened even if it’s not on the road map, that will also make it easier for people to find this issue instead of creating a new one.

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.

@scheglov
Copy link
Contributor

scheglov commented Jan 8, 2022

There is a distinction between Flutter (widgets) and non-Flutter classes in the analysis server.
Brian implemented this for Flutter widgets https://dart-review.googlesource.com/c/sdk/+/226482

@mateusfccp do you want this only for Flutter? Mostly for Flutter? For non-Flutter classes just as useful as for Flutter?

@mateusfccp
Copy link
Contributor

@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 swap(a, b) function, or an add(a, b) function).

@lukepighetti
Copy link

@scheglov I think this should act the same on all classes, not just Widgets

@rbdog
Copy link

rbdog commented Feb 12, 2022

Hello, great developers. May I write my tiny ideas here ? 😁
For now, Dart-lang default

  • isRequired = true
  • isNullable = false
  • isNamed = false

and here are already some keyword to controll them.

syntax means
{ } isRequired = false, isNamed = true
required isRequired = true
? isNullable = true

Let's change them for one-to-one 😆

syntax means
{ } isRequired = false
? isNullable = true
: isNamed = true

My code will be like this,

(int a, int? b, int c:, int? d:, { int e:, int? f: })
param isRequired isNullable isNamed
a true false false
b true true false
c true false true
d true true true
e false false true
f false true true

in summary,

  • default required
  • new syntax " : " for named params.
  • or else cahnge isNamed=true in default, and new syntax for isNamed=flase.
  • like Swift " _ " syntax.

I think both Compilers and Humans are happy with simple rule😍
Thank you

@rbdog
Copy link

rbdog commented Feb 12, 2022

and like this

ERROR don't define named-params before positional-params

@jeroen-meijer
Copy link

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.

  1. An argument is required.
  2. Unless it's named, indicated by { ... }
  3. Unless unless it's also preceded by required.

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 { ... }, or by a new optional keyword, for example). 💙 ✌🏻

@munificent
Copy link
Member

However, considering that (anecdotally) named parameters are generally preferred over positionals and these arguments are commonly set to required anyway

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.

@ykmnkmi
Copy link

ykmnkmi commented Feb 17, 2022

@munificent including private functions and methods?

@lukaspili
Copy link

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.

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.

@rrousselGit
Copy link

rrousselGit commented Feb 17, 2022

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.

I agree with others that Flutter is not the correct target for analysis on this matter.
In my personal apps, I make all my parameters named required. Whereas in my packages project, I use optional more, for the simple fact that I don't want to introduce breaking change whenever I add a new property.
Flutter is likely similar.

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

@munificent
Copy link
Member

@munificent including private functions and methods?

Yes, if I recall right.

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.

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 @required annotation. The Flutter framework was disciplined about applying that metadata annotation but I didn't know of open source Flutter app codebases that used it as consistently. Without that, I didn't know an easy way of telling which named parameters should be considered optional or required.

I can redo the analysis today now that we have null safety and required to get more data. Here's the result of analyzing all of the GitHub repos on itsallwidgets.com and another corpus of open source Flutter apps (4,248,662 lines in 31,439 files):

-- Parameters (137394 total) --
  90432 ( 65.819%): required positional  ==========================
  35143 ( 25.578%): optional named       ==========
  11153 (  8.118%): required named       ====
    666 (  0.485%): optional positional  =

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:

-- Signatures (126328 total) --
  56658 ( 44.850%): (P)
  44391 ( 35.139%): ()
   8690 (  6.879%): (P,P)
   2026 (  1.604%): (P,P,P)
   1936 (  1.533%): (N)
   1276 (  1.010%): (N,N)
   1271 (  1.006%): (P,P,N)
    900 (  0.712%): (P,N)
    785 (  0.621%): (N,N,N)
    550 (  0.435%): (N,N,N,N)
    540 (  0.427%): (N,N,N,N,N,N)
    535 (  0.424%): (P,P,P,P)
    496 (  0.393%): (R,R)
    450 (  0.356%): (R)
    364 (  0.288%): (P,N,N)
    348 (  0.275%): (N,N,N,N,N)
    339 (  0.268%): (R,R,R)
    241 (  0.191%): (N,N,N,R)
    234 (  0.185%): (O)
    204 (  0.161%): (P,O)
    189 (  0.150%): (P,P,P,P,P)
    175 (  0.139%): (R,R,R,R)
    170 (  0.135%): (P,N,N,N)
    164 (  0.130%): (N,N,R)
    146 (  0.116%): (R,R,R,R,R)
    144 (  0.114%): (N,N,N,N,N,N,N)
    140 (  0.111%): (N,N,N,N,R)
    133 (  0.105%): (R,R,R,R,R,R,R)
    128 (  0.101%): (N,N,N,N,N,N,N,R)
    123 (  0.097%): (R,R,R,R,R,R)
    120 (  0.095%): (N,N,N,N,N,R)
    112 (  0.089%): (N,N,N,N,N,N,R)

Here, P means required positional, O is optional positional, R is required named, and N is optional named.

Here is the script I used.

@rrousselGit
Copy link

rrousselGit commented Feb 17, 2022

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.

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 required keyword.

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.
In fact, that's probably part of why positional required are so popular, because they don't need any token both for defining the prototype and invoking the function.

@munificent
Copy link
Member

Named required parameters could very well be less popular than named optional because of the required keyword.

In a world where we instead had to write:

{ int namedRequired, optional int? namedOptional }

then the result would likely be different.

But by how much, and how would we determine that?

@rrousselGit
Copy link

rrousselGit commented Feb 18, 2022

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.
After-all, that top 4 means that 90% of the functions do not have optional parameters.


Maybe one path to explore would be to see how those top 4 functions are invoked. Such as:

  • how many of those parameters would benefit from being refactored to named and/or optional parameters?
    This could involve:

    • how many of those required positionals are primitives like booleans/strings/numbers?
    • how many of those required positionals are invoked with explicit null, such as fn(null) or fn(null, 42)?
    • how many functions have multiple parameters of the same type?
  • how often is a function invoked with a variable of the same name as the positional arg name in the prototype?
    This could be an indicator that if Dart had syntax sugar for typing fn(name: name), maybe people would use named more.

    Destructuring is a factor here too, where:

    void fn(Model model) {
      anotherFn(a: model.a);
    }

    would likely count toward this syntax sugar, as we may be able to do:

    void fn(Model(a) model) {
    anotherFn(a: a);
    }

@VKBobyr
Copy link

VKBobyr commented Feb 20, 2022

Edit:
I just read the cons to this approach earlier in the thread, and it's clear that things are not always as simple as what I presented.

I'll defer to Remi's answer:
#878 (comment)


Old post:

I wonder if it's worth considering making required implicit for non-nullable variables.

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 a required is already an error, so why not just make it implicitly required?
Explicit required can still exist for cases when you want required nullable parameters.

Moreover, this change would be non-breaking.

@rbdog
Copy link

rbdog commented Feb 20, 2022

'null' is used for many different messages.

  • missing (please handling error if its null)
  • omit (dont wanna use the option)
  • no-error (there was no problem)
  • others...

To force users to specify it's null or not,
people may want the required nullabe param
(Although I think they are not good code...)

From another angle, these 2 JSON objects are different.

{ a: 0 }
{ a: 0, b: null }

So, To keep consistency over language,
I think nullable and non-nullable type should be treated equally.

int a,
int b = 1,
int? c,
int? d = 1,

required c, but not d.
d is nullable but default value is not null.

@ollyde
Copy link

ollyde commented Sep 30, 2022

I'd say we copy typescript, the whole required keyword is seems a bit verbose, especially when the IDE tells you if it's required or not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems nnbd NNBD related issues
Projects
None yet
Development

No branches or pull requests