language icon indicating copy to clipboard operation
language copied to clipboard

Clarify the treatment of named parameters whose name starts with `_`

Open eernstg opened this issue 1 year ago • 24 comments

A declaration of a name that starts with _ is private to the current library, unless the declaration is

  • a local variable (where privacy makes no difference),
  • a label (where privacy also makes no difference, and which is in its own namespace anyway), or
  • a formal parameter (where the meaning of privacy has never been fully resolved)

Concretely, the tools are currently dealing with named formal parameters whose name starts with _ in slightly different ways:

// ----- Library 'n011lib.dart'.
class A {
  A({_name});
  void m({_name}) {}
}

class B {
  B({required _name});
  void m({required _name}) {}
}

// ----- Library 'n011.dart'.
import 'n011lib.dart';

void main() {
  var a = A(_name: 0);
  a.m(_name: 0);
  var b = B(_name: 0);
  b.m(_name: 0);
}

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 class B (.. "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 declaration this._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 formal this._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.

eernstg avatar Sep 22 '22 10:09 eernstg

I love the idea of making a formal parameter which starts with _ just a name like others, and that they could be used in initializing formals.

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.

srawlins avatar Sep 22 '22 16:09 srawlins

Names starting with _ can also be library names, which have no notion of privacy. (And are not accessible to the program without using dart:mirrors anyway.)

I'm fairly sure that allowing required positional parameters to be named with _-starting identifiers was just a plain specification mistake.

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

It is a compile-time error if the name of a named optional parameter
begins with an `_' character.

when we introduced required named parameters. Prior to that "named optional parameters" covered all named parameters, and the "optional" was just a reminder.

lrhn avatar Sep 22 '22 20:09 lrhn

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 _, and it's just another name (no privacy implied)? The use case is, of course, C({this._x}).

[Edit: That should not be C(this._x), it should be C({this._x}). Corrected.]

eernstg avatar Sep 23 '22 07:09 eernstg

The use case C(this._x) works today. We never prohibited private names for positional parameters, because the name is only visible inside the method anyway. At least from a language perspective, it leaks into documentation too, which is why purists like me don't want to use that pattern for public APIs.

If DartDoc considered such a parameter as being named x instead, then I'd be happy to use C(this._x) in public API (it should allow using [x] in the doc comment too.)

The problem comes when you want to do C({required this._x});. This name does matter to the language semantics.

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. Might as well make it C._({required this._x}); then. Which suggests that we could allow private-named required named parameters on private methods without issues.

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 int _x; field it initializes, which is private? A private _x (with symbol #_x) and a non-private _x (with symbol const Synbol("_x")) are two different and unrelated names (#_x != const Symbol("_x")).

If the parameter name (non-private _x) and the initialized field name (private _x) are not the same name anyway, then we might as well make the parameter name just be x, without the _.

I hope that we will at some point, say if we do primary constructors, make this._x give the parameter the name x. That can be breaking, if someone already has Foo(this._x, this.x), so it should be carried by a larger feature which makes the breakage worth it. For a primary constructor, Foo(int _x) would also be able to take advantage of such a renaming to not mess up the public API.

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.

lrhn avatar Sep 23 '22 08:09 lrhn

If the parameter name is not private

That's the case that I asked about, with the extra assumption that C({this._x}) can both initialize a private instance variable, and this named parameter can be passed (using _x: e) in any library. So it's a public name, and we are able to specify that this._x is an initializing formal that initializes said private instance variable. It's just a tiny novelty: connect those two names for this particular purpose. Probably not too hard to implement either.

With respect to symbols, I agree that #_x would be the symbol for the private name of the instance variable, and const Symbol('_x') would be the symbol for the public name of the formal parameter. No problems, and nothing new btw.

might as well make the parameter name just be x, without the _.

We could do that, but I think it's a bad idea.

  • This name translation step is capable of creating name clashes and complexity that we do not need (so do we stop reporting an error for this.x if x is a non-variable? .. does it matter whether or not there is a declaration named _x? .. do we report an error about a declaration named _x if it's not an instance variable, and there is an initializing formal this.x?).
  • The name translation removes the information that the initializing formal will initialize a private instance variable.

You could claim that it's already a violation of encapsulation to use this.x, because it reveals that x is an instance variable. This information may be useful (especially if we add final getters, #1518 ;-), but that part is in any case unchanged with respect to {this._x} and {required this._x}. In any case, you can just use the formal parameter declaration SomeType _x rather than this._x, and use an element in the initializer list to initialize _newNameOfX if you really, really have to rename that private variable.

The important point here is that we're removing information from the reader for no good reason!

If we allow C({this._x}) to initialize _x, the signature of the constructor immediately reveals that the resulting object has a private instance variable, and the x part of _x may be used to communicate the purpose of this variable. Both of those pieces of information may be useful (e.g., we know that we can't use that getter, so we need to look deeper into the entire interface of C if we wanted to use that getter, or perhaps to understand why we shouldn't have a getter for that variable).

If we use C({this.x}) to initialize _x then we're implicitly promising that we are initializing a public instance variable (perhaps final, perhaps not), and we may only discover that there is no x getter later on because it's an error to use it.

I can see the aesthetics, but I cannot see that the latter is more useful than the former.

eernstg avatar Sep 23 '22 11:09 eernstg

I do not propose that you can write this.x to initialize int _x;. I propose that you write this._x to initialize int _x;, and then we give the parameter a different public/external name. Instead of making that public name a non private _x, which is still a different name than the private _x, we just make it x.

Both names are different names than the private _x, one of them is actually pretty to look at.

It means that you can just do C(this._foo) instead of C(TheType foo) : _foo = foo;, and it means the same thing. (That'd get a big thank-you from me, I write that far too often).

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 _x there for backwards compatibility).

For named parameter names, it affects how you invoke the constructor. Doing C({this._foo}) instead of C({TheType? foo}) : _foo = foo; is shorter, and if it means the same thing, it is actually useful. In comparison {this._foo} is currently completely inaccessible, and if we just make it non-private _foo, having to do C(_foo: 42) is never going to be popular.

So about:

immediately reveals that the resulting object has a private instance variable,

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 this._x because this._x is the field you initialize. The external name should be what other people would want to write, people who do not know, and should never know, the private details of the class. That should never be a private _x name.

Even if we allowed foo({int? _x}) as a function signature, that you could call from another library using foo(_x: 42), without any privacy issues because parameter names are not affected by privacy, then it would confuse me when I read it. Why is it not private? If it's not private, what does it mean that it starts with _? Nothing? Why does it start with _ then? That's just not going to help anybody.

lrhn avatar Sep 23 '22 12:09 lrhn

you write this._x to initialize int _x;, and then we give the parameter a different public/external name. ... [let's] make it x.

That's a very interesting way to cut it!

This means that we preserve the convenience of declarations like C({this._x}), and we preserve the local documentation effect: If you look up the constructor, you get a hint that this particular constructor argument is used to construct the new C, but it's not going to be "a visible part" of that new C.

But I can't help commenting on the underlying understanding of encapsulation. Here we go:

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.

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 _x, because the developer who chose to use that name can do whatever they want with it (e.g., C({int _x}): y = _x;). The traditional (Parnas, 1972) perspective is that public APIs should be expressed in a way that allows evolution of the implementation. With OO subtyping we also want public APIs to maintain freedom for subtypes to choose different method implementations (as well as evolving them over time). The use of a non-private name starting with _ doesn't prevent any of these things, it's just another name.

However, I'm arguing that constructor parameters with names like non-private _x can serve as a hint to developers about the role played by actual arguments to that parameter. Consider an example like the following (from here):

    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 Align has this.alignment and this.child (via super.child, like all single child widgets), Padding has this.padding and this.child, SizedBox has this.height, this.child, and StoreCarouselList has this.documents and this.mapController.

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 mapController for our StoreCarouselList using the label mapController:, because that's the name of the property for that object (technically: it's the name of the getter which will return it when we need it).

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 _x to indicate that

  1. you are now providing an actual argument that will play the "x" role in the construction of the new object, and
  2. that "x" role is not a public property of the object.

For instance, you might want to create an instance of Account with a "password", and perhaps there is no password getter on the new object, because there shouldn't be any such getter.

My take on this is that we can support developers in making this distinction automatically and conveniently by allowing parameter declarations like C({this._x}). The instance variable _x has been made private for a reason, and it is relevant to developers who are going to use instances of C that x is not part of the public API of the new object.

Again, we haven't actually lost any implementation flexibility, and we could of course declare a getter named x in C, but the point is that the straightforward declaration yields the useful hints.

I think it would be useful to allow the syntax _x: e at call sites, and it would allow us to maintain a simpler and more consistent semantics. But it's not so terrible if call sites have x: e, because a simple mouse hover will show this._x if that's indeed how the parameter is declared.

In summary, I vehemently disagree that

That's just breaking abstraction.

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 _ parameter names, and it happens to be very convenient and appropriate in the special case where the argument is actually used to initialize a private instance variable.

eernstg avatar Sep 26 '22 09:09 eernstg

So what I'm suggesting that developers could do is to use parameter names like non-private _x to indicate that

  1. you are now providing an actual argument that will play the "x" role in the construction of the new object, and
  2. that "x" role is not a public property of the object.

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 _banana field from a named parameter compared to apple. The only reason that users write code like this—which they do, all the time—is to manually shave off that _. It's pointless busy work. The author, the reader, and the language knows exactly what the obvious name for this parameter is: it's banana. It's exceedingly unlikely to collide with another parameter name.

The author of Fruits does not want to see _banana at every constructor callsite. They aren't trying to express that. They just want to initialize the "banana" field with a "banana" parameter. The fact that the field happens to be private is immaterial to the class's API, and the fact that the parameter name and field name aren't actually 100% identical is entirely because of Dart's unfortunate choice to bake privacy into the field name.

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.

munificent avatar Sep 26 '22 21:09 munificent

I do not propose that you can write this.x to initialize int _x;. I propose that you write this._x to initialize int _x;, and then we give the parameter a different public/external name. Instead of making that public name a non private _x, which is still a different name than the private _x, we just make it x.

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 x and don't see x listed as a parameter.

leafpetersen avatar Sep 27 '22 04:09 leafpetersen

I wonder if we could make this explicit in the syntax for the parameters? A strawman might be to use _this.x to mean "initializing formal, with name x, which initializes the field _x. It's still pretty opaque if you haven't seen it before, but at least you know something is going on, and it has a searchable name.

leafpetersen avatar Sep 27 '22 04:09 leafpetersen

I wonder if we could make this explicit in the syntax for the parameters?

I considered this._x as someName (except that I suppose @lrhn might want to use as with parameters for #1311).

eernstg avatar Sep 27 '22 08:09 eernstg

@eernstg then @munificent wrote:

So what I'm suggesting that developers could do is to use parameter names like non-private _x to indicate that

  • you are now providing an actual argument that will play the "x" role in the construction of the new object, and
  • that "x" role is not a public property of the object.

We could do that, yes. But developers are not asking to be able to express that, and I see little motivation to encourage it.

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 _ names (and it's not a violation of encapsulation to have them, so why not?).

I know I know, the answer to "why not?" would be "everybody hates _". ;-)

But we could use this._x as x: The renaming is explicit and developer-controllable, and anyone who looks up the declaration of the constructor will know what is going on. All good!

eernstg avatar Sep 27 '22 09:09 eernstg

I would indeed want as for #1311. I also like it for this, but the two do not combine well (the #1311 as changes the internal variable, this one changes the public parameter, so we can't just make it do both.)

I'm not sure I worry about readers being confused. The rule of "this._x has parameter name x" feels like something you'll learn quickly as a code writer, because you'll be using it all the time. I'm more worried about people not realizing that it applies to local DartDoc too.

It would be better if we could combine this., _x and x in some way, and as does have a nice ring to it. C(x as this._x) doesn't work as well. Could be readable as C(this._x = x), but that conflicts with default values. C(x is this._x). Not confusing at all :)

lrhn avatar Sep 27 '22 09:09 lrhn

How about using C(x on this._x) respectively C(SomeType x on this._x)?

eernstg avatar Sep 27 '22 10:09 eernstg

But we could use this._x as x: The renaming is explicit and developer-controllable, and anyone who looks up the declaration of the constructor will know what is going on. All good!

That looks pretty nice when the underlying variable is named something short like x. But here's an example from a Flutter app:

  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 _ wouldn't appear in the parameter name. I don't love it. But it's a fairly small piece of magic to learn that "If you use a private name on a parameter, the _ doesn't appear at the callsite."

munificent avatar Sep 28 '22 21:09 munificent

[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 foo(int _x), or initial formals like A(this._x) or B({required int this._x}), or a super parameter like C({super._x}).

If n is a private name that consists of an underscore _ followed by an identifier id whose first character is not _ then the corresponding public name is id. A name like __x does not have a corresponding public name.

  • It is a compile-time error for any formal parameter to have a private name with no corresponding public name (e.g., __x cannot be the name of a parameter).
  • It is a compile-time error for any formal parameter which is not an initializing formal parameter or a super parameter to have a private name (e.g., _x cannot be the name of any parameter, except that this._x and super._x are allowed).
  • An initializing formal or super parameter whose name n is private with a corresponding public name id introduces the given private name into the formal parameter initializer scope, but not in the formal parameter scope. In the example above, we can use _x in the initializer list, but in the constructor body _x denotes the instance variable, as usual.
  • Such an initializing formal or super parameter, when named, can be denoted in invocations using the corresponding public name (so we can invoke the above constructor of B using B(x: e) or C(x: e)). It cannot be denoted by the private name, even in the same library as the one where the constructor is declared (so we can't use B(_x: e)).
  • A compile-time error occurs if a constructor declares a parameter named id and an initializing formal or super parameter with a private name whose corresponding public name is id.

The only muddled point yet, as far as I can see, is that C(super._x) where C is declared in a different library than the class that declares _x uses _x to refer to a private name spelled _x in a different library. I suggest we simply allow this, even though it is a slightly "magic" step.

[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 B(_x: e) is an error.]

eernstg avatar Oct 05 '22 11:10 eernstg

@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. and super. parameters, so you can declare a private field without an underscore-named parameter name.)

lrhn avatar Oct 05 '22 13:10 lrhn

The only muddled point yet, as far as I can see, is that C(super._x) where C is declared in a different library than the class that declares _x uses _x to refer to a private name spelled _x in a different library. I suggest we simply allow this, even though it is a slightly "magic" step.

This seems odd to me. In the unnamed case, the _x doesn't matter (and will show up in dart doc as x, right?) and in the named case, the constructor in question doesn't have a parameter named _x, only one named x. So it seems quite odd to me that currently I would write:

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?

leafpetersen avatar Oct 08 '22 01:10 leafpetersen

Agree, the A constructor doesn't have a parameter named _x, only one named x, so that's the name B class should use. The _x name is only used internally in the A class, for the field and the initializer-list local variable.

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 :frowning_face:)

lrhn avatar Oct 08 '22 08:10 lrhn

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, super._x is an error, but {super.x} will be considered to match {this._x} in a superconstructor.


We specify the treatment of a name starting with an underscore character as a formal parameter name.

E.g., a function parameter like foo(int _x), or an initial formal like A(this._x) or B({required int this._x}), or a super parameter like C({super._x}). Note that many of these are errors.

First we define corresponding public names: If n is a name that consists of an underscore _ followed by an identifier id whose first character is not _ then the corresponding public name is id. A name like __x does not have a corresponding public name.

The following rules apply:

  • It is a compile-time error for any formal parameter to have a private name with no corresponding public name. E.g., __x cannot be the name of a parameter, positional or named, optional or not.
  • It is a compile-time error for any formal parameter which is not an initializing formal parameter to have a private name. E.g., _x cannot be the name of any parameter, except that this._x is allowed and will initialize an instance variable with the private name _x.
  • An initializing formal parameter whose name n is private with a corresponding public name id introduces the given private name into the formal parameter initializer scope, but not in the formal parameter scope. So we can use _x in the initializer list, but in the constructor body _x denotes the instance variable, as usual.
  • A named initializing formal parameter whose name starts with an underscore is considered to have the corresponding public name in invocations. So we can invoke a constructor of B using B(x: e) in the case where the parameter is declared as this._x. Such parameters, when named, cannot be denoted by the private name in invocations, even in the same library as the one where the constructor is declared.
  • A compile-time error occurs if a constructor declares a parameter named id and an initializing formal parameter with a private name whose corresponding public name is id. So we avoid name clashes involving a regular name and a corresponding public name.

eernstg avatar May 08 '23 16:05 eernstg

@dart-lang/language-team, do you support this updated proposal?: https://github.com/dart-lang/language/issues/2509#issuecomment-1538727192.

Note that it may be helpful for https://github.com/dart-lang/linter/issues/4259 to know where we're going.

eernstg avatar May 08 '23 17:05 eernstg

I love it. I'll nit it anyway, but ship it!

Maybe mention explicitly that identifiers like _2, where the _ is not followed by an identifier, also doesn't have a corresponding public name.

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, just to reduce the breakage surface of the change. (It's silly, but currently allowed.)

If not, we should still allow _, preferably as a non-binding parameter name. (Also doesn't have a public name, but for positional parameters, I want to allow at least that anyway. If it's not non-binding, then maybe allow __, ___ etc. too.)

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. We probably need to formally specify two separate concepts: the declared name identifier of the parameter (the one in the source code), and the parameter name (which is also an identifier, and usually the same one). When the declared name identifier of an initializing formal parameter is a private identifier, then its parameter name is the corresponding public identifier. The "considered to have ... public name in invocations" is not precise enough. It has that name everywhere, including in the runtime function type of a torn-off constructor. It is the parameter name. The private name in the declaration is just something that was used to declare that parameter name.

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 (int thisOne), the identifier after this. for an initializing parameter (this.thisOne), the identifier after super. for a super-parameter (super.thisOne), and the identifier before the parameter list of a function-typed parameter (int thisOne(int x)).

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

  • It is a compile-time error for the declared name identifier of a named formal parameter to be a private identifier (start with an underscore), unless:
    • It's an initializing formal parameter declaration, and
    • the private identifier has a corresponding public identifier.
  • The name of a formal parameter with declared name identifier n is:
    • The corresponding public identifier of n if n is a private identifier, and the parameter is an initializing formal parameter.
    • The identifier n otherwise.
  • An initializing formal parameter (or super parameter) with declared name identifier n introduces the given the name n into the formal parameter initializer scope, but not in the formal parameter scope. So we can use _x in the initializer list, but in the constructor body _x denotes the instance variable, as usual.
  • An initializing formal parameter with declared name identifier n initializes the instance variable named n of the surrounding class.

This lists all the exceptions to the current behavior.

  • We introduce a distinction between the declared name identifier (syntax, in the declaration) and the name of the resulting parameter (semantics).
    • But it only differs for private-named initializing formals. Both named and positional. Huzzah!
  • We don't allow named parameters to have private names, ever. (We don't break private positional parameters.)
    • But initializing formals can have private declared name identifiers.
  • Because we "publicize" the declared names of initializing formal parameters to give them a public parameter name.
  • We use the declared name identifier only for two things:
    • Binding introduced into initializer list scope (for backwards compatibility, and it's local, so it's fine.)
    • Determining which field to initialize.

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.

lrhn avatar May 08 '23 20:05 lrhn

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 this._foo to initialize a field _foo with named parameter foo.

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:

-- Initializer type (16909 total) --
   5156 ( 30.493%): SimpleIdentifierImpl            =========
   2593 ( 15.335%): MethodInvocationImpl            =====
   2459 ( 14.543%): BinaryExpressionImpl            =====
   1214 (  7.180%): PrefixedIdentifierImpl          ===
   1158 (  6.848%): NullLiteralImpl                 ==
    885 (  5.234%): IndexExpressionImpl             ==
    633 (  3.744%): ConditionalExpressionImpl       ==
    563 (  3.330%): BooleanLiteralImpl              =
    490 (  2.898%): AsExpressionImpl                =
    378 (  2.235%): SetOrMapLiteralImpl             =
    274 (  1.620%): SimpleStringLiteralImpl         =
    267 (  1.579%): IntegerLiteralImpl              =
    203 (  1.201%): ListLiteralImpl                 =
    152 (  0.899%): InstanceCreationExpressionImpl  =
    116 (  0.686%): PropertyAccessImpl              =
     66 (  0.390%): StringInterpolationImpl         =
     65 (  0.384%): ParenthesizedExpressionImpl     =
     65 (  0.384%): PostfixExpressionImpl           =
     64 (  0.378%): PrefixExpressionImpl            =
     57 (  0.337%): CascadeExpressionImpl           =
     28 (  0.166%): DoubleLiteralImpl               =
     19 (  0.112%): AssignmentExpressionImpl        =
      3 (  0.018%): ThrowExpressionImpl             =
      1 (  0.006%): IsExpressionImpl                =

Almost all of those identifier expressions are references to constructor parameters (5120/5156). Looking at those identifiers:

-- Parameter initializer (5120 total) --
   4432 ( 86.563%): Make private: _foo = foo  ==============================
    507 (  9.902%): Different: foo = bar      ====
    181 (  3.535%): Same: foo = foo           ==

So ~3% are using the exact same name. Those could just be this. parameters already. Of the remaining, simply removing a _ is roughly 10 times more common than any other rename.

A general purpose rename feature covers 10% more use cases, but it makes the _ special case more verbose, since you have to write the whole name twice:

C({this._someLongFieldName as someLongFieldName});

In practice, these parameter names are often fairly long:

-- Parameter length (5120 total) --
     17 (  0.332%): 1   =
     34 (  0.664%): 2   =
     80 (  1.563%): 3   =
    286 (  5.586%): 4   ====
    382 (  7.461%): 5   =====
    521 ( 10.176%): 6   ======
    423 (  8.262%): 7   =====
    330 (  6.445%): 8   ====
    486 (  9.492%): 9   ======
    321 (  6.270%): 10  ====
    283 (  5.527%): 11  ====
    223 (  4.355%): 12  ===
    268 (  5.234%): 13  ===
    253 (  4.941%): 14  ===
    322 (  6.289%): 15  ====
    139 (  2.715%): 16  ==
    109 (  2.129%): 17  ==
    134 (  2.617%): 18  ==
    168 (  3.281%): 19  ==
     97 (  1.895%): 20  ==
     44 (  0.859%): 21  =
     45 (  0.879%): 22  =
     42 (  0.820%): 23  =
     17 (  0.332%): 24  =
     16 (  0.313%): 25  =
     11 (  0.215%): 26  =
     11 (  0.215%): 27  =
      5 (  0.098%): 28  =
     10 (  0.195%): 29  =
      8 (  0.156%): 30  =
      3 (  0.059%): 31  =
      1 (  0.020%): 32  =
      2 (  0.039%): 33  =
      1 (  0.020%): 34  =
      2 (  0.039%): 35  =
      5 (  0.098%): 37  =
      1 (  0.020%): 43  =
      1 (  0.020%): 44  =
      1 (  0.020%): 46  =
      8 (  0.156%): 47  =
      1 (  0.020%): 48  =
      8 (  0.156%): 49  =
      1 (  0.020%): 50  =

Average 10.816, median 10

Given that, I think it will be most helpful for users if we special case _. We could also do a general rename feature, but I suspect it doesn't carry its weight. You can always just write an explicit initializer for those remaining cases.

munificent avatar Jun 28 '23 23:06 munificent

@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?

eernstg avatar Apr 24 '24 08:04 eernstg