-
Notifications
You must be signed in to change notification settings - Fork 212
Clarify the treatment of named parameters whose name starts with _
#2509
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
Comments
_
_
I love the idea of making a formal parameter which starts with There was a lot of momentum behind "make private initializing formals a thing," several months ago but it lost steam. I personally think this is a great feature, on par with super parameters. |
Names starting with I'm fairly sure that allowing required positional parameters to be named with We never allowed it before introducing required named parameters. There was no good reason to allow it for required named parameters, but not for optional named parameters (if anything, the other direction makes more sense). We just forgot to remove "optional" from
when we introduced required named parameters. Prior to that "named optional parameters" covered all named parameters, and the "optional" was just a reminder. |
Very good, @lrhn, so it could be argued that we "used to prohibit" formal parameter names starting with But what are the disadvantages of simply saying that formal parameter names can start with [Edit: That should not be |
The use case If DartDoc considered such a parameter as being named The problem comes when you want to do If the parameter's name is a private name, then you can't call the constructor from outside the library. You can't express the name of the named parameter, and you need to do so in the argument list. If the parameter was not required, you could call the constructor. So we could also safely allow private optional named parameters on static methods. (I guess you could do a tear-off and using type inference to get a type you can't otherwise satisfy, but the same use-case breaks by adding a non-private optional parameter too.) If the parameter name is not private, how does it then correspond to the If the parameter name (non-private I hope that we will at some point, say if we do primary constructors, make With that, named parameters don't need to have private names at all. Now, if you actually want to have secret unspeakable parameters that other people can't pass ... just make a separate privately named function for that, and let the public method forward the public parameters to it. |
That's the case that I asked about, with the extra assumption that With respect to symbols, I agree that
We could do that, but I think it's a bad idea.
You could claim that it's already a violation of encapsulation to use The important point here is that we're removing information from the reader for no good reason! If we allow If we use I can see the aesthetics, but I cannot see that the latter is more useful than the former. |
I do not propose that you can write Both names are different names than the private It means that you can just do For positional parameter names, it only matters for documentation. You can't refer to external the name from outside the class anyway. (Well, we have to decide the name of the implicit variable in the initializer list, and we can use the private For named parameter names, it affects how you invoke the constructor. Doing So about:
That's just breaking abstraction. You should never reveal that a class has a private member through its public API, that's the entire point of making the member private. If anyone outside of the current library needs to know that information, your're doing something wrong. The source name should match what the code does when you look at the source. You write Even if we allowed |
That's a very interesting way to cut it! This means that we preserve the convenience of declarations like But I can't help commenting on the underlying understanding of encapsulation. Here we go:
Of course, any particular choice in software engineering can be wrong. However, faced with "you are doing something wrong", I'd say that your thinking seems to be based on a rather inflexible reliance on traditional wisdom which is just not applicable in Dart. First, we're not breaking abstractions by using a name like a non-private However, I'm arguing that constructor parameters with names like non-private return Align(
alignment: Alignment.topLeft,
child: Padding(
padding: const EdgeInsets.only(top: 10),
child: SizedBox(
height: 90,
child: StoreCarouselList(
documents: documents,
mapController: mapController,
),
),
),
); In this example, the class It just goes on and on, and the point is that the named constructor arguments (in Flutter, in particular) correspond quite faithfully to properties of the newly created object. So the developer who writes this code will constantly reinforce the awareness of properties of the many different kinds of objects. It's necessary to use the correct name for each named parameter in order to pass the actual arguments to the correct parameters, but it is also useful to maintain the tight coupling between object construction and object properties: It makes sense that we're choosing the right This is just a convention, of course, but I have no doubt that it's possible to get into serious trouble by declaring something that violates this convention: class Point {
final int x, y;
Point({int x, int y}): y = x, x = y; // Hehe.
} So what I'm suggesting that developers could do is to use parameter names like non-private
For instance, you might want to create an instance of My take on this is that we can support developers in making this distinction automatically and conveniently by allowing parameter declarations like Again, we haven't actually lost any implementation flexibility, and we could of course declare a getter named I think it would be useful to allow the syntax In summary, I vehemently disagree that
because we still have all the degrees of freedom in the implementation that we need in order to maintain encapsulation. I'm just arguing that it can be helpful for developers to build the correct understanding of the newly created object if we allow the author of the constructor to use these non-private |
We could do that, yes. But developers are not asking to be able to express that, and I see little motivation to encourage it. This sounds like a solution in search of a problem. Meanwhile, we do have a real annoyance: class Fruits {
String? apple;
String? _banana;
Fruits({this.apple, String banana}) : _banana = banana;
}
main() {
Fruits(apple: 'Fuji', banana: 'Cavendish');
} There's no compelling reason why it must be so much harder to initialize the private The author of If we're going to allow private names in named initializing formals, we should have their semantics map to what users already do, which is use a public parameter name to initialize that field. |
I worry a lot that this will be super confusing. It's fine if you go the doc page, but it's going to be super confusing if you go to definition from a call site which is passing |
I wonder if we could make this explicit in the syntax for the parameters? A strawman might be to use |
I considered |
@eernstg then @munificent wrote:
The motivation I see is that (1) we avoid introducing confusing and surprising rules about implicit renaming of parameters, and (2) there is a meaningful interpretation of those non-private I know I know, the answer to "why not?" would be "everybody hates But we could use |
I would indeed want I'm not sure I worry about readers being confused. The rule of " It would be better if we could combine |
How about using |
That looks pretty nice when the underlying variable is named something short like TrackingWorker({
required TrackingTask trackingTask,
required TrackNumberRepository trackNumberRepo,
required ShipmentRepository shipmentRepo,
required TrackingRepository trackingRepo,
required TrackingNotifyTask notifyTask,
required PlatformInfo platformInfo,
required AppSettings pref,
required TrackingLimiter trackingLimiter,
}) : _trackingTask = trackingTask,
_trackNumberRepo = trackNumberRepo,
_shipmentRepo = shipmentRepo,
_trackingRepo = trackingRepo,
_notifyTask = notifyTask,
_platformInfo = platformInfo,
_pref = pref,
_trackingLimiter = trackingLimiter; It's certainly better if they can write: TrackingWorker({
required this._trackingTask as trackingTask,
required this._trackNumberRepo as trackNumberRepo,
required this._shipmentRepo as shipmentRepo,
required this._trackingRepo as trackingRepo,
required this._notifyTask as notifyTask,
required this._platformInfo as platformInfo,
required this._pref as pref,
required this._trackingLimiter as trackingLimiter,
}); But even that seems verbose (and error-prone) compared to: TrackingWorker({
required this._trackingTask,
required this._trackNumberRepo,
required this._shipmentRepo,
required this._trackingRepo,
required this._notifyTask,
required this._platformInfo,
required this._pref,
required this._trackingLimiter,
}); It is a bit of magic that the |
[Edit: Updated, new version here.] It sounds like we could converge on the following: We'd like to specify the treatment of private names as formal parameter names, e.g., a function parameter like If
The only muddled point yet, as far as I can see, is that [Edit: Fixed a typo where a parameter error would be applicable to named parameters, but it's actually relevant for all parameters. Mentioned explicitly that |
@eernstg That looks like what I'd want. We should probably also say, informally, that DartDoc can use either the private or the public name to refer to a parameter (and we recommend the public name). (Come primary constructors, if that happens, I also want it to apply to all the primary constructor parameters, not just |
This seems odd to me. In the unnamed case, the class A {
int _x;
A({required int x}) : _x = x;
}
class B extends A {
B({required super.x});
} but if I switched to using this feature I would write: class A {
int _x;
A({required super._x});
}
class B extends A {
B({required super._x});
} I think that means it would also be a breaking change to switch to using this feature, which makes it non-ideal. Any reason not just to use the actual public signature of the constructor wrt super parameters? |
Agree, the class A {
int _x;
A({required this._x}) : assert(_x != null);
}
class B extends A {
B({required super.x});
} (Missed that paragraph, the things I agreed to was just the dotted items |
Here is an adjusted version of the proposal I wrote here, adopting the principle that the formal parameter name will never start with an underscore. So there is no need to discuss whether the name is private, it isn't. Also, We specify the treatment of a name starting with an underscore character as a formal parameter name. E.g., a function parameter like First we define corresponding public names: If The following rules apply:
|
@dart-lang/language-team, do you support this updated proposal?: #2509 (comment). Note that it may be helpful for dart-lang/sdk#59104 to know where we're going. |
I love it. I'll nit it anyway, but ship it! Maybe mention explicitly that identifiers like The first two items feel very overlapping. (They are, there are four subsets defined by the conditions of the two items, with three of the subsets being compile-time errors.) Consider rewording to one item. Not allowing any parameter to have a private name is more more breaking than necessary, because we don't actually need to do anything for positional parameters. We should probably allow private names for non-initializing positional parameters, If not, we should still allow Aaaaand, super pedantic: It's a little ambiguous about what the "name of a formal parameter" is. It says that an initializing formal parameter can have a name which is private, if that name has a corresponding public identifier. It then says that the name of the parameter is that public name. But that's inconsistent, because it started out saying the parameter's name was private. So maybe, we need to split those two into separate concepts. Definition: The declared name identifier of a parameter declaration is the "name" identifier of a "normal" parameter ( A parameter declaration has a declared name identifier (usually, a function type positional parameter can have none, but that doesn't matter), and it introduces a parameter into the parameter list of the function/constructor which has a parameter name (which is also an identifer).
This lists all the exceptions to the current behavior.
Then, everywhere else, we just use the "formal parameter name" the same way we do today, including the normal "no two formal parameters with the same name" rule. The distinction between the declared name identifier of a parameter declaration and the name of the parameter never leaves the constructor declaration. Everywhere else, it's all about "parameter names". And inside, it only matters for initializing formals. |
I was curious about whether a general rename facility for initializing formals (#3058) would be more useful than supporting only the simple case of allowing I scraped a corpus of 2,000 pub packages (~12MLOC). I looked at every constructor initializer. The most common kind of initializer expression is a simple identifier:
Almost all of those identifier expressions are references to constructor parameters (5120/5156). Looking at those identifiers:
So ~3% are using the exact same name. Those could just be A general purpose rename feature covers 10% more use cases, but it makes the C({this._someLongFieldName as someLongFieldName}); In practice, these parameter names are often fairly long:
Given that, I think it will be most helpful for users if we special case |
@dart-lang/language-team, I'd like to revive this idea, in particular, the concrete proposal from @lrhn which is stated here seems to work well. Perhaps we should bundle it with the wildcard proposal? |
I'm proposing that we generalize this feature slightly and make the "magic" behavior of certain private names explicit, in #3962. With that proposal we would support named formal initializing parameters of the form // Proposed here.
class A {
final int _x;
A({this._x = 0}); // Magically supports invocations like `A(x: 42)`.
}
// Using the proposal in dart-lang/linter#3962.
class A {
final int _x;
A({this._?x = 0}); // Explicitly indicates that we're dealing with two names, `_x` and `x`.
} The declaration is the only location that changes, both proposals have the same syntax at invocations. |
Not sure I'm sold on the syntax. Also because What we do should work consistently everywhere. Any parameter declaration which initializes a field and whose declared name is a private name that has a corresponding public name, should have a signature where the parameter uses the public name. Then that applies both to initializing formals and to primary constructor field parameters. Only question is syntax. |
I prefer this approach. Sometimes it is vital to pass value as implementation detail and not to make it publicly available |
That was also the kind of reasoning that made me suggest that the name could be private in the first place. However, that idea doesn't get any support from the rest of the language team, so we just aren't going to have that. A workaround could be to write a forwarding function. Wanted: void foo(String anotherParameter, {int _private = 42}) {...}
// Consider the affordances at call sites.
void main() {
// Caller from same library.
foo('Hello from insider!', _private: -15); // This is possible!
// Caller from another library.
foo('Hello from outsider!'); // We can't pass `_private`, so we must use the default.
} Workaround: void _foo(String anotherParameter, {int private = -25 /*whatever*/}) {...}
void foo(String anotherParameter) => _foo(anotherParameter, private: 42);
// Consider the affordances at call sites.
void main() {
// Caller from same library.
_foo('Hello from insider!', private: -15);
// Caller from another library.
foo('Hello from outsider!'); // We can't call `_foo`, so we must use the default.
} |
This workaround is unnecessary complicated IMHO. I don't understand why this doesn't get enough attention. Usecases for providing private implementation details are endless and lack of this in language which supports encapsulation is a big miss and inconsistency for me. Please, as a Dart team, reconsider this one more time. It is very strange, that some encapsulation is supported, while other - also important - is not. Even Python which does not have encapsulation, often uses |
Such inconsistency may be threated as a bug in the design |
That's probably why it doesn't matter. If I can access the function, I can access its type indirectly, maybe get it through inference. It's a type that I cannot write myself, because I can't write that name. All in all, the protection you would get is not perfect, and it causes problems, so it makes things easier for everybody if you just can't have named parameters that cannot be named. (And if you want a workaround, you can make it a public parameter with a private type: void foo(int publicArg, {_MineOnly? privateArg}) { ... }
class _MineOnly {
final int realValue;
_MineOnly(this.realValue);
} then nobody can pass a value, but they can express which parameter it is that they're not passing a value to. |
Make sense. Thank you for explanation! |
A declaration of a name that starts with
_
is private to the current library, unless the declaration isConcretely, the tools are currently dealing with named formal parameters whose name starts with
_
in slightly different ways:With the analyzer (commit 92638bf5d3a5668920d0a40a4b24a6a97f982b3f), no issues are reported; that is,
_name
is treated like any other name when it occurs as the name of a named parameter, required or not (that is, it isn't treated as a private name).But the common front end reports an error for both cases with the declaration of
A
(effectively saying "a named parameter name cannot be private", so we cannot even ask how it's treated at call sites), and allows classB
(.. "except if it's required").We could say that a formal parameter name starting with
_
is simply a name like all others, with no special exceptions. In particular, call sites in different libraries can invoke the enclosing function/method/constructor and pass an actual argument to a named parameter with such a name.We could also say that a formal parameter name starting with
_
is a private name. So if it's the name of a named parameter then it can't be passed at call sites in other libraries.The former makes it easy to use named initializing formals to initialize private instance variables (and the name
_name
in the parameter declarationthis._name
serves as documentation for the fact that we're initializing a private instance variable), but it is unfortunate that this usage will make it harder to rename said instance variable. (It would be nicer if the API would only reveal that we're initializing a private instance variable, and it wouldn't mention the name.)The latter provides support for a very special kind of privacy: Every caller outside the library must use the default value for a non-required named formal parameter with that kind of name, and the whole function/method/constructor can't be called at all outside the library if the parameter is required. This feature is again potentially useful, but rather accidental in nature.
At the conceptual level it may seem inconsistent to claim that
_name
is just another name (not private to any library) when it's used as the name of a named initializing formalthis._name
, because it does refer to the instance variable whose name is definitely private.So we need to clarify these issues and then ensure that implementations agree. We have the freedom to make different choices, because current code is (in practice) unable to depend on any particular behavior in the case where the tools behave differently.
The text was updated successfully, but these errors were encountered: