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

Rename for initializing formals (and similar parameters) #3058

Open
lrhn opened this issue May 10, 2023 · 5 comments
Open

Rename for initializing formals (and similar parameters) #3058

lrhn opened this issue May 10, 2023 · 5 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@lrhn
Copy link
Member

lrhn commented May 10, 2023

This is an alternative proposition to the #2509 issue.

The base issue is that this._foo is a good way to initialize a private variable, but it also makes the private name of the field be the name of the public-facing constructor parameter. That’s annoying/ugly for positional parameters, and prevents using the initializing formal entirely for named parameters, which cannot have private names. The “solution” is to not use an initializing formal, and do:

class C {
  final int _foo, _bar;
  C(int foo, {required int bar}) : _foo = foo, _bar = bar;
}

rather than using initializing formals.

The other solution is to ignore the problem for positional variables and do C(this._foo). It doesn’t look good in the documentation, but it works, since names of positional parameters are not important outside of the declaration.

With “primary constructors”, as currently defined, a “parameter” declaration of final int foo introduces an instance variable final int foo; and treats the parameter declaration as this.foo. To introduce a private-named instance variable, you‘d again need to have a public-facing private name, and it cannot be a named parameter. (For inline classes, as currently defined, primary constructors would likely almost exclusively be introducing private instance variables.) Primary constructors do not have initializer lists, so there is no rewrite, the alternative is to not use a primary constructor at all, if you want to initialize any private-name instance variable.

The underlying issue is that the same declaration, which has just one identifier, actually denotes two different things, which do not, and should not, necessarily have the same name.

So, a proposal for allowing you to explicitly change the name of a parameter whose declared name identifier is also used to reference something else.

Rename operator

Allow an initializing formal to declare a name. The name of the initialized field is just the default, which you get if you don't specify one yourself, just like the type of the initialized field is the default type, but you can write another type if you want to.

  C(this._x as x, {required thix._y as y});

The as name occurs after the parameter, before a default value if the parameter is optional. It applies to all currently allowed parameter shapes.

It can also be applied to super.x when used as a named parameter. (This is a little more questionable, because it allows changing how a parameter is passed to the super-constructor, but only in a very limited way, by renaming. It doesn't allow you to change a positional parameter into a name super-constructor argument or vice-versa. So why allow this particular, limited change. Basically, because we can. We can also choose to not allow it.)

The syntax is not needed for function types, and does not apply to such. It only applies to parameter lists of actual functions or constructors.

The syntax is only allowed for parameters where the same name would otherwise have two jobs, this.x and named-parameter super.x. It should extend to primary constructor field-introducing parameter declarations too.

Parsing

There should be no ambiguity in parsing existing this.x or super.x parameters followed by as identifier.

Examples:

// Constructors this.x or super.x parameters
  C(this.x as y);
  C({super.x as y});
  C({int this.x as y = 0});
  C(int this.x(int v)? as y); // Old-style function syntax.

For primary constructor parameters, which also introduce instance variables, there is not necessarily a this or super to synchronize on:

class C({covariant final num x as y = 0});
class C(SomeType x as y);

A primary constructor parameter must have a type or var/final, it cannot simply be class C(x);. (Right?)

The word as is a valid parameter name, it’s only a built-in identifier, but at least it’s not a valid type. The worst case example, (int as as as), should still be parsable. The parser probably has to be very careful about not making premature decisions about what’s going on.

A bigger worry is that the syntax may conflict with allowing patterns as parameters. We may (IMO: definitely) want to allow a parameter to be declaration pattern, and allow initializing formals as binding patterns inside them, something like:

class Point {
  final int x, y;
  Point.fromPair(var (this.x, this.y));
  // Works like:
  // Point.fromPair((int, int) $tmp) : x = $tmp.$1, y = $tmp.$2;
}

We do not allow cast patterns (p as SomeType) in declaration patterns, meaning that int x as y cannot be a cast pattern in a parameter declaration position, so it should not conflict with int x as rename. It still means parsing has to know the context, and cannot just “parse any pattern, then check if it’s valid here”. And it means that the same syntax, int x as y, means different things in different places.

Pros

More flexible than, say, deriving a “corresponding public name” from a private-named initializing formal, since you can choose names with no relation, say a field named _isEnabled and a parameter named enabled. It can be used to rename public-named field initializers too.

All names occurring in the program have a declaring occurrence. That’s a good thing for both tools and users. The analyzer won’t have to find and maintain connections between different names. It can rename one or the other name independently.

Directly applies to a primary constructor parameters, allowing them to introduce a field with a different name than the constructor parameter:

class Floo({bool isEnabled as enabled});

Cons

Can be verbose and repetitive, when you need to write this._veryLongName veryLongName. No way around that if we don’t want source names to be implicitly introduced. This is probably the minimal solution with that constraint.

Reuses an existing word, as which already have at least 2-3 meanings (import scope naming, and one or two type assertions, depending on whether you consider cast in expression and cast in pattern as the same thing.) Now it will have 3-4 meanings. It’s consistent with import scope naming in that it gives a name to something.

@lrhn lrhn added the feature Proposed language feature that solves one or more problems label May 10, 2023
@eernstg
Copy link
Member

eernstg commented May 12, 2023

Very nice! It's definitely an attractive property that this kind of renaming has more expressive power than the "corresponding public name" proposal in #2509. I suppose both proposals maintain the constraint that a formal parameter name cannot have _ as its first character (which enables _ on its own to be used as an "ignore" parameter at some point in the future, e.g., in (_, _, x) => .../*does not use the first two parameters*/...).

The verbosity could be reduced by introducing a default mechanism which kicks in when certain conditions are satisfied (in particular, it would kick in for a name which is private, because that's an error unless it's renamed):

class C {
  final int _veryLongName;
  C(this._veryLongName as veryLongName);
  C.couldBe(this._veryLongName); // Same as previous line.
  ... // Other stuff.
}

class CUsingPrimaryConstructor({int _veryLongName}) { // same as `int _veryLongName as veryLongName`.
  ... // Other stuff.
}

The default mechanism could be more elaborate, but if we make it kick in on private names only, and only in cases where that private name is useful (because it's an initializing final, or it's a parameter of a primary constructor), then we will get the regular compile-time error in the remaining cases (e.g., at any formal parameter of a non-constructor function, or a parameter which is not an initializing formal of any constructor which is not a primary constructor).

We could also have a lint saying "Oh, int _veryLongName as veryLongName is quite verbose, maybe you should just use int _veryLongName, which has the same effect".

@jakemac53
Copy link
Contributor

The verbosity could be reduced by introducing a default mechanism which kicks in when certain conditions are satisfied (in particular, it would kick in for a name which is private, because that's an error unless it's renamed):

This does introduce some magic though - I would argue that for a consumer of an api, having the renamed name be explicit is a benefit. Plus it is just one less thing to learn/specify. I think people would be plenty happy to just have the ability to choose a public name, and don't care so much about having it be automatic.

@cedvdb
Copy link

cedvdb commented May 21, 2023

FWIW, I do care about it being automatic as this will be the case for virtually all constructors which want to set private properties. Chances are if you wanted a different public name, that property would have had a different private name to begin with; the cases where the outside API lives in a different semantic context is rare.

The main benefits here, to me, isn't the ability to rename, It is the ability to have more concise constructors and nicer docs and I could live with only the automatic link part. The cases where as will be used are so rare that using initializers instead wouldn't bother me.

for a consumer of an api, having the renamed name be explicit is a benefit.

Why is it a benefit ? This is vague, as a consumer of an api I'm not concerned with the internals.

@mraleph
Copy link
Member

mraleph commented Jun 7, 2023

Is this effectively a duplicate of #2005 ?

@eernstg
Copy link
Member

eernstg commented Jun 7, 2023

I think you could say that this issue is an alternative to the proposal in #2005, based on the perspective that the implicit translation from a private name to a public name (as in #2005) is detrimental to the readability of the code. Also, renaming (as in this proposal) can handle a broader set of cases. A very similar discussion occurred in #2509, which is more broadly about private names and parameters.

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
Projects
None yet
Development

No branches or pull requests

5 participants