New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: spec: untyped composite literals #12854

Open
neild opened this Issue Oct 6, 2015 · 62 comments

Comments

Projects
None yet
@neild
Contributor

neild commented Oct 6, 2015

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 adg added the Proposal label Oct 6, 2015

@adg

This comment has been minimized.

Contributor

adg commented Oct 6, 2015

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

This comment has been minimized.

Member

bcmills commented Oct 6, 2015

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

This comment has been minimized.

Member

minux commented Oct 6, 2015

@neild

This comment has been minimized.

Contributor

neild commented Oct 6, 2015

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

This comment has been minimized.

Contributor

neild commented Oct 6, 2015

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

This comment has been minimized.

Member

jimmyfrasche commented Oct 6, 2015

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

This comment has been minimized.

Contributor

adg commented Oct 6, 2015

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
#12854 (comment).

@minux

This comment has been minimized.

Member

minux commented Oct 6, 2015

@neild

This comment has been minimized.

Contributor

neild commented Oct 6, 2015

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

@minux

This comment has been minimized.

Member

minux commented Oct 6, 2015

@bcmills

This comment has been minimized.

Member

bcmills commented Oct 6, 2015

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

@minux

This comment has been minimized.

Member

minux commented Oct 6, 2015

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

This comment has been minimized.

Contributor

neild commented Oct 6, 2015

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

@minux

This comment has been minimized.

Member

minux commented Oct 6, 2015

@neild

This comment has been minimized.

Contributor

neild commented Oct 6, 2015

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

This comment has been minimized.

Member

minux commented Oct 6, 2015

@neild

This comment has been minimized.

Contributor

neild commented Oct 6, 2015

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

This comment has been minimized.

Contributor

griesemer commented Oct 9, 2015

@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

This comment has been minimized.

Member

bcmills commented Oct 9, 2015

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

This comment has been minimized.

Contributor

neild commented Oct 9, 2015

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

This comment has been minimized.

Contributor

griesemer commented Oct 9, 2015

@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

This comment has been minimized.

Member

bcmills commented Oct 9, 2015

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

This comment has been minimized.

Member

jimmyfrasche commented Oct 10, 2015

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

This comment has been minimized.

Member

minux commented Oct 10, 2015

@ianlancetaylor

This comment has been minimized.

Contributor

ianlancetaylor commented Oct 10, 2015

@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

This comment has been minimized.

codeblooded commented Oct 14, 2015

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

This comment has been minimized.

Member

bcmills commented Oct 14, 2015

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

This comment has been minimized.

Member

bcmills commented Oct 14, 2015

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

This comment has been minimized.

codeblooded commented Oct 14, 2015

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

@alercah

This comment has been minimized.

alercah commented May 22, 2017

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.

@jimmyfrasche

This comment has been minimized.

Member

jimmyfrasche commented Sep 3, 2017

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.

@bcmills

This comment has been minimized.

Member

bcmills commented Mar 3, 2018

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 #24204 (comment).

Instead of

opts := append(append([]grpc.CallOptions{}, defaultOptions...), extraOptions...)

we could write:

opts := append(append({}, defaultOptions...), extraOptions...)

or, with #18605:

opts := append({}, defaultOptions..., extraOptions...)
@leafbebop

This comment has been minimized.

leafbebop commented Aug 7, 2018

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment