-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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
proposal: spec: permit eliding the type of struct fields in nested composite literals #21496
Comments
Not an argument for or against, but personally I always forget that this case is not allowed because it feels so similar to the cases where I can elide the type. |
Also not an argument for or against, just noting that this was discussed when we discussed type elision for array/slice/map literals. At that time we didn't have any experience with this form of type elision and we decided to err on the side of caution: There was a sentiment that type elision in struct literals may be more confusing (read: lead to less readable code) than elsewhere due to the possibly different types of struct fields. In contrast, for arrays, slices, and maps, all the (keys and) elements are of the same type. |
I'll have to dig up specific examples, but in my experience the lack of elision often leads to stutter in code: req := service.AddRequest{
A: 1,
B: 3,
// The Options field contains a value of type Options. Quelle surprise!
Options: service.Options{
SurrealNumericRegime: true,
},
} This is particularly heinous in massive, nested protocol buffer definitions, although I'm sympathetic to the argument that the fault lies in the massive, nested definitions rather than the language literal syntax. :) |
Actually the rule is that when the type of all the elided things is the same, the type may be elided. In a struct (in general), fields may have different types, so it's hard to see what the thing being elided is. We were concerned with structs in particular because interface fields would still need the type, for example if you have an R io.Reader, you have to write One thing I realized over the weekend is that this has implications for API design. In os/exec we have a Just things to think about, not an argument for or against. |
I'm not certain that it quite holds that the type of all the elided things is the same--maps permit elision of both keys and values, which are not necessarily the same type. Or if we say that all the keys of a map have the same value, then it is equally true that all the values of field I also don't entirely buy the point about interface fields; you can't elide the type from non-struct types in this case either. It's true that most implementations of an interface are usually struct types, but that's not necessarily the case. And, of course, permitting or forbidding elision of struct field types in other cases doesn't change this case at all, since you need to include the type either way. |
One thing to note is that the elisions done currently can always be done without impeding readability. This (and the others in #12854) would be elision(s) that could help or hinder readability depending on the context.
This would have to be a choice. As such, Being a choice, different codebases, or different sections in the same codebase could make the choice differently. However, this is already true of keyed vs. unkeyed fields in a struct. While keyed literals are generally preferred, unkeyed literals can make equal sense in some situations so there's no absolute rule. I would prefer the option to make this choice when it would help readability, personally. If a type is elided in such a way as to hinder readability, or in an inconsistent manner, that's a matter for code review and linting. |
This is a subjective opinion, not a statement of objective fact. _= T{
{1, 2, 3, 4}: {"Value"},
} Is the type elision in this example aiding or impeding readability? I would say that the answer depends on the context of the code, and that opinions will still differ. I do not believe that it is obviously correct that Note that the above example is valid under the current elision rules: |
Will this proposal allow me to use anonymous nested structs? a := struct {
A int
B struct {
C int
}
} {
A: 10,
B: {
C: 10,
},
} |
@drscre I think it should as IMO it is probably the case that would benefit the most from this proposal. In fact, I think it'd still be a huge win if the proposal was accepted only for the anonymous struct case. I find myself writing nested structs anonymous structs from time to time and am forced to always break them down into multiple independent structs that are the composed together. I agree that is better for most cases but in some cases it feels unnecessary. For example, recently I wrote something like this but only a a lot more field. type SubConfigA struct {
FirstField string
SecondField string
}
type SubConfigB struct {
FieldA string
FieldB string
}
type Config struct {
F1 int
F2 int
F3 int
SubA SubConfigA
SubB SubConfigB
} With lots of nested fields and lots of nested structs, it would have been nicer to be able to write the whole thing as a single type as it is very much self-contained. type config struct {
F1 int
F2 int
F3 int
SubConfA struct {
FirstField string
SecondField string
}
SubConfB struct {
FieldA string
FieldB string
}
} and then be able to use it to define a defualt config instance without having to re-type the entire definition again or split the struct definition into smaller pieces. defaultCfg := Config{
F1: "f1",
SubConfA: {
FirstField: "first",
},
SubConfB: {
FieldB: "bee",
},
} |
This would be suuuuper useful for configuration structs. When I configure an application, I usually write a config struct like this: type Config struct {
Runtime struct {
Environment string
Debug bool
Admins []string
Hostname string
}
GRPC struct {
Enabled bool
Port int
Services []string
}
} This makes the acceptable values really easy to read.. the anonymous structs just act as namespaces when configuring, so your config file looks something like this: Runtime:
Environment: dev
Debug: true
GRPC:
Enabled: true (etc) But then if you want to fill it out in actual go code, it's kind of awful: c := Config{
Runtime: struct {
Environment string
Debug bool
Admins []string
Hostname string
}{
Environment: "dev",
Debug: true,
},
GRPC: struct {
Enabled bool
Port int
Services []string
}{
Enabled: true,
},
} The only way around it, would be to make Runtime and GRPC named types, but that then makes it a lot harder to read the type declaration (and thus what actual values are available to be set): type Runtime struct {
Environment string
Debug bool
Admins []string
Hostname string
}
type GRPC struct {
Enabled bool
Port int
Services []string
}
type Config struct {
Runtime Runtime
GRPC GRPC
} Not only does this make it harder to see what you can set in the Config struct, it also "exposes" the structs internal to Config to the external world, even though they really only exist to namespace values inside the Config struct. And the annoying thing is that the compiler knows what every type is. Yes, if there's an interface you'd still have to specify, but the compiler would tell you when you forgot to specify that. I don't think "you'd still have to specify a type in one ambiguous case" is a reason not to allow inference the rest of the time. c := Config{
Runtime: {
Environment: "dev",
Debug: true,
},
GRPC: {
Enabled: true,
},
} With that change, the struct literal becomes almost a perfect duplicate of the type declaration. Please, let's allow type elision here. It would make a lot of code easier to read, easier to write, and easier to maintain. |
This is labelled go2 but it seems to be backwards compatible. Is there something that makes.it not possible to do in go1? |
Go 2 shipped last week. |
@natefinch We label all language changes as Go2. |
This comment was marked as off-topic.
This comment was marked as off-topic.
Hi, I am not sure about the status of this issue, I hope it's still under consideration. I just wanted to add an example that I didn't see mentioned above, showing that this behavior in practice even causes the rule allowing elision of other types to become useless: var dataRotate = []struct {
a, b []int
c int
}{
{
[]int{1, 2, 3, 4, 5, 6},
[]int{5, 6, 1, 2, 3, 4},
2,
},
{
[]int{1, 2, 3, 4, 5, 6, 7},
[]int{5, 6, 7, 1, 2, 3, 4},
3,
},
{
[]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
[]int{14, 15, 16, 17, 18, 19, 20, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13},
7,
},
[...]
} This is often how table-driven test data for real-life functions look like. This pattern is used often and is recommended by many for the appropriate situations: https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go (Obviously I don't mean to imply that a project member recommending this makes it "official" or whatever, just that it's a very common pattern and its ergonomics should be taken into consideration). |
When the type of a nested composite literal may derived from its context, that type may be elided in all cases except when the composite literal is the value of a struct field.
I propose that elision be permitted in this case as well.
I submit that the arguments in favor are the same as for eliding the type in other contexts, as well as conceptually simplifying the language--it is simpler to say that types may always be elided in nested literals (when the type can be derived from context) than to say that types may be elided in all but one case.
Proposed spec change:
This proposal is a subset of #12854, which proposes elision of types in all composite literals, not just nested ones.
The text was updated successfully, but these errors were encountered: