Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

proposal: spec: type inferred composite literals #12854

Open
neild opened this issue Oct 6, 2015 · 81 comments

Comments

@neild
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Member

commented Oct 6, 2015

@neild

This comment has been minimized.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor

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.

Copy link
Member

commented Oct 6, 2015

@neild

This comment has been minimized.

Copy link
Contributor Author

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.

Copy link
Member

commented Oct 6, 2015

@bcmills

This comment has been minimized.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

commented Oct 6, 2015

@neild

This comment has been minimized.

Copy link
Contributor Author

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.

Copy link
Member

commented Oct 6, 2015

@neild

This comment has been minimized.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

commented Oct 10, 2015

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

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.

Copy link

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link

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?

@afanda0

This comment was marked as off-topic.

Copy link

commented Dec 29, 2018

this is great

@myitcv

This comment was marked as off-topic.

Copy link
Member

commented Dec 29, 2018

@clareyy please be aware of https://github.com/golang/go/wiki/NoPlusOne. Feel free to add responses using emoji reactions instead.

@afanda0

This comment was marked as off-topic.

Copy link

commented Dec 30, 2018

@clareyy please be aware of https://github.com/golang/go/wiki/NoPlusOne. Feel free to add responses using emoji reactions instead.

sorry ,my fault

@chowey

This comment has been minimized.

Copy link

commented Jan 4, 2019

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

@divan

This comment has been minimized.

Copy link

commented Jan 15, 2019

Thought experiment/experience report where this can be useful (not the deal breaker though):
https://divan.github.io/posts/flutter_go/

@benpate

This comment has been minimized.

Copy link

commented May 17, 2019

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.

@randall77

This comment has been minimized.

Copy link
Contributor

commented May 17, 2019

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

@jimmyfrasche

This comment has been minimized.

Copy link
Member

commented May 17, 2019

Since you can write

[]*T{
  {Name: "foo"},
}

it would follow that it would just be

t = {Name: "foo"}
@randall77

This comment has been minimized.

Copy link
Contributor

commented May 17, 2019

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 :(

@dmitshur dmitshur added the Proposal label Jun 5, 2019

@tj

This comment has been minimized.

Copy link

commented Jul 2, 2019

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.

@steebchen

This comment has been minimized.

Copy link

commented Aug 2, 2019

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.

@mvdan mvdan changed the title proposal: spec: untyped composite literals proposal: spec: type inferred composite literals Aug 28, 2019

@mvdan

This comment has been minimized.

Copy link
Member

commented Aug 28, 2019

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.

@DeedleFake

This comment has been minimized.

Copy link

commented Aug 28, 2019

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.

@tj

This comment has been minimized.

Copy link

commented Aug 28, 2019

@DeedleFake maybe in the docs, but in auto-complete output it's usually not super clear

@DeedleFake

This comment has been minimized.

Copy link

commented Aug 28, 2019

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?

@bcmills

This comment has been minimized.

Copy link
Member

commented Aug 28, 2019

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

@stamblerre

This comment has been minimized.

Copy link
Contributor

commented Aug 29, 2019

Yes, gopls always uses type information for autocompletion, and it ranks results that match the expected type much higher than other results.

@turtleDev

This comment has been minimized.

Copy link

commented Aug 30, 2019

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.

@tj

This comment has been minimized.

Copy link

commented Aug 30, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.