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.,
x := []string{"a", "b", "c"}
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.
var x []string = {"a", "b", "c"}
var m map[string]int = {"a": 1}
type T struct {
V int
}
var s []*T = {{0}, {1}, {2}}
a := {1, 2, 3} // error: left-hand-type has no 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.
// Without untyped composite literals...
type FooArgs struct {
A, B, C int
}
func Foo(args FooArgs) { ... }
Foo(FooArgs{A: 1, B: 2, C:3})
// ...or with.
func Foo(args struct {
A, B, C int
}) { ... }
Foo({A: 1, B: 2, C: 3})
In general, untyped composite literals can serve as lightweight tuples in a variety of situations:
ch := make(chan struct{
value string
err error
})
ch <- {value: "result"}
They also simplify code that returns a zero-valued struct and an error:
return time.Time{}, err
return {}, err // untyped composite literal
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).
p.Options = append(p.Options, &foopb.Foo_FrotzOptions_Option{...}
p.Options = append(p.Options, {...}) // untyped composite literal
Comment From: adg
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/ https://codereview.appspot.com/5449067/
The code changes:
https://codereview.appspot.com/5449071/ https://codereview.appspot.com/5449070/ https://codereview.appspot.com/5448089/ https://codereview.appspot.com/5448088/
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.
Comment From: bcmills
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.)
Comment From: minux
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.
Comment From: neild
Under this proposal, the following assignments are identical:
var m map[string]int m = map[string]int{A: 1} m = {A: 1}
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. :)
Comment From: neild
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.
Comment From: jimmyfrasche
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
var t T = {}
since you could just do
var t = T{}
But the rest would certainly cut down on typing and allow nicer APIs in places.
For example,
Polygon({1, 2}, {3, 4}, {5, 4})
is arguably clearer than
Polygon([]image.Point{{1, 2}, {3, 4}, {5, 4}})
and the only alternative at present would be
Polygon(image.Point{1, 2}, image.Point{3, 4}, image.Point{5, 4})
Comment From: adg
I agree that the examples look nice, at a glance. But anything can look nice without context.
To move this proposal forward, one should apply the change to a corpus of real Go code so that we may observe its benefits and drawbacks in context.
On 7 October 2015 at 09:12, Damien Neil notifications@github.com wrote:
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.
— Reply to this email directly or view it on GitHub https://github.com/golang/go/issues/12854#issuecomment-146017331.
Comment From: minux
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.
Comment From: neild
To be clear, this proposal does not allow {A: 1} to implicitly become map[string]int{"A":1}.
Comment From: minux
https://github.com/golang/go/issues/12854#issuecomment-146016054 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}?
Comment From: bcmills
In that last example, A is an identifier (i.e. for a string variable or constant) - not a string literal itself.
Comment From: minux
did you mean that the code is actually:
const A = "A" var m map[string]int m = {A: 1}
Then there are more ambiguity in syntax. const A = "A" var x struct { A int } x = {A: 1}
What does this mean?
Note my concern is that it's possible to assign {A:1} to vastly different types: map[string]int and struct { A int } (what about map[interface{}]int and map[struct{string}]int?)
Comment From: neild
x = {A: 1}
is precisely equivalent to x = T{A: 1}
, where T
is the type of x
.
Comment From: minux
But we currently don't accept var m = map[string]int{A: 1}
Comment From: neild
We do, actually: http://play.golang.org/p/YubepmdVwy
A := "A"
var m map[string]int
m = map[string]int{A: 1}
fmt.Println(m)
Comment From: minux
We're talking in circles. In https://github.com/golang/go/issues/12854#issuecomment-146016054, 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.
Comment From: neild
If A is not otherwise defined, then the first case (m = {A: 1}
) will fail to compile with the same error you would get if it were written m = map[string]int{A: 1}
. i.e., it is syntactically valid but incorrect because A is undefined.
Comment From: griesemer
@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
1) x is assigned to a lhs (if x is an initialization expression, the lhs must have an explicit type) 2) x is passed as an argument 3) x is returned via a return statement 4) x is sent to a channel
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).
Comment From: bcmills
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.
Comment From: neild
On Tue, Oct 6, 2015 at 3:17 PM, Andrew Gerrand notifications@github.com wrote:
To move this proposal forward, one should apply the change to a corpus of real Go code so that we may observe its benefits and drawbacks in context.
I agree that it would be good to apply this change to a corpus of real code to observe its effects. I'm hunting through the stdlib to see if I can find a package that might change in an interesting fashion. Simply eliding all types in composite literals is uninformative, since the more interesting uses (e.g., lightweight tuples as function parameters) require some light refactoring.
Comment From: griesemer
@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.
Comment From: bcmills
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.)
Comment From: jimmyfrasche
The only other case I can think of (for consideration, if not inclusion) is eliding the type within another composite literal like
pkgname.Struct{
Field: {...},
}
for
pkgname.Struct{
Field: pkgname.AnotherCompositeLiteral{...},
}
Comment From: minux
FTR, I support more type elision (I proposed to use it to solve named parameter, see https://github.com/golang/go/issues/12296#issuecomment-134663135). 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.)
Comment From: ianlancetaylor
@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.)
Comment From: codeblooded
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 IsAlive
, a Male ≠ a Female.
Comment From: bcmills
what := {Name: "John"}
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.)
Comment From: bcmills
You would, however, be able to do the equivalent for plain assignments, as long as the variable has a concrete type:
var acc Account
acc = {Name: "Bob"} // ok: we already know that acc is an Account struct.
var profile interface{}
profile = {Name: "Bob"} // compile error: profile does not have a concrete type.
Comment From: codeblooded
@bcmills Ok… what would be the overhead on the compiler side of inferring the types and aborting if the type is ambiguous?
Comment From: mdempsky
None. The compiler already applies type inference for untyped nil (and untyped constants, though their default type conversion rules muddies the analogy). E.g.,
x := nil
is a compiler error, but
var y []byte
y = nil
var z interface{} = nil
are okay. The proposal for untyped struct literals is consistent with that.
Comment From: dsnet
Or another example:
var acc Account
acc = Person{Name: "Bob"}
// Compile error: cannot use Person literal (type Person) as type Account in assignment
The fact that the compiler can already complain about a type mismatch implies that the compiler already has the information to make this happen since it is expecting an Account type.
Comment From: codeblooded
Ok… makes sense. So is this a Go or a No? other thoughts
Comment From: extemporalgenome
I'm on the balance in favor of this proposal. I was already thinking about reasonable relaxations (such as elision when assigning into function-scoped variables).
There are some strange (probably useless) syntactic forms allowed with this degree of elision however, such as:
type T struct{}
T({}) // instead of T{}
type P struct { *P }
&P{{{{{}}}}}
Arguably the first form could be valid yet gofmt transformable into T{}
, but the second form is the simplest representation of that structure -- it's confusing but potentially legitimate.
Comment From: dsnet
I'm not particularly concerned about useless syntactic forms since that's always existed, but we don't really struggle with them today:
a := ((((((0)))))) // Arguably useless and is equivalent to 0
I do have a slight concern about the stylistic wars that may happen as a result. Which of the following would be in Go style?
var m = map[int]string{1: "foo", 2: "bar"}
var m map[int]string = {1: "foo", 2: "bar"}
FTR, I support more type elison and believe it will actually improve readability on complex structures like protobufs, where the embedded struct in use is obvious based on the field names used. The inclusion of the struct type is redundant and extra noise.
Comment From: mdempsky
Style warriors already have these options to contend over:
var i uint32 = 42
var i = uint32(42)
i := uint32(42)
I don't think untyped literals makes the situation significantly worse.
Comment From: extemporalgenome
I do have a slight concern about the stylistic wars that may happen as a result. Which of the following would be in Go style?
For consistency, it seems likely that the following would be favored:
var m = map[int]string{1: "foo", 2: "bar"}
It's what you have to do if you want to use the short declaration form, and leverages right-to-left inference. Since composite types can't be constant in Go, the current const elision rules have no impact on this particular question of style.
Comment From: rsc
Ok… makes sense. So is this a Go or a No? other thoughts
This proposal would be a very significant language change. If this were to happen I expect it would take many months of consideration. By all means continue the discussion, but if you insist on a quick answer, that answer will certainly be "no".
Comment From: glasser
One note on the "large parameter list as struct" use case. Let's say this proposal is accepted and people choose to use the idiom described above with anonymous struct types as arguments.
This will improve the most common way of calling the function by removing the redundant type name. But it will make it much more difficult to construct the argument to the function in a way that is not fully literal (eg if a field needs to be set conditionally) because it will be very difficult to declare an object of the argument type. While this proposal would not require libraries to define their functions using anonymous struct types as arguments, I would consider it a problem if this became popular, and it is the first example in the proposal.
Comment From: Perelandric
@glasser: That would be a bit of an annoyance, but it would be simple enough to use variables for those conditional values.
@codeblooded
WRT your Account
vs Profile
example https://github.com/golang/go/issues/12854#issuecomment-148102117, I would hope what := {Name: "John"}
would be allowed and would result in neither type but would rather be equivalent to this:
what := struct {
Name string
}{Name: "John"}
It would only have a named type if it was assigned to a typed variable/field/parameter or cast as Account(what)
.
This syntax would help with the problem @glasser describes.
params := {A: 1, B: 2, C: 3}
if true {
params.B += 40
}
Foo(params)
This could only be used for structs and not maps, of course, unless the syntax was changed to be unambiguous:
what := struct {Name: "John"}
const Name = "NAME"
what2 := map {Name: "John"}
Though perhaps parsing this would be more difficult.
Comment From: extemporalgenome
Question: what would happen if a package defined a function like the following?
type unexported struct { X string }
func DoSomething(opts unexported) {}
Would you allow another package to call the DoSomething function?
Comment From: jimmyfrasche
That's a very interesting point.
My gut says that yes is less complicated and more regular than no.
It would be somewhat analogous to
package example
type unexported string
func DoSomething(opt unexported)
//somewhere else
example.DoSomething("I know constants are a different thing")
The real conundrum is
package example
type unexported struct {
unexported string
}
func DoSomething(opt unexported)
//somewhere else
example.DoSomething({"this makes sense but also feels deeply wrong and bad"})
Comment From: minux
the visibility rules are unaffected by this change, so given:
type unexported struct { X string } func DoSomething(opts unexpected) {}
DoSomething still can't be called by external packages, unless there is an exported way to create structs of unexported type.
Regarding the conditional argument problem, it's not a problem, or rather, it's actually better than status quo. Note that to pass conditional arguments, you don't need to declare a variable of the argument struct type.
Consider this example: if something { f(a, b, 1) } else { f(a, c, 2) // a is duplicated, should arg1 in both branches stay the same? }
it will change to this for f that utilizes the proposal: arg2, arg3 := c, 2 if something { arg2, arg3 = b, 1 } f({A: a, B: arg2, C: arg3}) // now it's obvious, arg1 should always be a.
Comment From: neild
On Thu, Feb 4, 2016 at 8:17 PM, Kevin Gillette notifications@github.com wrote:
Question: what would happen if a package defined a function like the following?
type unexported struct { X string } func DoSomething(opts unexported) {}
Would you allow another package to call the DoSomething function?
Yes. This is already possible so long as the parameter's type is unnamed: somepackage.DoSomething(struct { X string }{"value"})
http://play.golang.org/p/HJ0pt8OEre
Comment From: bcmills
@Perelandric
I would hope
what := {Name: "John"}
would be allowed and would result in neither type but would rather be equivalent to this
That would be inconsistent with the proposal (which is more "allow elision of composite types" than "allow untyped composite literals"). As you noted, using it for untyped structs introduces an unpleasant ambiguity between structs and maps. It would also not work at all for composite literals using positional fields instead of explicit field names.
Besides, it would be inconsistent: an implicitly-typed literal would mean "a struct with only the mentioned fields" in some contexts and "a struct with non-mentioned fields having zero-values" in others. Given that Go does not (and IMO should not) apply subtyping to structs based on subsets of fields, that would make the feature surprising and non-orthogonal to the rest of the language.
@minux Honestly, I find that example more readable with a direct transliteration.
if something {
f({A: a, B: b, C: 1})
} else {
f({A: a, B: c, C: 2})
}
Comment From: bcmills
@neild: I think @minux is right about the unexported case. The elided type should be the exact type inferred from the context in which the literal appears, not an equivalent unnamed struct type.
Comment From: neild
On Thu, Feb 4, 2016 at 8:17 PM, Kevin Gillette notifications@github.com wrote:
type unexported struct { X string }
func DoSomething(opts unexported) {}
A more convoluted example which does get at your point might be:
type unexportedA struct { X string } type unexportedB struct { Y unexportedA }
func DoSomething(opts unexportedB) {}
I don't believe it is currently possible to call that function from outside its package. (Other than, maybe, by using reflection.)
I'm inclined to say that it if we permitted eliding the type on composite literals, it would be less complicated to allow calling that function with DoSomething({}) than to prohibit it.
Comment From: neild
On Fri, Feb 5, 2016 at 9:32 AM, Bryan C. Mills notifications@github.com wrote:
@neild https://github.com/neild: I think @minux https://github.com/minux is right about the unexported case. The elided type should be the exact type inferred from the context in which the literal appears, not an equivalent unnamed struct type.
I suppose the answer depends on whether the literals are untyped (in the same sense as untyped constants) or merely have their type elided (but still effectively present). I tend towards the former as being the simpler change, but I get the impression that this is a minority viewpoint. :)
Comment From: bcmills
@neild Yes, that's exactly the issue.
The major problem with the "untyped literal" perspective is the ambiguity (or asymmetry) between struct field names and map keys. Given the following program:
const animal = "gopher"
var x = {animal: "rabbit"}
what should the type of x
be? (Is it map[string]string
, or struct{ animal string }
?)
If that question has a well-defined answer, then the spec for untyped literals must contain an arbitrary bias toward either structs or maps and programmers will have to remember which way that bias runs. (The situation is analogous to C++'s "most vexing parse".)
If ambiguous cases are defined as a program error, users will be confused and surprised when they accidentally run afoul of it. (Analogous to Go's existing parsing ambiguity for composite literals in brace-delimited control-flow statements.)
Comment From: neild
On Fri, Feb 5, 2016 at 9:52 AM, Bryan C. Mills notifications@github.com wrote:
The major problem with the "untyped literal" perspective is the ambiguity (or asymmetry) between struct field names and map keys. Given the following program:
const animal = "gopher" var x = {animal: "rabbit"}
what should the type of x be? (Is it map[string]string, or struct{ animal string }?)
I don't think we should attempt to define default types for untyped/type-elided literals, so that would be an error no matter what.
Comment From: Splizard
Would be good to implement this for constants at least. eg {"a", "b", "c"} => []string{"a", "b", "c"}
Comment From: adg
@splizard constants cannot be slices. See: https://blog.golang.org/constants
Comment From: adg
I think this is waiting for a detailed proposal and/or a prototype (maybe in the form of a gofmt simplifier rule). We need to see how this works in practice, surveying a wide variety of extant go code.
Comment From: adg
The title of this issue should probably be changed. The composite literals are not "untyped", but rather the type name is elided. It's more like "type inferred composite literals".
Comment From: adg
@ianlancetaylor has more ideas for potential titles for this issue.
Comment From: DeedleFake
While this would be very nice for certain cases, I can definitely see the arguments about readability. That being said, how about a more conservative proposal for while the finer points of this are being debated? What if type name ellision was allowed for anonymous structs in any use, but not for other cases except where it's already allowed? This would help with some of the most common cases, such as embedded anonymous structs for, for exanple, JSON marshalling/unmarshalling, and things like c := make(chan *struct {arg string; ret chan<- string})
, but with hopefully less impact on general readability.
For example, this would be allowed:
type Example struct {
Inner struct {
Val string
}
}
func main() {
e := Example{
Inner: {Val: "This is an example."},
}
}
This would not be:
type Example struct {
Inner ExampleInner
}
type ExampleInner struct {
Val string
}
func main() {
e := Example{
Inner: {Val: "This is an example."},
}
}
Similarly, this would be allowed:
func main() {
var e struct {val string}
e = {val: "This is an example"}
}
That last example is a potential issue, and my biggest problem with this suggestion, but I don't think it would be too much of an issue in practice.
Comment From: bcmills
@DeedleFake That would lose a lot of the benefit of the proposal (e.g. for Protocol Buffers), and adding more special-cases ("only for unnamed struct types") doesn't do a lot to reduce the cognitive burden added by the feature.
Comment From: dsnet
I agree with @bcmills. My opinion is that the language rules should be simple and I think many of the concerns regarding readability should actually become lint checks. Suppose this proposal passes, and we understand better how people properly and improperly use it (style-wise), we can develop lint rules that guide some sane usage of type elision.
Comment From: rsc
There are a lot of implications to a change like this, even though the change itself is completely syntactic sugar. It seems best to leave this for consideration as part of potentially bigger changes to the language in Go 2, rather than add one reasonable thing after another without a coherent plan for when we stop.
Comment From: alercah
This seems related to #19642, in that both require a limited type inference. Perhaps it makes sense, then, to define something like a typed context which includes the following cases (I may have missed one or two, apologies if so):
- The rhs of an assignment
- The rhs of a variable or constant declaration that includes an explicit type declaration
- A return statement
- A send statement
- A function argument
- A composite literal (possibly other than an unkeyed struct literal)
In these cases, the type is known from context and so a composite literal (or _
, if that is accepted as the zero value) is permitted.
Comment From: jimmyfrasche
As a micro-experience report I just ran into a situation today where I had a type that could have been
type task struct {
// a bunch of other stuff
accumulator map[string]struct{
ref string
from *T
}
}
accumulator
only exists transiently to collect data during task.run
and gets processed into a useful result before returning. Much of the code only reads accumulator
so the type's name doesn't matter. But I had to insert values into accumulator
multiple places (in various methods of task
that run
calls) so I ended up writing
type (
//exists only to have less awkward composite literal syntax
thisIsAUselessType = struct {
ref string
from *T
}
task struct {
map[string]thisIsAUselessType
}
)
If I had been able to write
aTask.accumulator[k] = { ref: theRef, from: from }
the package code, as a whole, would have been clearer because I wouldn't have had to create a type at the top level.
As it is I have a note that the type is only used by task
so a reader doesn't think it has some wider importance or scope. I would have much rather kept it contained.
(Equivalently, I could have written a method on task
that takes two strings and a *T
and only have to have written the struct definition twice. Or I could have just written out the definition each time I needed. Aside from verbosity, it would be more awkward to read than without the type as I would have been breaking idiom merely for a principle)
Edit: of course there would still be the map[string]struct{ ref string; from *T}{}
in constructing the task
. I'm fine with that.
Comment From: bcmills
Here's another nice use-case: constructing a new slice with the contents of some (read-only) slice and a suffix. (That comes up when appending a call-specific options to a list of default options with APIs that accept variadic options, such as in grpc.)
See also https://github.com/golang/go/issues/24204#issuecomment-369769721.
Instead of
opts := append(append([]grpc.CallOptions{}, defaultOptions...), extraOptions...)
we could write:
opts := append(append({}, defaultOptions...), extraOptions...)
or, with https://github.com/golang/go/issues/18605:
opts := append({}, defaultOptions..., extraOptions...)
Comment From: ghost
I think in certain cases that using this proposal would significantly reduce readability.
For example, having a global var declared in another file and set it with elluded literal can easily cause confusion.
Yet I can see the proposal is of much value and has its potential with go, so I think we might need to set some rules about those readability redunction.
Rules can be spec rules that compiler would refuse to compile some cases, or it can be a vet rule which warns users on bad-written code. In my oppionion, vet rules can be more flexible and more aggressive, so I would say vet rules.
Comment From: UFOXD
this is great
Comment From: myitcv
@clareyy please be aware of https://github.com/golang/go/wiki/NoPlusOne. Feel free to add responses using emoji reactions instead.
Comment From: UFOXD
@clareyy please be aware of https://github.com/golang/go/wiki/NoPlusOne. Feel free to add responses using emoji reactions instead.
sorry ,my fault
Comment From: chowey
To me, this proposal seems very Go-like. To be honest, I intuitively expect Go to already work like this and I'm surprised/annoyed when the compiler doesn't allow it.
func sendValues(w io.Writer, values []string) error {
// Here is some complex JSON struct needed to provide my values to a
// 3rd-party web service.
var v struct {
Result struct {
Location []struct {
Value string
}
}
}
for _, v := range values {
// I'd expect Go to help me out.
v.Result.Location = append(v.Result.Location, {Value: v})
// COMPILER ERROR
}
return json.NewEncoder(w).Encode(v)
}
So instead I often find myself doing something like this:
func sendValues(w io.Writer, values []string) error {
type Location struct {
Value string
}
var v struct {
Result struct {
Location []Location
}
}
for _, v := range values {
v.Result.Location = append(v.Result.Location, Location{Value: v})
}
return json.NewEncoder(w).Encode(v)
}
This is less legible to me, since I don't "see" the JSON struct with v
. I have to piece it together mentally. It gets worse with more complex structs.
Sometimes I don't want to dignify a struct with a type name, particularly for once-off uses like this. I don't think that's wrong. I think it is idiomatic Go (or would be if the compiler allowed it).
Comment From: divan
Thought experiment/experience report where this can be useful (not the deal breaker though): https://divan.github.io/posts/flutter_go/
Comment From: benpate
I came here to try and express this same need in my code, and was relieved that so many other people had already described it so much better than I can. This proposal cleanly solves one of the major sources of stutter in my code base, along with commonly requested "named parameters" feature that I miss from previous languages. So I'm really, really hoping this idea makes it into some version of Go2 on the horizon.
My specific use case is in trying to implement complex, standards-based, web APIs. These typically have deeply nested structures and include many varying data types. It's not feasible to improve the data structure itself (as much as I'd like to) because it's a standard API that I don't control. I see this proposal as a practical solution to the reality that real data is messy and outside of application developers' control.
I do want to add one small thought about readability: Source code is not a static document, because modern tooling (such as gocode
) includes code suggestions and tool-tips for every name on the screen. With this proposal, if something is ever unclear or unfamiliar, it would still be easy for the reader to see the elided types by just moving their mouse over the line in question. The current tooling in place makes this an easy win with very little downside.
Comment From: randall77
I just read through this whole issue (phew!) and didn't see any discussion of the way I always use struct literals, which is with &
in front:
type T struct {
Name string
}
var t *T
t = &T{Name: "foo"}
So how do I write that with this proposal?
type T struct {
Name string
}
var t *T
t = &{Name: "foo"}
Does the type implication flow down through the &
?
Comment From: jimmyfrasche
Since you can write
[]*T{
{Name: "foo"},
}
it would follow that it would just be
t = {Name: "foo"}
Comment From: randall77
Hmmm..., I'm not a fan of that. I want to be explicit about the address operation.
But then, I'm not a fan of the missing &
in your first snippet either. And that's already in the spec :(
Comment From: tj
It kind of reduces clarity a bit but editors should be able to expand the type easily, 👍 from me I think. I'd much prefer this to the "functional options" convention going around, those have horrible discovery unless they're all prefixed with With*
etc, we could use some stronger conventions here IMO.
Comment From: steebchen
This proposal is a game-changer. It can make code so much simpler to write and read. For example, tools like prisma.io which provide a library over your complete database schema could make use of this to not end up in parameter structs like UserUpdateParams
, CreatePostWithoutUsers
etc.
Comment From: mvdan
I've seen yet more people confused by the old title, so I've gone ahead with @adg's suggestion from 2016. If anyone has a better title in mind, please go ahead or leave a comment.
Comment From: DeedleFake
those have horrible discovery unless they're all prefixed with With* etc, we could use some stronger conventions here IMO.
Not always. If you type the output, then go doc
will list them as constructors. For example:
type Option struct {
do func(s *settings)
}
func Strict() Option {
return Option{
do: func(s *settings) {
s.strict = true
},
}
}
func Separators(sep string) Option {
return Option{
do: func(s *settings) {
s.sep = sep
},
}
}
That being said, I'd still really like the type ellision that was proposed here.
Comment From: tj
@DeedleFake maybe in the docs, but in auto-complete output it's usually not super clear
Comment From: DeedleFake
That sounds like a problem with the autocomplete. I would expect an autocomplete to show only things which could return something that's useful for the given place that it's completing, so if you've typed funcThatTakesOptions(example.
so far, then I would expect it to list all of the functions that return an Option
. I don't usually use an autocompleting editor for Go, though. Is that not how they work?
Comment From: bcmills
@DeedleFake, I believe that the gopls
tool (under active development) is intended to be the supported solution for autocompletion, and I know that it does type-checking when it can, so I, too would expect it to perform type-sensitive completions when possible, even when the types are inferred.
@stamblerre or @ianthehat could probably answer that more definitively, though.
Comment From: stamblerre
Yes, gopls
always uses type information for autocompletion, and it ranks results that match the expected type much higher than other results.
Comment From: turtleDev
I'm a bit icky about structs, but maps and slices are a big +1 for me. One thing that concerns me is that it may make code easier to type out, but it may hurt readability, and may confuse beginners a bit.
Comment From: tj
@turtleDev agreed, I feel like if the type signature is visible elsewhere then there's no need to repeat it (return type for example), but otherwise it might hurt things.
Comment From: ehsan-khaliki
What is the state of this issue?
Comment From: ianlancetaylor
@ehsan-khaliki This issue is open. It has neither been accepted nor rejected.
Comment From: coolcodehere
great issue!
Comment From: beoran
@adg, I just read that original spec, and I think that implementing that alone would be largely sufficient to solve this issue. The original spec still required the outer type, only the inner keys and fields can be omitted. I think the idea of this being less clear was not correct. Especially to work more easily with the option struct idiom, this would simplify calling functions.
Comment From: quenbyako
So, will it be implemented someday? I mean, this issue is opened 5 years in a row 👀
Comment From: DmitriyMV
It's a proposal - it is not yet (or at all) accepted, cause there is not clear vision on how it should be done.
Comment From: quenbyako
@DmitriyMV looks like this feature is already added, but in so specific cases like this one. I thing, this proposal don't break current language spec, and it isn't too hard to implement.
To be honest, i want to discuss, this thing is really useful)
Comment From: quenbyako
@neild are you still open to talk about it?
Comment From: neild
Not sure what there is for me to talk about; I still think this is a good feature, but the proposal has not (yet?) been accepted. I expect that it's unlikely to be seriously reconsidered while generics are still in progress.
Comment From: griesemer
@ololosha228 What @neild said. Every language change, however small adds significant cost (compiler, tools, documentation, updating libraries if desired, eduction, etc.). We don't want to take on new things while we're trying to make progress on the generics front.
Comment From: tooolbox
I'm a little surprised at how many people are coming out in favor of this. I think the general concept of "elide type information where we can infer it" will harm the maintainability of Go code. I believe Dave Cheney said it best:
So much has been said, about the importance of readability, not just in Go, but all programming languages. People like me who stand on stages advocating for Go use words like simplicity, readability, clarity, productivity, but ultimately they are all synonyms for one word–maintainability.
The real goal is to write maintainable code. Code that can live on after the original author. Code that can exist not just as a point in time investment, but as a foundation for future value. It’s not that readability doesn’t matter, maintainability matters more.
Go is not a language that optimises for clever one liners. Go is not a language which optimises for the least number of lines in a program. We’re not optimising for the size of the source code on disk, nor how long it takes to type the program into an editor. Rather, we want to optimise our code to be clear to the reader. Because its the reader who’s going to have to maintain this code.
If you’re writing a program for yourself, maybe it only has to run once, or you’re the only person who’ll ever see it, then do what ever works for you. But if this is a piece of software that more than one person will contribute to, or that will be used by people over a long enough time that requirements, features, or the environment it runs in may change, then your goal must be for your program to be maintainable. If software cannot be maintained, then it will be rewritten; and that could be the last time your company will invest in Go.
Can the thing you worked hard to build be maintained after you’re gone? What can you do today to make it easier for someone to maintain your code tomorrow?
Most of the commentary here seems to be about writing code, not reading it, which is why I bring this up.
Some people have brought up how editors can help expose the type information. I would rather the editor helped me write the code, with auto-completion, so I don't need its help to read it later when the code is no longer in my editor (perhaps it's on GitHub?).
Compare the two lines below. If you're in an unfamiliar codebase, which is more helpful?
a.b <- {Bar: "baz"}
a.b <- pkg.Foo{Bar: "baz"}
It might be an unpopular opinion in this thread, but I don't think this proposal will help Go in the long run, as a language to facilitate programming at scale. The future readers of Go code will thank us for staying explicit rather than implicit.
Comment From: dsnet
As a counter argument to above, the snippet below becomes more readable with type-eliding:
compress.NewReader(r, {Level: compress.BestSpeed})
compress.NewReader(r, compress.NewReaderOptions{Level: compress.BestSpeed}
You'll find examples that may get better and examples that may get worse if eliding were always performed. Personally, I prefer to at least have this choice.
Comment From: tooolbox
As a counter argument to above, the snippet below becomes more readable with type-eliding
Your "bad case" can be improved through existing Go mechanisms.
// Alias the package name
cmp.NewReader(r, cmp.ReaderOptions{Level: cmp.BestSpeed})
// Use the functional options pattern
compress.NewReader(r, compress.WithBestSpeed())
// Or both
cmp.NewReader(r, cmp.WithBestSpeed())
You can also split your code onto two lines if it gets verbose.
Comment From: cespare
@tooolbox
Compare the two lines below. If you're in an unfamiliar codebase, which is more helpful?
go a.b <- {Bar: "baz"} a.b <- pkg.Foo{Bar: "baz"}
One case in which the first version is more readable is when it is
a.b <- {Foo: "...", Bar: 123}
rather than
a.b <- pkg.FooAndBar{Foo: "...", Bar: 123}
This is not hypothetical: I often see tiny tuple structs (often with two members) that only exist for piping data through a channel, because the language does not support this more directly (unlike with function returns, say). Naming the type in these cases is mostly just stutter.
Having the choice seems better than not.
Comment From: urandom
Compare the two lines below. If you're in an unfamiliar codebase, which is more helpful?
go a.b <- {Bar: "baz"} a.b <- pkg.Foo{Bar: "baz"}
Looking at these two snippets, I don't think any one of them is more readable than the other. We don't know what the type of a.b
is, so we have to assume that it's the same type as what is being sent. In that regard, Both lines convey the information that some value is being sent on a channel, and we presume the types are correct or it wouldn't compile. We know nothing about type type pkg.Foo
, so it being present doesn't really improve readability that much. They only thing it tells us is that a.b
might be of the same type, but as a casual reader, that doesn't mean much. We don't know and can't deduce any of the properties of that type by just reading it's name.
So, besides being longer, how does the second line really improve readability?
Comment From: alvaroloes
As a counter argument to above, the snippet below becomes more readable with type-eliding:
go compress.NewReader(r, {Level: compress.BestSpeed}) compress.NewReader(r, compress.NewReaderOptions{Level: compress.BestSpeed}
You'll find examples that may get better and examples that may get worse if eliding were always performed. Personally, I prefer to at least have this choice.
I find this comment to be "most correct". There are certain situations in which one way is way more readable than the other and vice-versa. Having the choice would allow programmers to use the best approach in every case (and, of course, also the wrong approach, but this can be learned)
Something similar happens with the current syntax when defining a struct literal that allows you not to write the field names and just write the values separated by commas:
type WordCount struct {
word string
count int
}
// --> Specifying field names:
var goCount = WordCount{
word: "go",
count: 11,
}
var golangCount = WordCount{
word: "golang",
count: 8,
}
// --> Eliding field names:
var goCount = WordCount{"go", 11}
var golangCount = WordCount{"golang", 8}
In my opinion, eliding field names is almost always worse, but in certain situations where the meaning is clear, using it can improve readability a bit (by avoiding repeating the same names over an over again).
This proposal (inferred composite literals) has many more beneficial use cases than field name elision, so I'm in favor of it
Comment From: benpate
It's really great to watch such a thoughtful discussion of a (relatively small) aspect of a programming language. Go has always done a great job at this, along with having first-rate supporting tools, like go fmt and gpls. You all are awesome.
One small point that seems relevant to me is that developers aren't building Go programs with Notepad. Modern editors can all use gpls to include additional code insights when they're needed, to dig deeper into a type or get more information about it than is included in the current source document.
Looking at gpls in this way, Go ALREADY hides tons of type information from source code. It's necessary to minimize repetition. I think the issue here is "how much is too much." Above, @alvaroloes shows other places where Go has already chosen to make struct field names optional. I agree that "this is almost always worse" but it doesn't seem to have troubled the rest of the Go community so far. It can be used when needed to simplify the source code, and ignored otherwise.
So, this proposal makes a lot of sense to me. It's within the bounds of what the Go team has already determined for readability and would go a long way to "avoiding stutter" which is always a good thing.
Comment From: tooolbox
go a.b <- {Bar: "baz"} a.b <- pkg.Foo{Bar: "baz"}
Looking at these two snippets, I don't think any one of them is more readable than the other. We don't know what the type of a.b is, so we have to assume that it's the same type as what is being sent. In that regard, Both lines convey the information that some value is being sent on a channel, and we presume the types are correct or it wouldn't compile. We know nothing about type type pkg.Foo, so it being present doesn't really improve readability that much. They only thing it tells us is that a.b might be of the same type, but as a casual reader, that doesn't mean much. We don't know and can't deduce any of the properties of that type by just reading it's name.
So, besides being longer, how does the second line really improve readability?
In the second version, you know that a.b
is a channel of pkg.Foo
or an interface that pkg.Foo
satisfies, therefore that line contains more information. This is helpful when you are studying an unfamiliar codebase. Perhaps that's not something you personally want, but one can hardly argue that the lines are equivalent.
I'm seeing a pattern in the comments here:
You'll find examples that may get better and examples that may get worse if eliding were always performed.
One case in which the first version is more readable is
There are certain situations in which one way is way more readable than the other and vice-versa.
This says to me that the proposal is too broad.
It's a property of Go code that it's uniform and unsurprising to the point of being boring, an attribute decried by language designers and praised by maintenance engineers. Broad introduction of optional type elision would muddy the concept of idiomatic Go; new engineers may opt to elide all their types, just because they can, and produce codebases that are difficult to understand. Consigning the problem to style guides or linters is closing the barn door after the horse is gone, and not in keeping with the spirit of a language that treats unused imports as errors. Relying on editors ignores all the other places source code can live or be seen (does your source control or diff tool have intellisense?)
However, that's not to say that all type elision is bad. Eliding types in literals for slice elements, and for map keys & values, is superb. That's a case where it's proven to be helpful and not harmful because "the type signature is right there, so why repeat yourself?"
This is not hypothetical: I often see tiny tuple structs (often with two members) that only exist for piping data through a channel, because the language does not support this more directly (unlike with function returns, say). Naming the type in these cases is mostly just stutter.
Perhaps this is a case for a more directed proposal.
What if type-elided structs are permitted when passed as an argument, sent on a channel, returned in a function, etc. when the argument type, channel type, etc. is itself an anonymous definition?
// This example then becomes valid.
ch := make(chan struct{
value string
err error
})
ch <- {value: "result"}
// This is valid.
func sendValues(w io.Writer, values []string) error {
var v struct {
Result struct {
Location []struct {
Value string
}
}
}
for _, v := range values {
v.Result.Location = append(v.Result.Location, {Value: v})
}
return json.NewEncoder(w).Encode(v)
}
// Not valid, it's the named type grpc.DialOption
opts := append(append({}, defaultOptions...), extraOptions...)
// Also not valid, unless the signature of `NewReader` defined struct{Level int}
compress.NewReader(r, {Level: compress.BestSpeed})
By adopting this rule, you can look at {Foo: bar}
and know the following:
- It's an anonymous struct, not a named type.
- Its type definition is available in full wherever you are passing or returning or sending it.
That example proposal is off the cuff, but it illustrates a focused change that solves an ergonomics issue, without creating a footgun to broadly hide type information in the language. The benefits of usability might actually be worth the cost in lost information. The same can't be said for allowing general type elision.
Again, that's just an example. The point is we should be focused and conservative about where and how we permit eliding information in source code. Go is and should continue to be optimized for the next guy to come along, rather than for brevity. If we go too far in removing information, we will come full circle with proposals to put it back (not quite the same, but see the subject of dot imports at https://github.com/golang/go/issues/29036 and https://github.com/golang/go/issues/29326).
Comment From: neild
There's been a lot of discussion on this proposal already. While more input is fine, I don't think we're really treading any new ground here.
Fundamentally, the arguments for and against this proposal are:
- We should not do it, because eliding the top level type of a composite literal can be confusing. (I do not believe anyone has argued that it is always confusing.)
- We should do it, because eliding the top level type of a composite literal can improve readabililty. (I do not believe anyone has argued that it always increases readability either.)
- The decisive argument for now: We aren't making significant language changes until generics are finished. (And possibly not even then, but definitely not right now.)
I do not see an objective argument for either points 1 or 2; it's a judgement call.
One argument which has been made which I do not believe is useful is that explicit is always better than implicit. If that is the case, we should avoid writing x := f()
(which implicitly assigns a type to x
) as opposed to var x T = f()
. If we accept that eliding types in short variable declarations is reasonable, then eliding types in other areas is not prima facie unreasonable.
But it doesn't matter for now, because no decision is being made on this either way any time soon. There are higher priorities.
Comment From: urandom
In the second version, you know that
a.b
is a channel ofpkg.Foo
or an interface thatpkg.Foo
satisfies, therefore that line contains more information. This is helpful when you are studying an unfamiliar codebase. Perhaps that's not something you personally want, but one can hardly argue that the lines are equivalent.
I never said they were equivalent. I said the second line doesn't make the code more readable. More information does not necessarily make things more readable, sometimes it has the opposite effect. What is pkg.Foo
in this case? If this is an unknown codebase ,you'd have to look up the definition of pkg.Foo, which means you already have to jump around, of if you are lucky and have the codebase in your editor, you'd have to jump to the definition. This is something you can do for any type already, including the potential elided example in the previous line. In both cases, you'd have to stop your reading and go look something up. Now lets consider if the codebase is known. You could already know what pkg.Foo
is, so that helps. But you could already know what a.b
is as well, which means you know what the elided type is. There could be a potential gain in readability here, but that really depends on how well you know the codebase.
Comment From: jimmyfrasche
It's hard to say whether a single line is clearer in isolation. The context could be something like this:
a.b = make(chan pkg.Foo)
go a.run()
a.b <- pkg.Foo{Bar: "baz"}
Comment From: urandom
@jimmyfrasche That context seems a bit too trivial and might suggest that elision is preferred.
Here's a simplified real world example with two candidates for elision:
func (c *Store) GetProducts(mapping map[string]interface{}) (products []Product, err error) {
hash, err := getHashForMapping(mapping)
if err != nil {
return []Product{}, err
}
cache.mu.RLock()
products, okProducts := cache.m[hash]
cache.mu.RUnlock()
if okProducts {
return products, nil
}
chanResult := make(chan productsJobResult)
c.chanGetProductsJob <- productsJob{
hash: hash,
mapping: mapping,
site: site,
language: language,
chanResult: chanResult,
}
result := <-chanResult
if result.err != nil {
return []Product{}, result.err
}
return result.products, nil
}
The first candidate is the return instance. To me, that seems like a good candidate for elision, since the type is quite local, being in the signature of the function itself. So rather than []Product{}
, one could write {}
without sacrificing anything.
The other is the channel send line. You guys have no idea what the productsJob
struct is, so the question is, is having that present helping your readability in any way? If I was new to this piece of code, my personal opinion would be that the type doesn't add anything of value here. Without the type, it would still be clear that whatever type the channel wants, has the fields present during instantiation. The type could have more fields, but that's not something that's obvious even if the type itself is present. In both cases, one would have to go to the definition of the type to see what kind of fields are available.
Comment From: ghost
I feel like rather than eliding productsJob, it would be better to give c.chanGetCatalogueProductsJob a shorter name.
It is obvious in most cases that it is a channel of productsJob, so this is really tedious.
I think the problem is that there is currently a Go Idom of having short names and let types (especially function signature) give more information, but it is not as widely adopted as other Go Idoms.
It seems this issue and that idom is aiming to solve similar issues, but is heading different directions, which causes conflicts implicitly.
On Thu, Sep 24, 2020, 3:40 PM Viktor Kojouharov notifications@github.com wrote:
@jimmyfrasche https://github.com/jimmyfrasche That context seems a bit too trivial and might suggest that elision is preferred.
Here's a simplified real world example with two candidates for elision:
func (c *Store) GetProducts(mapping map[string]interface{}) (products []Product, err error) { hash, err := getHashForMapping(mapping) if err != nil { return []Product{}, err }
cache.mu.RLock() products, okProducts := cache.m[hash] cache.mu.RUnlock() if okProducts { return products, nil }
chanResult := make(chan catalogueProductsJobResult) c.chanGetCatalogueProductsJob <- productsJob{ hash: hash, mapping: mapping, site: site, language: language, chanResult: chanResult, } result := <-chanResult if result.err != nil { return []Product{}, result.err }
return result.products, nil }
The first candidate is the return instance. To me, that seems like a good candidate for elision, since the type is quite local, being in the signature of the function itself. So rather than []Product{}, one could write {}without sacrificing anything.
The other is the channel send line. You guys have no idea what the productsJob struct is, so the question is, is having that present helping your readability in any way? If I was new to this piece of code, my personal opinion would be that the type doesn't add anything of value here. Without the type, it would still be clear that whatever type the channel wants, has the fields present during instantiation. The type could have more fields, but that's not something that's obvious even if the type itself is present. In both cases, one would have to go to the definition of the type to see what kind of fields are available.
— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/golang/go/issues/12854#issuecomment-698173981, or unsubscribe https://github.com/notifications/unsubscribe-auth/AIAYXPTNVGM7QZEDHEN45NTSHLZWPANCNFSM4BRIOL7A .
Comment From: urandom
I feel like rather than eliding productsJob, it would be better to give c.chanGetCatalogueProductsJob a shorter name.
While I fully agree with you, this is just real-world code that's written by someone, which I as a reader picked up as an illustration where type elision might be desirable (or not). I think naming is not in the scope of this issue, and shouldn't be discussed, at least not here.
Comment From: ghost
As I stated in the previous comment, the current idiomatic naming convention is based on the facts that type information is (almost) always presented, and thus I think eliding (hiding) type information is very related.
It seems to me that many examples, including this one you just showed, seems to be a "naming convention vs hiding types" issue, with a few others that feels only eliding can remove its boilerplate (e.g., repeating type names in map literals).
On Thu, Sep 24, 2020, 11:14 PM Viktor Kojouharov notifications@github.com wrote:
I feel like rather than eliding productsJob, it would be better to give c.chanGetCatalogueProductsJob a shorter name.
While I fully agree with you, this is just real-world code that's written by someone and is only meant as an illustration where type elision might be desirable (or not). I think naming is not in the scope of this issue, and shouldn't be discussed, at least not here.
— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/golang/go/issues/12854#issuecomment-698409292, or unsubscribe https://github.com/notifications/unsubscribe-auth/AIAYXPUSOA6GPAWOQWYDPU3SHNO4TANCNFSM4BRIOL7A .
Comment From: VinGarcia
Summarizing the discussion so far
Since I want this discussion to advance and it has got very long by now, I decided to prepare a summary of all the important pros and cons so that we can have an easier time moving forward.
Since this was a long discussion I had to choose which arguments to keep but I believe I was able to select all the important ones.
I also tried to avoid adding too much of my own view in the quotes from others by being explicit in the parts where I had an opinion and always keeping a link to the original comments.
Wrapping up what has been discussed
This proposal regards extending the rules for allowing type elision to work in more cases than they already do.
It does not propose the creation of new types nor untyped constructs.
This proposal should not change in any way what we can do with the language, it will only allow the type to be omitted in situations where the compiler can infer the type from the context, and in situations where it can't, it will just cause a compilation error as it already does.
We are currently discussing a few different places where type elision could be performed:
- When passing arguments to a function:
golang Polygon({1, 2}, {3, 4}, {5, 4})
- When creating nested structures:
golang var v struct { Location []struct { Value string } } v.Location = append(v.Location, {Value: v})
Or:golang pkgname.Struct{ Field: {...}, }
- When using channels:
golang ch := make(chan struct{ value string err error }) ch <- {value: "result"}
- When returning from a function:
golang // Not using type elision: return time.Time{}, err // Using type elision: return {}, err
- When using maps:
golang var m map[string]int m = {"a": 1}
- When using slices:
golang type T struct { V int } var s []*T s = {{0}, {1}, {2}}
Discussed advantages
Its easy to implement and won't make the compiler any slower
@griesemer stated that the implementation of this proposal is straightforward since this is simply a relaxation of existing rules.
@mdempsky stated that it should not add any overhead on the compiler since it already applies type inference for untyped nil.
Could improve readability
@chowey showed an example where having type elision would allow him to work with nested structs without giving them names, which he argues would make it more readable. The example he used is the following:
func sendValues(w io.Writer, values []string) error {
// Here is some complex JSON struct needed to provide my values to a
// 3rd-party web service.
var v struct {
Result struct {
Location []struct {
Value string
}
}
}
for _, v := range values {
// I'd expect Go to help me out.
v.Result.Location = append(v.Result.Location, {Value: v})
// COMPILER ERROR
}
return json.NewEncoder(w).Encode(v)
}
The editor should be able to compensate for any loss of information
@tj stated that using struct arguments would be more discoverable by the IDE than the current idiom of "functional options" which would be an advantage.
I highlighted this comment especially because I think that the "functional options" idiom is harmful to readability and this proposal might help discouraging it.
Using structs as tuples often causes Stutter
@cespare argues that since there is no easy way of creating anonymous structs or tuples in Go he often sees lines such as this one:
a.b <- pkg.FooAndBar{Foo: "...", Bar: 123}
In which the typename only causes stutter and adds no benefits for readability at all.
Reported concerns
Stylistic wars regarding how to declare variables
@dsnet is concerned with stylistic wars regarding where to put the type on declaration such as:
var m = map[int]string{1: "foo", 2: "bar"}
var m map[int]string = {1: "foo", 2: "bar"}
Note: Most people apparently don't share this concern, namely @mdempsky and @extemporalgenome replied saying they don't think this will start a war.
People might be encouraged to use more anonymous structs
@glasser is concerned with the possibility of libraries starting using anonymous structs for the arguments options, e.g.:
(I wrote the examples below to illustrate what I understood of his argument)
package example
func New(args *struct{
Arg1 int
Arg2 string
// ...
}) ExampleType {
// ...
}
Since this could make it harder to instantiate a variable of that type on the programmers' side, e.g.:
// This struct would have to be declared
// exactly like the original one:
var arg *struct{
Arg1 int
Arg2 string
// ...
}
if someCondition {
arg = &{Arg1: 10, Arg2: "foo"}
}
exampleInstance := example.New(arg)
Note: Personally I don't think this will be an issue, since it is easy to work around this problem in the same way it is easy to do it with positional arguments.
Loss in readability/maintainability
@leafbebop is concerned about the readability loss that might be caused by this proposal, if for example the variable is declared in one package as a global variable and set in another package.
@tooolbox thinks this will hurt the readability of Go code since this is optimizing writing speed not reading clarity. So he argues against this feature for this reason.
He mentions the following example where he suggests that the explicit one is more readable:
a.b <- {Bar: "baz"}
a.b <- pkg.Foo{Bar: "baz"}
@dsnet counter argues that it depends on the specific example and shows an example where he believes the readability will actually be improved by type elision:
compress.NewReader(r, {Level: compress.BestSpeed})
compress.NewReader(r, compress.NewReaderOptions{Level: compress.BestSpeed}
This proposal conflicts with the Go idiomatic naming rules
@leafbebop argued that the current naming convention we use in Go would be hurt by this proposal since it relies on having type information readily available which could cease to be the case if this proposal was accepted.
Using elision with maps might create confusion
This concern is actually mine, even though others have discussed something very similar
When eliding the type of maps things might get a little bit confusing if we use variables as keys, and we elide the type, e.g.:
A := "a"
var m map[string]int
var x struct { A int }
// This looks the same
m = {A: 1}
// As this:
x = {A: 1}
Q&A:
This is a complex question since as shown by @neild in his comment this is already allowed as long as you use the full declaration of the struct instead of just the name, which is unexported, so not allowed, e.g.:
```golang package main
import "fmt"
type unexported struct{ X string }
func F(u unexported) { fmt.Println(u.X) }
func main() { F(struct{ X string }{"foo"}) // (this works even from another package) } ```
The general opinion regarding this matter seems to be that we should implement whichever option is easier.
@jimmyfrasche argued that since this is already possible in Golang:
golang
[]*T{
{Name: "foo"},
}
it would make sense to have:
golang
var t *T = {Name: "foo"}
Personally I would prefer to be explicit with the
&
operator, as does @randall77.
Other arguments
Restricting the scope so elision only works for anonymous structs
@DeedleFake and later @tooolbox suggested here and here that this proposal might be too broad and maybe we should restrict it so it only works on anonymous structs.
@bcmills and @dsnet disagree with this proposal since it will undermine most of the advantages and might make the feature more complicated.
Real code examples
- @jimmyfrasche https://github.com/golang/go/issues/12854#issuecomment-326839729
- @urandom https://github.com/golang/go/issues/12854#issuecomment-698173981
Comment From: VinGarcia
So, to be honest I wrote the review above because I wanted to add a comment in a way that would actually help us get to the solution.
So first I summarized what we currently have so that now I can discuss the point brought up by @tooolbox, which is that we might benefit from restricting the scope of this proposal.
Currently, we have a total of 6 use-cases, and below I am ordering them according to how useful I think each of them is (considering pros and cons in my mental experiments):
- When passing arguments to a function
- When creating nested structures
- When using channels
- When returning from a function
- When using maps
- When using slices
So one option that would probably help us have these features implemented faster would be to just create separate proposals for each of these use-cases.
I do expect that some of these use-cases might not be accepted, for example, I think that the benefit of having more type elision on maps and slices have marginal benefits and might actually, unnecessarily, decrease readability in several situations, e.g.:
A := "a"
var m map[string]int
var x struct { A int }
// ... several lines of code ...
// These 2 look the same for an uninformed reader:
m = {A: 1}
x = {A: 1}
On the other hand, I do believe that having type elision on function calls would be so useful in all the fronts that it actually has no cons IMHO, e.g.:
``golang
// With no type elision the signature of funcs with optional arguments tend to look like this:
err := posts.GetPosts(userID, nil, someBadVariableName) // what is the
nil` value?
// With type elision the creator of the getPosts
func would be
// encouraged write a different signature for the function:
err := posts.GetPosts(userID, {
Category: nil, // (keeping this attribute just for comparison, but this could be omitted)
CallbackFn: someBadVariableName,
})
```
So in this situation, we would have:
- A better function signature that won't force us to insert
nil
on optional arguments - No stutter, since I didn't have to write the struct name which would be something like
posts.GetPostsOptions
- And now we have more information for the reader, not less: we do know now that the second argument is the category, and that
someBadVariableName
is actually a callback. - We also have even more information regarding the decision of moving only some arguments to the struct: It's likely that the
userID
is mandatory and that the ones inside the struct are optional arguments.
Conclusion
So what I want to say is that I would prefer to have these use-cases separated so that I could defend the ones I find more useful and have them available sooner.
Comment From: neild
So one option that would probably help us have these features implemented faster would be to just create separate proposals for each of these use-cases.
I rather firmly feel that, as I described in the proposal at the top of this issue, the simplest and most consistent option--simpler and more consistent than the current language specification, even!--is to say that a composite literal with no type specified is assignable to any variable of array, slice, or map type, or pointer to same. (Note that under the nomenclature of the language specification, "assignable to a variable..." covers passing parameters to a function, sending values to a channel, etc.)
Enumerating the cases under which the type may be omitted is, in contrast, inherently inconsistent.
Comment From: quenbyako
@neild is to say that a composite literal with no type specified is assignable to any variable of array, slice, or map type, or pointer to same
Agree, but partially:
Imagine you have something like this:
var x map[string]interface{} = {
"value": {
A: 123,
B: "abc",
C: true,
},
}
Will this be the correct definition of the variable? If so, how does go know that the interface contains the struct SomeStruct
and not a VerySimilarStruct
? The main point here is not that you need to write code as simply as possible, but that you do not write the same information from time to time: when we define
var someMap map[string]interface{}
someMap = map[string]interface{}{"a": 1, "b": true}
then we overcomplicate life of typical go developer: the compiler can determine the type of the value that is assigned to this map without us, what is the point of writing the type of the map twice?
BUT. If we are talking about interface values in slices, maps, etc., like error
numeric
, then there is no reason to force the compiler to predict the value. just because the compiler is not a baba vanga, it does not predict your intentions.
Also important question: we can make pointer to interface like *error
(idk why, but we can). So, will var err *error = &{Text: "some text of error"}
f.e. will be correct? we defined pointer too.
Comment From: urandom2
Imagine you have something like this:
go var x map[string]interface{} = { "value": { A: 123, B: "abc", C: true, }, }
Based on @neild's summary:
a composite literal with no type specified is assignable to any variable of array, slice, or map type, or pointer to same.
Your example is not covered; also, there is no type that can be inferred for an interface, other than an untyped nil; so imo, your example is undefined and should not be allowed.
Comment From: VinGarcia
@quenbyako I think this has already been discussed, type inference is already made by the compiler when it is possible, we don't plan to change how it works we only want to relax the rules so we can have some benefits. But in your first example with a map, and in your last example with the error there is no way the compiler would know the dynamic type if you don't write it down yourself so it would just cause a compiler error.
@neild I understand your argument, I agree that it would be simpler to implement it this way, but I don't think this would increase the complexity of the language to anyone who is learning it, since this is just a syntax sugar I imagine people will learn about it in two ways:
- Observing the usage examples of other packages on its README files
- Guessing and making experiments to see if the compiler accepts that use-case
Both situations are orthogonal to the process of learning the language the first time, so I think that the criteria of whether this would improve of worsen the readability is more important and is actually what is going to define whether we can add this to the language or not. Meaning that we need to come up with strategies that will allow us to harness the maximum readability in most situations with the minimum loss of it in the remaining cases.
And if we are going to keep discussing in terms of all the use-cases or none, we won't be able to fine-tune our discussions to that degree.
Comment From: quenbyako
@arnottcr yeup, that is exactly what i wanted to say)
@VinGarcia just wanted to fresh up knowledge of this specific case 🙃
Comment From: VinGarcia
Uhm, I just noticed one problem we'll have to keep in mind if/when implementing this proposal. I opened the go2go playground where we can play with generics and the initial code made me realize that omitting the type on generic functions might be problematic, the very first example at https://go2goplay.golang.org/ illustrates the problem very well:
func Print[T any](s []T) {
for _, v := range s {
fmt.Print(v)
}
}
func main() {
Print([]string{"Hello, ", "playground\n"})
}
Here if we omitted the type in []string{"Hello, ", "playground\n"}
I think the compiler would have no way of knowing the type in the way it normally does. You may argue that it knows it's a []string
because it contains strings but this is not written anywhere and it could as easily be interpreted as []interface{}
.
Maybe the best solution for this would be to just throw a compile error saying it can't infer the type or something like that, otherwise we would be expanding the scope of this proposal.
Comment From: urandom
@VinGarcia
IIRC, the generics proposal only infers the parametric type when feasible. For all other cases, the user needs to specify it as
Print[[]string]({"Hello, ", "playground\n"})
Comment From: VinGarcia
Yeah, but I think that this would still complicate the implementation of this feature, because this is a new situation that didn't existed before, i.e. without this proposal, the compiler error could happen only if the generic type T is not in the function signature meaning that it can't infer the type T from the arguments, now even if the type T is in the function signature it will also need to check whether the type was omitted or not.
I don't know the actual compiler's code, so I am just guessing, but I think it is important to have this in mind.
Comment From: quenbyako
@urandom lol this looks REALLY ugly 😂
Comment From: urandom2
I would expect that if this and generics land, we could consider allowing this too:
Print([]string{"Hello, ", "playground\n"})
but recall that today, you have to:
Print[[]string]([]string{"Hello, ", "playground\n"})
Comment From: mdempsky
I would expect that if this and generics land, we could consider allowing this too:
go Print([]string{"Hello, ", "playground\n"})
I'm confused. That code already appears in the example code at go2goplay.golang.org.
Comment From: urandom2
We are drifting off topic, so sorry for contributing; but I thought that generic type elision was not an mvp feature, and it would be considered at a later date?
EDIT: looks like they reverted that stance:
Type inference permits omitting the type arguments of a function call in common cases.
We can still just follow the pattern from Rust of do one or the other, when the compiler cannot infer.
Comment From: mrg0lden
@VinGarcia
IIRC, the generics proposal only infers the parametric type when feasible. For all other cases, the user needs to specify it as
go Print[[]string]({"Hello, ", "playground\n"})
It should look like this instead
Print[string]({"Hello, ", "playground\n"})
because in the definition of Print
, the argument is []T
, in which T
is string
in this case.
BTW if this becomes allowed (which to my eyes looks nice and clean), I think one of the two styles should be preferred, either using go vet
or go fmt
, to keep Go code look consistent.
Comment From: lukesneeringer
Fundamentally, the arguments for and against this proposal are:
- We should not do it, because eliding the top level type of a composite literal can be confusing. (I do not believe anyone has argued that it is always confusing.)
- We should do it, because eliding the top level type of a composite literal can improve readabililty. (I do not believe anyone has argued that it always increases readability either.)
- The decisive argument for now: We aren't making significant language changes until generics are finished. (And possibly not even then, but definitely not right now.)
I do not see an objective argument for either points 1 or 2; it's a judgement call.
One objective argument is that (2) allows users to make the judgement (since the user can always decide not to elide the type), and allows different users to make different judgements depending on the situation, while (1) entails the Go team making the judgement universally on behalf of all users, all the time.
Comment From: VinGarcia
Now that Generics is available I was wondering whether it would be possible to continue this discussion. Not that I have anything to add, but it might be beneficial to have some feedback from the Go team about the feasibility of this feature and whether it is fine as is or if we might need to refine it a little bit more.
I see 4 possible paths moving forward:
- The proposal is not accepted (I hope not)
- The proposal could be fully accepted
- Problems with the current proposal could be pointed out so we can refine it
- The proposal could be broken into smaller proposals, given that type inference can happen in several different contexts and maybe some of these contexts might have more benefits than costs while others might have more costs than benefits (although I would not be against a full approval).
Comment From: jba
This would make slog's API nicer. (See #56345.) Instead of
slog.Info("msg", "a", "b", "c", "d")
which many people don't like because it's hard to tell what are keys and what are values, we could instead write
slog.Info("msg", {"a", "b"}, {"c", "d"})
which is marginally more verbose but much easier to read.
Comment From: majelbstoat
This would make slog's API nicer. (See #56345.) Instead of
slog.Info("msg", "a", "b", "c", "d")
which many people don't like because it's hard to tell what are keys and what are values, we could instead write
slog.Info("msg", {"a", "b"}, {"c", "d"})
which is marginally more verbose but much easier to read.
The same is potentially true of GRPC’s metadata.Pairs (which also panics at runtime if there’s an odd number of parameters).
Comment From: KevinJCross
I personally really like the fact that this could be a mechanism to enable named parameters to functions: This can be considered a counter point to @glasser being conscerned about anonomous structs as function arguments.
func namedParams(struct {
one int
two int
three int
length int
width int
}) {}
func SomeFunc() {
namedParams({one: 1, two: 2, three: 3, length: 4, width: 5})
}
This is a very simple way of adding this feature to go without having to add any more new syntax.
Edit: After some searching I've found that this proposal was suggested exactly for this use. https://github.com/golang/go/issues/12296
Comment From: igorcafe
I think that would be pretty useful when you are dealing with JSON APIs with lots of nested objects. Rewritting all the nested structs is completely unfeasible so I had to type each "substruct".
Comment From: KevinJCross
@rsc Please can reconsider why this is marked as V2? We have just added generics into v1 which is a syntactic change. This is much less of a syntactic change more a higher level of inference. Im also very sure this could be done in V1 with backward compatibility? Unless there is an actual date when v2 comes out I think we should try progress this further.
Comment From: zephyrtronium
@KevinJCross v2 is a label applied to all language change proposals to facilitate triage and searching. It doesn't have any bearing on whether the proposal can be accepted before Go 2.
Comment From: rprtr258
This would make slog's API nicer. (See #56345.) Instead of
slog.Info("msg", "a", "b", "c", "d")
which many people don't like because it's hard to tell what are keys and what are values, we could instead write
slog.Info("msg", {"a", "b"}, {"c", "d"})
which is marginally more verbose but much easier to read.
That is not the case for the proposal. slog.Info accepts (string, ...any)
, so no way to recognize the type of {"a", "b"}
.
And, even if we were to restrict slog.Info
to signature like (string, ...slog.Attr)
, slog.Attr
as of now is
struct {
Key string
Value slog.Value
}
So, if we elide {"a", "b"}
to slog.Attr
, "a"
becomes Key
and "b"
should become Value
. But slog.Value is somewhat complex struct with no public fields, so still no way to elide slog.Info
arguments.
Now, even if we suppose slog.Value
to be just any
type, {"a", "b"}
still could not be elided, since there are two possibilities:
- Value
is string("b")
, corresponding to slog.String("a", "b")
for {"a", "b"}
- Value
is any("b")
, corresponding to slog.Any("a", "b")
for {"a", "b"}
and {"a", "b"}
still could not be elided.
which is similar to argument made in https://github.com/golang/go/issues/12854#issuecomment-823219644
Comment From: natefinch
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: "foo@example.com",
}
}
}
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: "foo@example.com",
}
}
}
And certainly, the lack of User
after Leader is no less expressive than the lack of Group
after "devs"
. You don't need to see the Group
type to see that the Leader
has an email of "foo@exmple.com". If that's all you're writing anyway, adding User
is not really adding any information.
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.
@ianlancetaylor @rsc
Comment From: ydnar
It uses the anonymous structs as namespacing, with short, clear field names in the anonymous structs.
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
Comment From: bronger
@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 User
. I remember a couple of times when I was annoyed to be forced to spell out type information in similar situations, but it was my code and I knew it well.
Comment From: DeedleFake
@ydnar
It's not really what it was designed for, but some of the proposed solutions in #34515 could theoretically allow that, such as var db typeof(Config.DB)
.
Comment From: ydnar
It's not really what it was designed for, but some of the proposed solutions in #34515 could theoretically allow that, such as
var db typeof(Config.DB)
.
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.
Comment From: natefinch
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: "admin@example.com",
},
}
_ = GroupStruct{
Leader: User{
Email: "admin@example.com",
},
}
}
Comment From: mikeschinkel
@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... :raised_hands:
Comment From: humdogm
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 []int
s that I would have to write are visual clutter when they can be elided from the struct definition.
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 gopls
language server showing me the field name hints that clearly label each test case's test data and expected values.
Comment From: ngrilly
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.
Comment From: eihigh
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.
Comment From: eihigh
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?
Comment From: DeedleFake
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.
go m := map{keyStr: "value"} s := []{idxInt: "value"} // []string v := {value: "value"} // struct{value string}
Unless I'm misunderstanding something, that syntax overlap doesn't exist. There's no unquoted shortcut syntax for map keys currently.
Comment From: cespare
@DeedleFake it's not "unquoted keys", it's the fact that the key can be a variable: https://go.dev/play/p/TvEURZjvfHR
Comment From: neild
It's already the case that you can't tell what the key of a composite literal is at parse time.
f(T{
Key: "value", // Is Key a value, or a field name? We don't know until we know what T is.
})
Eliding T
from the composite literal doesn't add any additional parsing ambiguity. We already need to defer interpretation until typecheck.
Comment From: dtrckd
Hi, I'm sorry to jump into the conversation like a hairsbreadth, but having just discovered this language feature, I investigated to find out when and it which release it was introduced. But I am struggling to find out, and feel confused about the fact that this issue is still open while the type inference in composite literal seems to be implemented in go1.22.
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 : Within a composite literal of array, slice, or map type T, elements or map keys that are themselves composite literals may elide the respective literal type if it is identical to the element or key type of T. Similarly, elements or keys that are addresses of composite literals may elide the &T when the element or key type is *T.
Comment From: neild
This proposal is neither accepted nor implemented. The part of the specification you're referencing is what this proposal proposes to amend.
Comment From: igorcafe
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",
},
},
}
Comment From: antichris
instead of:
(code snippet elided)
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) `tests` gets defined with a stupid named helper type you have little to none other use. Which is not optimal.
@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 :+1: on the relevant prior posts. Be sure you haven't missed other ideas relevant to you in earlier posts! :wink:
Comment From: igorcafe
@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
Comment From: jimmyfrasche
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.)
Comment From: Galabar001
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:
go 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.
Comment From: sanan-go
I read the whole discussion, and I believe "returning a zero-valued struct" benefit of this proposal is not appreciated enough.
I have recognized tendency in many Go projects (and admittedly I did it myself) where developers tend to use *T
instead of T
as return type just because it is easier to return nil
compared to zero value. Example:
package search
type PagedResponse struct {
// ...
}
type Engine interface {
Search(query string) (*PagedResponse, error) // developers tend to prefer this
Search(query string) (PagedResponse, error) // rather than this
}
Solely because it is easier to write this:
package google
import "search"
type Google int
func (g *Google) Search(query string) (*search.PagedResponse, error) {
if query == "" {
return nil, ErrInvalid
}
// ...
}
instead of this:
package google
import "search"
type Google int
func (g *Google) Search(query string) (search.PagedResponse, error) {
if query == "" {
return search.PagedResponse{}, ErrInvalid
}
// ...
}
Under this proposal, being able to return {}, ErrInvalid
would eliminate the "artificial force" that is not relevant to the matter.
It would allow developers to choose between struct vs pointer to it based on relevant reasons (design, performance, etc.)
Considering that almost all go projects contain significantly more zero values than "filled" values in code (usually there are multiple error cases and a single happy case), concise syntax for zero values would be nice.