-
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: type inferred composite literals #12854
Comments
There is some prior art. We actually implemented this (or something very similar) in the lead-up to Go 1. The spec changes: https://codereview.appspot.com/5450067/ The code changes: https://codereview.appspot.com/5449071/ There may be other changes that I'm missing. But in the end we abandoned the changes (and reverted the spec changes); it tended to make the code less readable on balance. |
I only see one spec change there (the other one you linked is the compiler implementation). At any rate: "tend[ing] to make less readable on balance" depends a lot on the specific code. Presumably we've learned more about real-world Go usage (including Protocol Buffers and a variety of other nesting data-types) in the time since then - perhaps it's worth revisiting? (I've badly wanted literals for return values and channel sends on many occasions - they would be particularly useful when a struct is just a named version of "a pair of X and Y" and the field names suffice to fully describe it.) |
while I support more type eliding rules, I don't like the concept
of untyped composite literals. The new concept is too big for
Go 1, IMHO.
For example, composite literals for map and structs are different,
so I don't expect that you can { A: 1 } assign to a map[string]int.
And what will happen if the type doesn't have the A field? should
that be an error?
And, if we have untyped composite literal, we need to figure out
whether we allow const untyped composite literals.
BTW: I think every untyped type should have a default type,
otherwise it will be too confusing to use.
|
Under this proposal, the following assignments are identical: var m map[string]int The only difference is that in the latter case, the type of the literal is derived from the RHS of the expression. In both cases, the compiler will interpret A as a variable name. I would not allow const untyped composite literals; that's a can of worms. I think untyped composite literals would be too confusing to use (and compile!) if they came with a default type. :) |
On readability: This would be a significant language change. Go code would become somewhat terser, on balance. In some cases this would lead to less readable code; in others more so. I feel that the benefits would outweigh the costs, but that's obviously a subjective judgement. In particular, I think that lightweight tuples (as in the above examples) would be a substantial improvement in a number of places. |
I assume that this proposal is simply expanding the places in which you can elide a type literal. If so, referring to it as untyped composite literals is a bit confusing as untyped has a specific meaning in Go. It might make more sense to consider each place separately. I don't see much of a point, other than consistency, in allowing
since you could just do
But the rest would certainly cut down on typing and allow nicer APIs in places. For example,
is arguably clearer than
and the only alternative at present would be
|
I agree that the examples look nice, at a glance. But anything can look To move this proposal forward, one should apply the change to a corpus of On 7 October 2015 at 09:12, Damien Neil [email protected] wrote:
|
The examples look nice, but I'd suggest we don't introduce a
new kind of untyped literal. (I especially don't like allowing to
convert {A: 1} to map[string]int{ "A": 1 } implicitly.)
Instead, we can extend the spec by adding more places where
type can be elided. This has a higher chance being accepted.
|
To be clear, this proposal does not allow {A: 1} to implicitly become map[string]int{"A":1}. |
#12854 (comment)
says these are allowed:
var m map[string]int
m = map[string]int{A: 1}
m = {A: 1}
Isn't the last one implicitly converts {A:1} to map[string]int{"A":1}?
|
In that last example, A is an identifier (i.e. for a string variable or constant) - not a string literal itself. |
did you mean that the code is actually: const A = "A" Then there are more ambiguity in syntax. What does this mean? Note my concern is that it's possible to assign {A:1} to |
|
But we currently don't accept
var m = map[string]int{A: 1}
|
We do, actually: A := "A"
var m map[string]int
m = map[string]int{A: 1}
fmt.Println(m) |
We're talking in circles.
In #12854 (comment),
there is no mention of where does A come from.
Let me ask again, does this proposal support:
var m map[string]int
m = {A: 1}
var x struct { A int }
x = {A: 1}
where A is not otherwise defined?
The proposal says "Untyped composite literals are assignable to any
composite type."
so I'd assume the answer to my question is true.
|
If A is not otherwise defined, then the first case ( |
@minux The implementation of this proposal is actually rather straight-forward and the explanation reasonably simple and clear: Whenever the type of a composite literal is known, we can elide it. Once the type is known, the meaning of a composite literal key value ('A' in the previous examples) is answered the same as it is before. (Implementation-wise this only affects the type-checker, and there's already provisions for this for the case where we allow type elision already.) So the proposal is indeed simply a relaxation of the existing rules as @jimmyfrasche pointed out. Another way of phrasing this is: In an assignment in all its forms (including parameter passing, "assigning" return values via a return statement, setting values in other composite literals, channel sends, there may be more) where the type of the destination is known, we can leave away the composite literal type if the value to be assigned/passed/returned/sent is a composite literal. (We cannot leave it away in a variable declaration with an initialization expression where the variable is not explicitly typed.) In the past we have deliberately restricted this freedom even within composite literals. This liberal form would overturn that decision. Thus, we may want to start more cautiously. Here's a reduced form of the proposal that enumerates all permitted uses: In addition to the existing elision rules inside composite literals, we can also elide the composite literal type of a composite value x when
In all cases, the variable/parameter/return value/channel value type must be a composite literal type (no interfaces). We could even reduce further and start with only 1) or 2). |
The downside of limiting the cases in which elision is allowed is that the programmer must remember what those cases are. The two extreme endpoints ("never" and "whenever the type is otherwise known") are easier to remember - and simpler to describe - due to their uniformity. |
On Tue, Oct 6, 2015 at 3:17 PM, Andrew Gerrand [email protected]
I agree that it would be good to apply this change to a corpus of real code |
@bcmills I would agree if we started with this as a general concept. In this case "whenever the type is otherwise known" is not sufficiently clear. For instance, in a struct comparison a == b, the types may be known but the exact rules are subtle. This proposal is a rule that describes an exception, namely when it is allowed to elide a composite literal type. It is clearer to be explicit. |
That assumes that "eliding the type" is the exception rather than the rule. s/allowed to elide/necessary to specify/ and the same argument applies in the other direction. (We can explicitly enumerate the cases in which a type tag is necessary in exactly the same way that we can explicitly enumerate the cases in which it is not.) |
The only other case I can think of (for consideration, if not inclusion) is eliding the type within another composite literal like
for
|
FTR, I support more type elision (I proposed to use it to solve named
parameter,
see #12296 (comment)).
But I don't support adding a new kind of untyped literal (where they can't
be
used as const and don't have a default type -- too dissimilar from existing
untyped values.)
|
@minux This is a type elision proposal. The term "untyped" in the title is misleading. I can't tell: is there anything specific you object to this in this proposal? (I'm not sure I support this proposal myself, but I'm trying to understand your objection.) |
This is a turn of events. I initially proposed #12296, but I found that named parameters where not a solution with current Go-idioms. As for inferred structs… I have been in favor of this for a while; however, I have recently hit some pitfalls. I'm (now, surprisingly) leaning against this, because of legibility and common behavior: // assume there are 2 struct types with an exported Name of type string
// they are called Account and Profile…
// Would this result in an Account of a Profile?
what := {Name: "John"} (see http://play.golang.org/p/KM5slOe7nZ) Perhaps, I'm missing something but duck typing does not apply to similar structs in the language… type Male struct {
IsAlive bool
}
type Female struct {
IsAlive bool
} Even though Male and Female both only have |
would produce a compile error under this proposal. (It fails the "when the literal type can be derived" constraint, which would be applied per-statement. For this statement, the elided type cannot be derived unambiguously: it could by any struct type with a "Name" field, including an anonymous struct type. If there is a constant or variable named "Name" in scope, it could be a map type as well.) |
You would, however, be able to do the equivalent for plain assignments, as long as the variable has a concrete type:
|
@bcmills Ok… what would be the overhead on the compiler side of inferring the types and aborting if the type is ambiguous? |
That is not the case for the proposal. slog.Info accepts And, even if we were to restrict struct {
Key string
Value slog.Value
} So, if we elide Now, even if we suppose
and which is similar to argument made in #12854 (comment) |
This is an ongoing difficulty for me. I tend to make a lot of "config files" in regular go code, because why map from yaml or whatever to go, when I can just write it in go and get compile time checks, code completion, etc. However, there are a lot of cases where type inference should let me elide the types and yet I can't, leading to less readable code. For example: type User struct {
Email string
}
type Group struct {
Leader User
}
var Groups map[string]Group = {
"devs" : {
Leader: User{
Email: "[email protected]",
}
}
} The go tool was modified so maps and slices don't need to specify the type when it's a struct, but struct fields still need to have the type specified when the field is a struct. Why? If the field name is obvious in context, you really don't need the type name. This seems perfectly legible to me: var Groups map[string]Group = {
"devs" : {
Leader: {
Email: "[email protected]",
}
}
} And certainly, the lack of This gets compounded with larger structs, especially if you're making an anonymous struct as a field in another struct. type Config struct {
Telemetry struct {
Port int
URL string
Password string
}
DB struct {
ConnectionStr string
Password string
}
} I like making anonymous structs as fields inside another struct because the whole of the struct definition is all in one place. You don't have to jump around to see what each field's sub-fields are. It uses the anonymous structs as namespacing, with short, clear field names in the anonymous structs. There's very clearly no logic tied to the struct fields, because they're not defined as individual types. It's just data representation. The problem with anonymous struct fields right now is that constructing that type requires re-specifying the whole structure of the anonymous struct, even though the compiler already knows what it has to be. cfg := Config {
Telemetry: struct {Port int; URL string; Password string} {
Port: 8888,
URL: ProdTelemUrl,
Password: os.Getenv("TELEMETRY_PASSWORD"),
},
DB: struct {ConnectionStr string; Password string} {
ConnectionStr: ProdDBUrl,
Password: os.Getenv("DB_PASSWORD"),
},
} If the go tool could elide the struct definitions this could be: cfg := Config {
Telemetry: {
Port: 8888,
URL: ProdTelemUrl,
Password: os.Getenv("TELEMETRY_PASSWORD"),
},
DB: {
ConnectionStr: ProdDBUrl,
Password: os.Getenv("DB_PASSWORD"),
},
} This is both easier to read and easier to write, and matches up really nicely with the struct definition. To avoid the verbose retyping of anonymous struct types, people either make separate types, which make you jump around just to see what all the fields for this config are, and make you wonder if there's actual logic on that TelemetryConfig struct, or they put all the fields in the base struct and prefix the names with their namespace, which is repetitive and harder to read because of all the long field names. Please update the go tool to allow us to elide types when constructing struct literals with fields that are also structs. It would make Go almost as concise as yaml, but without all the yaml footguns. |
It would be nice to be able to grab an anonymous struct type out of the parent: type Config struct {
Telemetry struct {
Port int
URL string
Password string
}
DB struct {
ConnectionStr string
Password string
}
}
var db Config.DB
// do something with db |
@natefinch I really think this is in the eye of the beholder, whether the struct type info is helping or not. FWIW, it makes your first example more legible to me if there is an explicit |
Inspired by Swift’s use of empty enums as namespaces, which can allow for less-mangled type names. I don’t want to hijack this proposal and will discuss elsewhere. |
Here's a really good comparison. Almost identical definitions, but the struct form doesn't let you elide the type: type User struct {
Email string
}
type GroupMap map[string]User
type GroupStruct struct {
Leader User
}
func main() {
_ = GroupMap{
"Leader": {
Email: "[email protected]",
},
}
_ = GroupStruct{
Leader: User{
Email: "[email protected]",
},
}
} |
@VinGarcia — Your summarizing comment from almost 3 years ago did not include as the reason for eliding probably my number 1 feature request for Go, and that is for use when passing options via an options struct: package main
type StandardWidgetOptions struct {
Foo int
Bar string
Baz bool
}
type StandardWidget struct {
Name string
Opts StandardWidgetOptions
}
func NewStandardWidget(name string, opts StandardWidgetOptions) *StandardWidget {
return &StandardWidget{
Name: name,
Opts: opts,
}
}
func main() {
// Current malaise
w1 := NewStandardWidget("my_widget1", StandardWidgetOptions{
Foo: 10,
Bar: "Hello",
Baz: false,
})
// Hoped for future
w2 := NewStandardWidget("my_widget2", {
Foo: 25,
Bar: "Sweet",
Baz: true,
})
// Or, also!
w3 := NewStandardWidget("my_widget2", {Foo: 100,Bar: "World",Baz: true})
// Stop Go complaining about unused vars
print(w1,w2,w3)
} Just commenting in hopes to keep the dream alive... 🙌 |
I read through this thread, and I believe I have a different use case. I want to loop over a slice of test cases. I also want to keep the test and expected values close together. With current syntax, it looks something like: type testData struct {
test []int
expected []int
}
var tests = []testData{
{[]int{0, 0}, []int{0, 0}},
{[]int{0, 1}, []int{1, 0}},
} I have 18 such test cases in this slice. In my opinion, all of the If I want to use this style, the below is also valid syntax: type testData [2][]int
var tests = []testData{
{{0, 0}, {0, 0}},
{{0, 1}, {1, 0}},
} and in this case the types are elided, but with this, I lose the ergonomics from the |
I agree with @mikeschinkel and @humdogm. Zig is solving that problem with Anonymous Struct Literals. They allow omitting the struct type of the literal, and can be coerced to an actual struct type if the coercion is completely unambiguous. |
Currently, the functional option pattern is often introduced as a way to achieve optional arguments in Go. However, I feel this approach is not appropriate because it introduces excessive complexity. If zero values were sufficient, we could simply use structs instead of functional option patterns (FOPs), but this is not widely recognized at the moment. If users are frustrated by having to write long struct names for every call and think, "Go has no way to achieve optional arguments except through FOP!" then I believe the introduction of this feature could lead many programmers to make better design choices. I also believe that being able to write shorter code, not only for FOP but also for functions that take tuples, such as in logging with log/slog, could lead to better designs as well. |
I am in favor of this proposal and will try to answer the concerns that have been raised. The key of map initialization and the field name of the structure are indistinguishable This is probably the most critical issue, in my opinion, as only structs can be untyped, or map and slice would need to have a separate syntax. I do not consider it that important to be able to write a map or slice that way, compared to being able to shortly define structures that have important uses, such as tuples and optional arguments. m := map{keyStr: "value"}
s := []{idxInt: "value"} // []string
v := {value: "value"} // struct{value string} Implicitness has a negative impact on readability I don't think this is a big problem; Go already has a lot of "if you don't know the definition, you don't know the type" situations. x := f() // what's the type of x?
ch <- {value: "value"} // what's the type of ch? |
Unless I'm misunderstanding something, that syntax overlap doesn't exist. There's no unquoted shortcut syntax for map keys currently. |
@DeedleFake it's not "unquoted keys", it's the fact that the key can be a variable: https://go.dev/play/p/TvEURZjvfHR |
It's already the case that you can't tell what the key of a composite literal is at parse time.
Eliding |
Hi, I'd be grateful to any soul who could enlighten me on this matter. ps: I am referring to this part of specification in particular : |
This proposal is neither accepted nor implemented. The part of the specification you're referencing is what this proposal proposes to amend. |
That would be specially useful for nested structs like for table-driven tests, json DTOs, and so on: tests := []struct{
desc string
val1 int
val2 bool
val3 string
user struct{
id string
name string
address string
}
}{
{
desc: "test name",
val1: 123,
user: {
id: "user id",
name: "name",
},
},
{
desc: "test name",
val1: 123,
user: {
id: "user id",
name: "name",
},
},
{
desc: "test name",
val1: 123,
user: {
id: "user id",
name: "name",
},
},
} instead of: tests := []struct{
desc string
val1 int
val2 bool
val3 string
user struct{
id string
name string
address string
}
}{
{
desc: "test name",
val1: 123,
user: struct{
id string
name string
address string
}{
id: "user id",
name: "name",
},
},
{
desc: "test name",
val1: 123,
user: struct{
id string
name string
address string
}{
id: "user id",
name: "name",
},
},
{
desc: "test name",
val1: 123,
user: struct{
id string
name string
address string
}{
id: "user id",
name: "name",
},
},
} |
Or, as a sensible gopher would write,
type testUser struct{
id string
name string
address string
}
tests := []struct{
desc string
val1 int
val2 bool
val3 string
user testUser
}{
{
desc: "test name",
val1: 123,
user: testUser{
id: "user id",
name: "name",
},
},
{
desc: "test name",
val1: 123,
user: testUser{
id: "user id",
name: "name",
},
},
{
desc: "test name",
val1: 123,
user: testUser{
id: "user id",
name: "name",
},
},
} Which, in this very specific case, is not really that awful. Overall, yeah, as it stands now, even though you get a concise, hopefully, clear name you can look up for more details about it, you also have to pollute the scope where the type of (in this case) @igorcafe, you're making a very valid point. But it has already been made before, and, sadly, does not contribute much more new information to the discussion here than a simple 👍 on the relevant prior posts. Be sure you haven't missed other ideas relevant to you in earlier posts! 😉 |
@antichris problem in that is that you need to create a new type that will only be used as a "nested type", which adds unnecessary complexity to the code, when the real relevant type is the top level type |
Similar to the last few posts, one of the major places where this has been pinching me lately is table tests with subtables. I'll have tests that are identical except for some shared config that would be awkward to manage if duplicated in dozens of consecutive rows or as separate tests that are identical except for some configuration. What I'd really like to do is just: var table = []struct{
/* shared config */
subtable []struct {
/* input/expected */
}
}{
{/*...*/, {
{/*...*/},
}},
} (There are many, many ways to write that now but they all involve adding extraneous bits that at best don't add anything to the code and at worst litter it with distractions.) |
Just another vote for this in table drive tests. While working with Go, this has always been painful and the proposal would make this much nicer. |
Composite literals construct values for structs, arrays, slices, and maps. They consist of a type followed by a brace-bound list of elements. e.g.,
I propose adding untyped composite literals, which omit the type. Untyped composite literals are assignable to any composite type. They do not have a default type, and it is an error to use one as the right-hand-side of an assignment where the left-hand-side does not have an explicit type specified.
Go already allows the elision of the type of a composite literal under certain circumstances. This proposal extends that permission to all occasions when the literal type can be derived.
This proposal allows more concise code. Succinctness is a double-edged sword; it may increase or decrease clarity. I believe that the benefits in well-written code outweigh the harm in poorly-written code. We cannot prevent poor programmers from producing unclear code, and should not hamper good programmers in an attempt to do so.
This proposal may slightly simplify the language by removing the rules on when composite literal types may be elided.
Examples
Functions with large parameter lists are frequently written to take a single struct parameter instead. Untyped composite literals allow this pattern without introducing a single-purpose type or repetition.
In general, untyped composite literals can serve as lightweight tuples in a variety of situations:
They also simplify code that returns a zero-valued struct and an error:
Code working with protocol buffers frequently constructs large, deeply nested composite literal values. These values frequently have types with long names dictated by the protobuf compiler. Eliding types will make code of this nature easier to write (and, arguably, read).
The text was updated successfully, but these errors were encountered: