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 · 122 comments
Open

proposal: spec: type inferred composite literals #12854

neild opened this issue Oct 6, 2015 · 122 comments

Comments

@neild
Copy link
Contributor

@neild 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
Copy link
Contributor

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

Loading

@bcmills
Copy link
Member

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

Loading

@minux
Copy link
Member

@minux minux commented Oct 6, 2015

Loading

@neild
Copy link
Contributor Author

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

Loading

@neild
Copy link
Contributor Author

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

Loading

@jimmyfrasche
Copy link
Member

@jimmyfrasche 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})

Loading

@adg
Copy link
Contributor

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

Loading

@minux
Copy link
Member

@minux minux commented Oct 6, 2015

Loading

@neild
Copy link
Contributor Author

@neild neild commented Oct 6, 2015

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

Loading

@minux
Copy link
Member

@minux minux commented Oct 6, 2015

Loading

@bcmills
Copy link
Member

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

Loading

@minux
Copy link
Member

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

Loading

@neild
Copy link
Contributor Author

@neild neild commented Oct 6, 2015

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

Loading

@minux
Copy link
Member

@minux minux commented Oct 6, 2015

Loading

@neild
Copy link
Contributor Author

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

Loading

@minux
Copy link
Member

@minux minux commented Oct 6, 2015

Loading

@neild
Copy link
Contributor Author

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

Loading

@griesemer
Copy link
Contributor

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

Loading

@bcmills
Copy link
Member

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

Loading

@neild
Copy link
Contributor Author

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

Loading

@griesemer
Copy link
Contributor

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

Loading

@bcmills
Copy link
Member

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

Loading

@jimmyfrasche
Copy link
Member

@jimmyfrasche 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{...},
 }

Loading

@minux
Copy link
Member

@minux minux commented Oct 10, 2015

Loading

@ianlancetaylor
Copy link
Contributor

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

Loading

@codeblooded
Copy link

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

Loading

@bcmills
Copy link
Member

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

Loading

@bcmills
Copy link
Member

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

Loading

@codeblooded
Copy link

@codeblooded 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?

Loading

@tooolbox
Copy link

@tooolbox tooolbox commented Sep 22, 2020

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:

  1. It's an anonymous struct, not a named type.
  2. 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 #29036 and #29326).

Loading

@neild
Copy link
Contributor Author

@neild neild commented Sep 22, 2020

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:

  1. 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.)
  2. 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.)
  3. 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.

Loading

@urandom
Copy link

@urandom urandom commented Sep 23, 2020

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

Loading

@jimmyfrasche
Copy link
Member

@jimmyfrasche jimmyfrasche commented Sep 23, 2020

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

Loading

@urandom
Copy link

@urandom urandom commented Sep 24, 2020

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

Loading

@leafbebop
Copy link

@leafbebop leafbebop commented Sep 24, 2020

Loading

@urandom
Copy link

@urandom urandom commented Sep 24, 2020

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.

Loading

@leafbebop
Copy link

@leafbebop leafbebop commented Sep 24, 2020

Loading

@VinGarcia
Copy link

@VinGarcia VinGarcia commented Apr 13, 2021

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:

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:

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

Loading

@VinGarcia
Copy link

@VinGarcia VinGarcia commented Apr 16, 2021

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

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

  1. A better function signature that won't force us to insert nil on optional arguments
  2. No stutter, since I didn't have to write the struct name which would be something like posts.GetPostsOptions
  3. 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.
  4. 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.

Loading

@neild
Copy link
Contributor Author

@neild neild commented Apr 16, 2021

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.

Loading

@quenbyako
Copy link

@quenbyako quenbyako commented Apr 16, 2021

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

Loading

@arnottcr
Copy link
Contributor

@arnottcr arnottcr commented Apr 16, 2021

Imagine you have something like this:

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.

Loading

@VinGarcia
Copy link

@VinGarcia VinGarcia commented Apr 16, 2021

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

  1. Observing the usage examples of other packages on its README files
  2. 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.

Loading

@quenbyako
Copy link

@quenbyako quenbyako commented Apr 16, 2021

@arnottcr yeup, that is exactly what i wanted to say)

@VinGarcia just wanted to fresh up knowledge of this specific case 🙃

Loading

@VinGarcia
Copy link

@VinGarcia VinGarcia commented Apr 20, 2021

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.

Loading

@urandom
Copy link

@urandom urandom commented Apr 20, 2021

@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"})

Loading

@VinGarcia
Copy link

@VinGarcia VinGarcia commented Apr 20, 2021

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.

Loading

@quenbyako
Copy link

@quenbyako quenbyako commented Apr 22, 2021

@urandom lol this looks REALLY ugly 😂

Loading

@arnottcr
Copy link
Contributor

@arnottcr arnottcr commented Apr 22, 2021

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​"​})

Loading

@mdempsky
Copy link
Member

@mdempsky mdempsky commented Apr 22, 2021

I would expect that if this and generics land, we could consider allowing this too:

Print​([]string{​"Hello, "​, ​"playground​\n​"​})

I'm confused. That code already appears in the example code at go2goplay.golang.org.

Loading

@arnottcr
Copy link
Contributor

@arnottcr arnottcr commented Apr 22, 2021

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.

Loading

@mrg0lden
Copy link

@mrg0lden mrg0lden commented Apr 24, 2021

@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"})

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.

Loading

@lukesneeringer
Copy link

@lukesneeringer lukesneeringer commented May 6, 2021

Fundamentally, the arguments for and against this proposal are:

  1. 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.)
  2. 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.)
  3. 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.

Loading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet