golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
123.97k stars 17.67k forks source link

proposal: spec: type inferred composite literals #12854

Open neild opened 9 years ago

neild commented 9 years ago

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
adg commented 9 years ago

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.

bcmills commented 9 years ago

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.)

minux commented 9 years ago

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.

neild commented 9 years ago

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. :)

neild commented 9 years ago

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.

jimmyfrasche commented 9 years ago

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})
adg commented 9 years ago

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.

minux commented 9 years ago

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.

neild commented 9 years ago

To be clear, this proposal does not allow {A: 1} to implicitly become map[string]int{"A":1}.

minux commented 9 years ago

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}?

bcmills commented 9 years ago

In that last example, A is an identifier (i.e. for a string variable or constant) - not a string literal itself.

minux commented 9 years ago

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?)

neild commented 9 years ago

x = {A: 1} is precisely equivalent to x = T{A: 1}, where T is the type of x.

minux commented 9 years ago

But we currently don't accept var m = map[string]int{A: 1}

neild commented 9 years ago

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)
minux commented 9 years ago

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.

neild commented 9 years ago

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.

griesemer commented 9 years ago

@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).

bcmills commented 9 years ago

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.

neild commented 9 years ago

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.

griesemer commented 9 years ago

@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.

bcmills commented 9 years ago

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.)

jimmyfrasche commented 9 years ago

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{...},
 }
minux commented 9 years ago

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.)

ianlancetaylor commented 9 years ago

@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.)

codeblooded commented 9 years ago

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.

bcmills commented 9 years ago
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.)

bcmills commented 9 years ago

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.
codeblooded commented 9 years ago

@bcmills Ok… what would be the overhead on the compiler side of inferring the types and aborting if the type is ambiguous?

mdempsky commented 9 years ago

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.

dsnet commented 9 years ago

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.

codeblooded commented 9 years ago

Ok… makes sense. So is this a Go or a No? other thoughts

extemporalgenome commented 9 years ago

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.

dsnet commented 9 years ago

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.

mdempsky commented 9 years ago

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.

extemporalgenome commented 9 years ago

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.

rsc commented 9 years ago

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".

glasser commented 8 years ago

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.

Perelandric commented 8 years ago

@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.

extemporalgenome commented 8 years ago

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?

jimmyfrasche commented 8 years ago

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"})
minux commented 8 years ago

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.

neild commented 8 years ago

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

bcmills commented 8 years ago

@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})
}
bcmills commented 8 years ago

@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.

neild commented 8 years ago

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.

neild commented 8 years ago

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. :)

bcmills commented 8 years ago

@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.)

neild commented 8 years ago

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.

Splizard commented 8 years ago

Would be good to implement this for constants at least. eg {"a", "b", "c"} => []string{"a", "b", "c"}