-
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
Should NNBD type inference infer required
when necessary?
#938
Comments
There are arguments in either direction, but I feel like the arguments for are based on the syntax, not the semantics. We already inherit I'd rather infer So I think we should just make it a compile-time error — you have to add either (Hmm, if I override |
I argued in #630 (comment) that it is a net negative for developers if we infer a nullable parameter type @lrhn does have a point that it is a mere syntactic distinction that we are willing to infer Inferring So I'd support any of the models that infer class A {
void f(void Function({required int x})) {}
void g(void Function({required int? x})) {}
}
void test() {
var a = new A();
a.f(({x}) {}); // OK, {required int x}.
a.f(({required x}) {}); // OK, {required int x}.
a.f(({int x}) {}); // OK, {required int x}.
a.f(({int x = 42}) {}); // OK, unchanged.
a.f(({int? x}) {}); // OK, unchanged.
a.g(({x}) {}); // OK, {int? x}.
} |
I don't find this logic very convincing. The syntax for optional positional parameters is not parallel to the syntax for required named parameters, and so arguing by parallelism between them doesn't work. Optional positional parameters are explicitly called out in a separate delineated section, whereas named parameters go into a single section, with required as a per variable modifier. So yes, I think if the syntax for required positional parameters was More to the point, there is a huge difference between ignoring something that the user explicitly wrote (treating
Why not? There is incredibly strong evidence in the program as to what the right one is. The context type says The argument that we can't guess the right thing to do here applies exactly as well to type inference. How do we know that given |
What makes that adding the required implicitly may be a mistake? These cases are not immediately obvious to me. Are these cases something that people write often, or just rare edge cases? |
If the author writes nothing in the subclass, they inherit the superclass type. I'm fine with also inheriting other annotations in that case. So, the superclass has However, if the subclass writes I don't think there is incredibly strong evidence what the right thing is. Maybe I'm just being obtuse, that's a definite possibility. The one thing that would favor Would inferring foo(int bar({required int x}) { ... }
foo(({x]) => x); //?
foo(({int x}) => x); //? ? Again, I'd be OK with doing it for As a counter-opinion, it's not a good user experience when providing more correct information gives you worse inference. I think that principle actually wins for me, so ... let's inherit/infer |
I'm sympathetic to the general desire. I think if we don't do this for lambdas, users will be annoyed (though lambdas with named parameters are relatively rare). However, I would feel weird about adding an inference feature for lambdas that does not have matching behavior in override inference. If we supported this for overrides, how would it compose? Given: class A {
foo({required i}) {}
bar({required i}) {}
}
mixin M {
foo({int i}) {}
bar({i}) {}
}
class B extends A with M {
foo({i}) {}
bar({i}) {}
} What are the inferred types of I'm less worried about the idea that inferred required named parameters doesn't match now optional positional parameters works. The syntax for those is quite different and encourages a different mental model. With named parameters, the parameter is there and required-ness is an attribute of it. With positional parameters, there are two independent sets of parameters, the required and optional ones. Optional-ness is part of the parameter's identity. |
@lrhn I don't understand where you got to inheritance - nothing in this issue is about inheritance based inference. We could consider doing inheritance based inference for this, but that isn't what I filed this issue about.
That is what this issue is about. |
@munificent We already do pretty different things here. I guess not doubling down is an argument, but they're different enough that it hadn't even occurred to me to consider proposing this for override based inference. In general, I don't think override inference carries it's weight - very few users know it exists, and I don't see it very widely used. I don't object to adding this there, but I think very few users expect to be able to write a method with no types and get typed code, whereas they expect to be able to write lambdas with no types and get typed code.
@munificent In your example,
|
I can see that I lost track somewhere throughout the thread. It is indeed about function literals. I think we should be doing the same thing here as wrt. inheritance, so whatever we decide in either case should apply in both. I think (now) that we should at least infer So for context type ({x}) {} ↦ ({required int x}){}
({required x}) {} ↦ ({required int x}){}
({int x}) {} ↦ ({required int x}){} The cases where it's not necessary, the ({int? x}) {}
({int x = 42}) {} If they write a different type which is still non-nullable, I'm not sure whether to keep the ({num x}) {} ↦ ({required num x}){} If the type of the context type parameter doesn't require the parameter to be required, we do not insert it. With context type ({x}) ()
({int? x}) ()
({num? x}) () It's not necessary, so if they actually want the parameter to be non-required, they need a way to write that. So, only insert
The only question left to answer is why is this different from default values? If we have void foo({int x = 42}) => ...;
...
foo(({x}) {}); then the function literal is not valid after inferring |
Because default values aren't part of the types, and we do inference from the types.
I don't know what this program is supposed to be? It's passing a function where an integer is expected.
In general, we can't invent a default value, and we'd have to since it's part of the type. For the "making the type nullable" version, that was at least partially the proposal in the issue that this issue was split off from. It is something we could do. I think the argument for doing so is less compelling to me though. Some examples where we could do this:
All of this said, @munificent makes the fair point that he believes that closures with named parameters are very rare. His argument is that this is rare enough to make this really not worth spending the effort, time, and cognitive budget on. That seems like a fair point, and he and @lrhn are in a better position to judge this than I am. So maybe not worth doing? |
I'd support inferring So we could say "This may be worthwhile, but it is not top priority, and we can do it later". |
I got data! I cobbled together a little script and ran it on the Flutter repo, Dart SDK repo, and the 2,000 most recent packages on pub, as of today. That's 14,399,669 lines of Dart code in 46,082 files. Here is a histogram of every parameter signature in every lambda:
This is a histogram which counts number of occurrences of each type. For example, this means that there are 117,277 lambdas whose parameter signature is Bucketing by what kinds of parameters appear:
So of the quarter million lambdas, only 301 (about 0.1%, or one in a thousand) have any named parameters. |
That is cool! |
@munificent Thanks! That's tremendously useful! I'm pretty inclined not to spend effort on this feature then, sorry for the noise. @lrhn does that seem reasonable to you as well? |
I don't understand why closure not using named parameters influence the
decision, unless I am misunderstanding something?
The inference of required would matter for constructors and methods mostly,
not closures.
I'm probably missing something
…On Fri, May 15, 2020, 01:49 Leaf Petersen ***@***.***> wrote:
@munificent <https://github.com/munificent> Thanks! That's tremendously
useful! I'm pretty inclined not to spend effort on this feature then, sorry
for the noise. @lrhn <https://github.com/lrhn> does that seem reasonable
to you as well?
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#938 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AEZ3I3M3225HX6DSZUDHKSDRRSGQ3ANCNFSM4MPUXPYQ>
.
|
This issue was primarily (from my standpoint) about closures. That is by far the most common place for parameter type inference to be done, and by far the place where brevity seems most valued by users (see for example Kotlin where even eliding the parameter is a feature). It's also a place where inference only matters to the local code. That is, given
There is no inference for constructors. For methods, the only relevant inference would be override based inference: that is, given a super class method We could still do this (or really, do both, since if we do one it feels like we should do the other), and it is true that named method parameters are fairly common. On the other hand, my sense is that override inference is not very widely used, and frankly it's not clear to me that it's a feature we should encourage users to lean on. In general, my experience with type inference from other languages is that as a general rule you don't want your API surface to be inferred. It's good documentation and good error checking to have API be explicitly written out. That's my take anyway. |
I'm not talking about override inference, but new constructors and new
functions.
I don't think the inference of "required" uses the same rules.
We are not inferring the parameter type, but the fact that it is required.
The parameter type is unchanged
Inferring:
```dart
void function({required int x}) {}
```
When the user writes
```dart
void function({int x}) {}
```
Is harmless.
There is only a single possibility where this code can work, as this code
otherwise does not compile.
There is no unknown behavior or scenarios where that's not what we want.
In 100% of the situations, writing the above code can only mean that the
parameter is required.
So having to write it is redundant and decrease readability.
|
I've argued above that there is more than one possibility. Adding And for an abstract function, you don't need to add I'm (now) OK with copying whatever requirement is provided externally by another function signature (required parameter in context type for literals, required or default value in super-class declaration for overrides), but not inventing something on the spot to make invalid code valid. |
And for an abstract function, you don't need to add required, so the
difference in behavior between abstract and concrete functions is probably
going to be confusing.
I didn't think of that one. That's fair then.
Le ven. 15 mai 2020 à 09:29, Lasse R.H. Nielsen <notifications@github.com>
a écrit :
… @rrousselGit <https://github.com/rrousselGit>
There is only a single possibility where this code can work, as this code
otherwise does not compile.
I've argued above that there is more than one possibility. Adding required,
? or a default value will all work.
Inventing a default value is tricky (but inheriting it should be safe).
Adding ? is changing the signature, and it might make the body invalid.
It might not, and then it's probably (but not certainly) the right thing.
Adding required is also changing the signature, but only invalidates
calls, which probably don't exist yet when the code isn't compiling yet.
And for an abstract function, you don't need to add required, so the
difference in behavior between abstract and concrete functions is probably
going to be confusing.
I'm (now) OK with copying whatever requirement is provided externally by
another function signature (required parameter in context type for
literals, required or default value in super-class declaration for
overrides), but not inventing something on the spot to make invalid code
valid.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#938 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AEZ3I3MK3F65RVCBJNODXQ3RRT4PVANCNFSM4MPUXPYQ>
.
|
You are asking about a different kind of inference here. In your example, you are asking about inferring Your request was considered in detail here: #156 (and in particular this comment), and #878. We decided it wasn't worth the potential confusion, in part because of the issue with abstract methods Lasse mentions here. |
This is not planned. |
Consider this API:
It seems natural to want to use this API as follows:
As currently specified, however, this is an immediate error, since
x
is not required in the lambda. It seems useful to infer required here, since otherwise the code is guaranteed to be an error. Should we?If so, in what circumstances do we do so? Consider:
a.f({x}) {}
a.f({required x}) {}
a.f({int x}) {}
a.f({int x = 42}) {}
a.f({int? x}) {}
Consider also the case where it would not be an error:
Should we still infer
required
ina.g({x} {})
based on uniformity?See also #630 .
cc @lrhn @eernstg @munificent
The text was updated successfully, but these errors were encountered: