-
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
Primary constructor on classes #2364
Comments
I'd say "yes". If it works, it feels odd to not allow it. I can't see a reason it shouldn't work. But, that depends a lot on what model/capabilities we end up with for the fields. The current model uses the primary constructor to declare fields, and only secondarily as a template for a default constructor. That is, it's not necessarily a "constructor". We could consider a different model where the "primary constructor" directly defines the unnamed constructor. With that, I'd also say that any other generative constructor must be redirecting (eventually) to the primary constructor. If we do allow the syntax for classes too, I'd also have those classes get the default A |
Yes, please! ;-) We could use the following rule: The primary constructor of a struct declares non-late instance variables that are final by default. The primary constructor of a class declares non-late instance variables that are non-final by default, but may have the keyword Classes can still declare additional instance variables, of any kind, as before. Structs might be able to do this as well—what's the harm in allowing a struct to have The primary constructor syntax gives rise to a constructor declaration and a set of instance variable declarations. Every constructor is checked statically relative to the result of this desugaring step. In other words, there is no reason to require that all constructors are redirected to the generated primary constructor, they just need to satisfy the normal constraints that we have today (e.g., that a non-late final instance variable must be initialized before any code with access to About this:
I think that's a very interesting idea to explore. |
Not sure I follow this. I was proposing to allow other generative constructors, they just must also initialize all of the fields as usual.
On reflection, I was thinking of modifying the proposal to say that if there is a "primary constructor", then there is always a
This was the model I had in mind. |
I totally agree that this syntax should be applicable to classes as well. You can make an argument that it is good enough if it only supports simple cases, e.g. class X (var x, int y, final String z, {super.key}) extends Y {
final int w = x + y;
}
// equivalent to
class X extends Y {
var x;
int y;
final String z;
final int w;
X(this.x, this.y, this.z, {super.key}) : w = x + y;
} and for anything else people can resort to traditional constructor syntax. You can probably make We can maybe even make something like: class X (var x, int y, final String z, {key}) extends Y(x, key: key) {
final int w = x + y;
} work. The weakest point of this is going from shorthand syntax to long syntax once you realise that you need constructor body, but maybe this rarely happens. |
I 1000% want any primary constructor syntax to be generalized to classes. In fact, I personally care more about that than I care about the entire views proposal. :) Most user-types do not have value semantics ( To validate that, I scraped a big corpus of code from itsallwidgets.com, open source Flutter apps, and pub packages (18+MLOC) to determine which classes with at least one generative constructor could not use a proposed primary constructor syntax. The reasons a class might not be able to use a primary constructor sugar that I considered are:
The results are:
So a little more than 3/4 of all existing class declarations could use something close to the proposed primary constructor syntax. Note that I'm assuming here that a primary constructor syntax would support users controlling which parameters are positional, named, and/or optional and would allow private names. (In other words, I did not treat those as failures.) I think the biggest design challenges are:
A while back, I worked on a primary constructor strawman syntax that looked like: class Rect new (
final int x,
final int y,
final int width,
final int height,
); So instead of a parameter list right after the class name, there is a You can put it after the other header clauses. Since the field list is likely longer than the class ArgumentSublist extends Rule<Expression> implements FormatSpan new (
/// The full argument list from the AST.
final List<Expression> _allArguments,
/// The positional arguments, in order.
final List<Expression> _positional,
/// The named arguments, in order.
final List<Expression> _named,
) {
/// The number of leading block arguments, excluding functions.
///
/// If all arguments are blocks, this counts them.
final int _leadingBlocks;
/// The number of trailing blocks arguments.
///
/// If all arguments are blocks, this is zero.
final int _trailingBlocks;
void visit(SourceVisitor visitor) { ... }
} Compare that to what you'd get using the current proposal: class ArgumentSublist(
/// The full argument list from the AST.
final List<Expression> _allArguments,
/// The positional arguments, in order.
final List<Expression> _positional,
/// The named arguments, in order.
final List<Expression> _named,
) extends Rule<Expression> implements FormatSpan {
/// The number of leading block arguments, excluding functions.
///
/// If all arguments are blocks, this counts them.
final int _leadingBlocks;
/// The number of trailing blocks arguments.
///
/// If all arguments are blocks, this is zero.
final int _trailingBlocks;
void visit(SourceVisitor visitor) { ... }
} Note how the You can use different keywords. In my strawman, you could use class Rect final (
int x,
int y,
int width,
int height,
); We could then do the same thing for In other words, this means the only thing writing You can provide a constructor name. Having a keyword before the parameter list instead of the class name also provides a natural place to insert a constructor name if you want the primary constructor to be named: class NestingLevel extends FastHash new.empty(
/// The nesting level surrounding this one, or `null` if this is represents
/// top level code in a block.
final NestingLevel? parent,
/// The number of characters that this nesting level is indented relative to
/// the containing level.
///
/// Normally, this is [Indent.expression], but cascades use [Indent.cascade].
final int indent,
) {
/// The total number of characters of indentation from this level and all of
/// its parents, after determining which nesting levels are actually used.
///
/// This is only valid during line splitting.
int get totalUsedIndent => _totalUsedIndent!;
int? _totalUsedIndent;
} The downside, of course, is that this is a bit more verbose and a little different coming from other languages whose primary constructor is right after the class name. In cases where there isn't much else in the type header, there are few fields, and they aren't documented, I think the classic primary constructor syntax looks better. But once the type scales up (and in particular, once you document your fields, which I think is generally a good idea), it gets kind of hard to read. |
This is assuming no changes to documentation conventions, which I think is not realistic. From a brief look at some kotlin code, the equivalent might look more like: /// @param _allArguments The full argument list from the AST.
/// @param _positiional The positional arguments, in order.
/// @param _named The named arguments, in order.
class ArgumentSublist(
final List<Expression> _allArguments,
final List<Expression> _positional,
final List<Expression> _named,
) extends Rule<Expression> implements FormatSpan {
/// The number of leading block arguments, excluding functions.
///
/// If all arguments are blocks, this counts them.
final int _leadingBlocks;
/// The number of trailing blocks arguments.
///
/// If all arguments are blocks, this is zero.
final int _trailingBlocks;
void visit(SourceVisitor visitor) { ... }
} Which looks fine to me (nit, I don't understand how the extra fields work in this class, since they're not initialized in the constructor?) One way of looking at this is that intuitively, we write
Is there any reason not say that every primary constructor is a const constructor (at least if the superclass has a const constructor)?
We could. It looks pretty weird to me though.
This really feels a bit over-generalized to me. If you want a named constructor, just write the constructor.
This is really the rub. My sense is that the more we generalize this, the more we lose the actual benefits. Your data scraping suggests that a huge majority of classes don't need the generality. So every bit of generality that we add that makes that majority more verbose has a massive incremental cost in aggregate, and only benefits a few niche cases. |
We'd need to give you a way to opt out of being a
That's a very good point. The only counter-point is that ever feature we make default and automatic causes an extra step if you ever need to migrate away from the shorthand. If we make a primary constructor implicitly |
FWIW I think that majority of Dart developers don't concern themselves with such matters because they are not writing reusable code. So I think we should not optimise defaults towards the minority that does. |
@ lrhn
To be slightly provocative, maybe the answer is to say "if you don't want it to be const, don't use a primary constructor". As @mraleph says, I think there is a lot of value for a feature like this that you don't have to use in optimizing strongly for the common case. To be slightly less provocative, we could at least say that getting an implicit const constructor is part of the deal with
I hear this, but I also think that there is an inherent cliff here. If you want a constructor body, you have to migrate away. If you want to delegate, you have to migrate away. If you want to initialize some fields in the initializer list, you have to migrate away. So saying that if you want non-const you have to migrate away doesn't feel that bad to me. |
That's a good point. Hoisting all the field docs alleviates much of my readability concerns.
Yeah, I agree it is 100% intuitive to have the parameters right there. I just think it looks funny when you end up having the extends/implements/with clauses jammed between the primary constructor and the class body. But... I'm convinced that it's the least bad approach.
No, good point.
Yes, I think I'm sold. I've poked around a bunch of Kotlin code and it does look weird to me to have the superclasses and superinterfaces wedged between the primary constructor and class body. But in practice, it seems like most classes with complex inheritance hierarchies don't use primary constructors. For those that do... it looks a little weird (and people seem to format them in a variety of creative ways), but not intolerable. OK, so what I'd suggest then is:
There's the weird wrinkle around private named fields as named parameters in the primary constructor. I think I'd be OK with saying that you just can't do that. |
What @munificent says. Parameter list occurs after class name — and after type parameters if any. I'm actually, uncharacteristically, fine with allowing the field names in the parameter list to be private, and automatically make them public in the implicitly added constructor. It's reasonable to want private fields, and unreasonable to have private parameter names. Something needs to be tweaked. (I'd even be willing to contemplate making the name of the parameter of the common I'm now OK with making the constructor Biggest issue: Do we need a way to specify a super-constructor other than the unnamed one? If we allow
Most people will just use the unnamed primary constructor for everything, and that'll just work. On second thought, there is one problem with implicitly inferring
then whether the constructor actually is const will depend on very fragile and accidental choices. Adding a field like I think that's generally going to be too fragile. I'd recommend you having to write const class Foo(int x, int y); That's an explicit opt-in to the primary constructor being constant. It makes it easy to give errors if some other part of the class doesn't support being Yes, it's one more word, and it'll likely be used a lot, but as long as we don't have |
Yeah, I think I agree that it's too fragile, and I'd be fine with this choice (to put const before the class). For structs/data classes (if we do them) perhaps it would be reasonable to say that |
Data classes/ The current proposal allows non-potentially-constant initializer expressions. struct Foo({List<int> indices = [0]}); cannot have a constant constructor. Again it becomes fragile to infer If initializer expressions have to be constant for structs, then we make all structs I mentioned earlier that we could have separate syntaxes for constant default values and non-constant initializers, say: struct Foo({int x = 0, List<int> l ??= <int>[0]}); That would allow a struct with only I'd still prefer to go with |
I don't have a strong opinion that all kinds of classes should have primary constructors. Do you @Hixie want to reject primary constructors on data classes, as well? |
Records already have a primary constructor like syntax, anyway. |
Summary of my opinion.
|
I am not familiar with a data classes proposal so I have no opinion to offer regarding this proposal's application to that one. |
Sorry, I've understood the syntax of data classes is not fixed, and I opened new issue #3198. |
There are (at least) three different kinds of language features:
The least one is what we usually call "syntactic sugar", but there's a wonky line beltween "what you cannot do" and "what you cannot reasonably do". It's all Turing equivalent anyway, and we include the lambda calculus, so you can do any computation. You just can't do it inside the language framework we provide (classes, interfaces, well typed functions, etc.) Primary constructors are firmly in the syntactic sugar group. They do not allow you to do anything you couldn't without them, they just provide a shorter, less repetitive, more up-front syntax which is specialized for a particular use case: Small data-driven classes with little abstraction. In comparison, class modifiers is in the second group: Allowing you to prevent things that you couldn't prevent before. Those are "easy" to implement, they just reject some programs, and everything else works the same as before (hopefully with some more chances for optimization, now that the compiler has more information). Null safety is in both of the first two groups. It allows you to express information that couldn't be expressed before (this value can be null, that value cannot), and prevent null-unsafe programs that couldn't be prevented before. Extension methods are technically syntactic sugar for static functions, but within the language framework, they allow you to express things idiomatically that couldn't be expressed that way before. Patterns are also pure syntactic sugar too, but so concentrated sugar that it allows you to cleanly express something in one line today, that could take ten lines before, and with more code comes more risk of making a mistake. You can argue against individual features, and will probably be correct in several cases, but in every case it was introduced based on the knownledge available at the time, from the requests of developers who wanted the feature, comparison to other possible features solving the same problems, and the available resources. (Rewriting the language from scratch, with everything we know today, may give the best resulting language, but what will we do in the ten years it takes to get there?) Every feature should be weighted against the cost of designing and implementing it, including opportunity cost and loss of free syntax space, the complexity it introduces for both implementors and users, and the benefit it eventually brings to users. Low cost, low complexity, high benefit is awesome, but there are only so many low-hanging fruits. On the other hand, saying "all the good features are taken" and doing nothing is the way to stagnation. That means that we are often taking on more costly features now than we have before, because there is less syntax space left to use, more existing features to interact with, and few good low-hanging fruits as alternatives. |
This comparison of JetPack Compose, Swift UI, and Flutter is interesting: I would still prefer to see this syntax be introduced in a manner that builds on macros, and failing at least a syntax that avoids the cliff when you switch from primary constructors to anything else, but I understand the value of the proposal. |
With the above in mind I've been trying to think about how I'd use this feature in code (I'm mostly writing sample code these days so it's the kind of code I think would be particularly likely to benefit from this feature). One thing I've noticed is that it is quite common for constructors to have initializer asserts. I see the current proposal does not support that. It would be unfortunate to have to specify all the fields explicitly just to be able to add asserts to document the class contract. I expect if we don't support initializer asserts in the syntactic sugar for constructors we may have a chilling effect on the user of such asserts, which would be unfortunate given how valuable they have been so far. |
The cliff issue and the support for assertions could be handled by adopting this proposal, which is mentioned in the primary constructor proposal in the discussion section. The basic idea is that a primary constructor has a magic power (that is, the ability to implicitly induce instance variable declarations), and that specific power could just as well be given to a distinguished non-primary constructor. In that case there can't be a primary constructor, and there can't be more than one of those distinguished non-primary constructors. The distinguished non-primary constructor would have an explicit syntactic marker (in the proposal it's Except for the fact that some parameters can (and must) induce a variable declaration, the The syntax would of course need to be discussed; for example, |
- A "this." parameter can implicitly introduce a new field if there isn't already a field with its name. If so, the field's type is the same as the parameter's. - The constructor doesn't have to repeat the class name and can instead be just "const" or "new".
What's wrong with this? I think it is short enough...
Other languages have a lot more surrounding noise around. Sure, Also, how should this be documented reasonably if the list of parameters (ehh fields) grows? |
I don't think |
Right, cf. this remark. |
You are correct; ThemeData is not an appropriate use case for this feature. I used ThemeData as an example to illustrate how some Flutter API documentation appears from a user's perspective. It's evident that this issue needs to be addressed. However, this feature appears to worsen the problem by introducing additional functionality, now declarations inside the constructors. My concern is that this might make addressing the issue more challenging. If the primary purpose and constrain of this feature is to create classes with a limited number of required fields for initialization, then this feature could be beneficial. |
We have discussed various readability issues that may come up in the case where a primary constructor contains a large number of elements. The answer is always "then don't use a primary constructor". With that in mind, it should not be a problem that this mechanism is optimized for cases where it is small and simple. It is purely a piece of syntactic sugar, and nobody needs to use it if they don't like it in a particular case. |
I'd rather: class Point {
Point(int this.x, int this.y);
Point.onXAxis(this.x) : y = 0;
}
|
Improvements on the OP, replace:
To
|
I think we do need something that screams that a class is exposing implicit fields, however, maybe we could just add a flag to the class: // works
primary class Foo {
Foo(field int x);
} // exception: you can't have primary constructor stuff without flagging the class as primary
class Foo {
Foo(field int x);
} // lint: primary flag not needed
primary class Foo {
Foo();
} |
@jodinathan there doesn't need to be two keywords there, imo this is fine
but I prefer class Foo {
Foo(int this.x);
} as it is more familiar |
yeah, I do like
but I am not sure if the class declaration should warn that it has magical constructors or not I had some hard time with TypeScript because of that |
Since Foo(int this.x); is already valid and useful syntax, we can't use that. class Foo {
Foo(final int this.x);
} would declare a ( If we don't want it in an initializing formal, it can probably go in the initializer list too class Foo {
Foo(int x): final int x = x;
} Not sure how I feel about the readability. |
I think we're straying away from the initial goal of primary constructors, which is to find a syntax as succinct as possible for the 95% use-case I hope that we'll one day have nested class definition, for the sake of sealed classes. And primary constructors would play a large role in the usability here IMO there's a big difference between: sealed class Entity {
class City(final String name, {required final int population});
class Person(final String name, {required final int age});
} vs: sealed class Entity {
class City {
City(String name, {required int population})
: final String name,
final int age;
}
class Person {
Person(final String name, {required int age})
: final String name,
final int age;
}
} I'd personally prefer a syntax that's limited but effective. And suggest users to refactor to normal constructors if they need something more specific. I assume a good IDE refactor would go a long way here. |
Agreed, this one seems unnecessary: class Foo {
Foo(int x): final int x = x;
} Hopefully it can also support private members: class Foo {
Foo({ final int this._x });
}
final foo = Foo(x: 3); |
I'd stick to the syntax used by extension types personally. extension type Example(/* primary constructor*/) {
// Optionally define extra constructors if we wish to
Example.name(/* named constructor */ ): this(...);
} Just replace I'd find it more confusing if classes deviated from extension types in that regard. In that sense, I personally really dislike how pattern matching introduced |
[Update: There is considerable interest on the team in adding primary constructors as a general feature. This original discussion issue has been repurposed as the tracking issue for the general feature request.]
Introduction
Primary constructors is a feature that allows for specifying one constructor
and a set of instance variables, with a concise and crisp syntax. Consider this
Point
class defined using the current class syntax for the constructor and fields:With the primary constructor feature, this class can defined with this much shorter syntax:
Discussion
[original issue content below]
In the proposal for structs and extension structs, I propose to add primary constructors to structs. Briefly, the class name (or the type parameter list if any) may/must be followed by a parenthesized list of variable declarations as such:
In the struct proposal, these are always final by default, and are restricted in various ways (i.e. they may not be late, they may not be const). They are allowed to declare initializers, which are used to generate default initialization values.
This issue is to discuss the possibility of splitting this out, and making it a general feature for classes as well.
Initial points in favor of this include:
Initial points against include:
The text was updated successfully, but these errors were encountered: