Repository navigation
proposal: spec: extended type inference for make and new #34515
Description
Rationale
Currently in Go, type inference works in one direction and one direction only: From the values of an assignment to the variable declarations. In other words, given some expression with a statically-determinable type, which is basically every expression, the type of a declaration can be omitted. This allows for a lot of the perceived lightness of Go over many statically typed languages.
However, there are a number of situations where this is not possible, most of which have to do with make() and new(), both of which are unique, not including some stuff in the unsafe package, in that they take a type name as an argument. Normally this is a non-issue, as that type can be used to determine the return of the expression, thus allowing for inference in the normal manner:
m := make(map[string]interface{})
s := make([]string, 0, 10)
p := new(int)Sometimes the variable must have a separately declared type, though, such as in the case of struct fields:
type Example struct {
m map[string]interface{}
}
func NewExample() *Example {
return &Example{
m: make(map[string]interface{}),
}
}This leads to unwanted repetition of the type name, making later alteration more awkward. In particular, I thought of this while reading through one of the many proposals about ways to make anonymous structs more useful with channels, and I realized that the following pattern could get very annoying, exacerbating the existing issue:
type Example struct {
c chan struct {
v string
r chan<- int
}
}
func NewExample() *Example {
return &Example{
c: make(chan struct {
v string
r chan<- int
},
}
}Proposal
I propose the introduction of a new keyword, say typeof, which takes a variable, function name, or a struct field identifier and essentially acts as a stand-in for a standard type name, possibly with the restriction of only being available as the argument to a make() or new() call. For example,
return &Example{
m: make(typeof Example.m),
}This would allow a type name to be pegged to an already declared variable type elsewhere.
Alternative
Alternatively, make() and new() could allow for the aforementioned types of identifiers directly, such as
return &Example{
m: make(Example.m),
}This has the advantage of being backwards compatible, but is potentially less flexible if one wants to extend the functionality elsewhere later, such as to generics.
Activity
ianlancetaylor commentedon Sep 25, 2019
If we are going to introduce
typeof, I can't think of any reason why we would restrict it to being used withmakeandnew. That would not be very orthogonal.dmkra commentedon Sep 25, 2019
Type of
minExampleis already known. SotypeoforExample.mcan be omitted:return &Example { m: make() }DeedleFake commentedon Sep 25, 2019
That's backwards compatible, too, that way, although it brings with it an implicit assumption that
typename.fieldnamehas a general meaning.Edit: The comment this was a response to seems to have been deleted. Not entirely sure what happened there. It had a suggestion to use
Example.m.(type)instead oftypeof Example.m.bradfitz commentedon Oct 22, 2019
That's what I want myself. @griesemer points out that you'd also want to do
new()then.griesemer commentedon Oct 22, 2019
Regarding
typeof: Instead of a new keyword,typeofcould easily be a built-in. The possible "danger" of atypeofoperator is that people might start using it all over the place, for all kinds of declarations, thereby reducing the readability of the code.There's clearly a strong sentiment for being able to use
make()ornew()and have both of them infer the necessary type. Though we don't have anything else in the language that behaves like this at the moment. Is there a better syntax?bcmills commentedon Oct 23, 2019
Building on @dmkra's observation, we could use
_as a stand-in for “infer the obvious type”.bcmills commentedon Oct 23, 2019
That said, I think #21496 is a better fit for the
mapexample:And I suspect that #28366 would address most of the realistic use-cases for the
chanexample:bcmills commentedon Oct 23, 2019
@DeedleFake, there is some precedent for
.(type)in earlier proposals too.(For example, I used
(type)as the “type, not value” indicator in https://github.com/golang/proposal/blob/master/design/15292/2016-09-compile-time-functions.md.)DeedleFake commentedon Oct 23, 2019
I like the
_for inference, but it feels a bit like it overlaps with_as a discard assignment. This would allow it to be used on the right-hand side, technically, though only in the unusual case ofmake()andnew()which take actual type names as arguments.Maybe
_.(type)? Maybe not.bradfitz commentedon Oct 23, 2019
Using
.(type)for both dynamic type checks (as today with type switches) and static types would slightly be weird.bcmills commentedon Oct 23, 2019
@bradfitz, at least there is precedent for that! The
cases within aswitchstatement can already be either dynamic values or static types, depending on whether you're switching onxorx.(type).8 remaining items
beoran commentedon Nov 14, 2019
I think
typeof()as a built in function has the best potential here, and would be the most backwards compatible solution, and useful in several other cases, such as in low level programming, in conjunction withunsafe.Sizeof,unsafe.Alignofandunsafe.Offsetof.If there is any risk that
typeof()will end up being overused, then this can be solved through go lint checking. Personally I think the risk for abuse is low, since if you dotypeof(x), thenxwill already be in scope, and the type ofxshould be relatively clear to the reader.ianlancetaylor commentedon Nov 26, 2019
Another idea would be to permit referring to the basic kind of type without filling out the elements. For example,
This might make the code clearer to the reader, while still permitting the omission of the redundant type information.
dotaheor commentedon Nov 27, 2019
The
new()andmake(length, capacity)form looks consistent with this generic proposal.griesemer commentedon Apr 8, 2020
Just a few observations:
newandmakeare special because they do expect a type rather than a value as their first argument. If we permit leaving those types away in calls, we lose the visual cue that these calls are indeed calling the built-in functionsnewandmake, and not some user-defined regular functions. (We don't have that cue either if the type argument is a type name rather than a type literal, but that seems rare.) It may be beneficial for readability to retain some sort of visual signal. On the other hand, we already permit the omission of types in certain nested composite literals, and thus leaving away the type inmakeandnewcalls has some (remote) precedent, especially because often these calls do appear when constructing composite literals. Also, overwrittennewandmakeidentifiers are rare.typeofbuilt-in function can't be used in situations where the variable is both declared and initialized as follows:var v []T = make(typeof(v), n)because the newly declared variable won't be in scope until after the variable declaration (which includes the initialization expression). Of course, in these cases one could just writevar v = make([]T, n). But it's not clear that adding a new built-in is making this problem simpler - e.g., it doesn't help much if writing the type is shorter than writing the built-in call.[],mapandchan, which is simple and provides information about the kind of type that's being initialized. The problem with this approach though is that none of[],mapandchanare currently syntactically valid type expressions. We'd have to allow them in general, or at the very least as first arguments to calls of functions calledmake(the parser cannot know that it is parsing a call to the built-inmakefunction, we need type information for that). Probably doable, but requiring much more work than one might anticipate at first (parser and AST will have to be adjusted)....to denote the inferred type would be comparatively easy. We already accept...as expression in array declarations, so no parser or AST changes are needed. Doing type-inference for...in the type-checker would be relatively straight-forward.Of these four approaches, it seems that 1) (just leave the type away) and 4) (write
...instead of the type) are the most promising ideas so far.As nice as
...might seem, it does open a bit of a pandora's box (where else should we allow...?). It is also not strictly necessary (in contrast to the use of...in array and parameter declarations or when unpacking a slice).Thus, notwithstanding some more general concept for
...use, it seems to me that leaving away the type altogether might be the most pragmatic and Go-like approach, should we decide to proceed with this. It does solve the problem, it would be relatively straight-forward to implement, and it would make the code shorter. If readability is problematic, one can always write the type as we do now. Andnewandmakeare already very special because they accept a type argument, so this simple form of type inference wouldn't make them much more special.jimmyfrasche commentedon Apr 8, 2020
Even if all other proposals using
...are accepted, I think that omitting the type is a better fit. This situation is more analogous to the type inference in the generics draft than it is to any of the other...proposals. Even if that particular draft doesn't survive, the type inference for function calls likely would. I wouldn't want to have to typef(...)(x, y, z)for every generic function call.jimmyfrasche commentedon Apr 8, 2020
There's also a tangential connection to #12854 in that the contexts in which the type could be deduced are the same for both proposals.
DeedleFake commentedon May 17, 2020
@griesemer
This proposal was specifically not intended for those cases, as those cases already don't require writing the type twice. This proposal was intended to help only in the cases where the declaration and assignment have to be separate, such as in struct fields. I thought that a way to tell the compiler 'use whatever type this variable was declared as here' would allow for a general solution to it, as the problem stems from the type inference currently only being possible do from value to variable and not the other way around.
DeedleFake commentedon Feb 11, 2021
It was pointed out in episode #166 of Go Time that this type of inference would not help in situations where the user was initializing using a composite literal, such as
That's true. As I said in the original proposal, my primary motivation was channel initialization, though I did think of empty map initialization, too. Slices would be less useful, as appending to a nil slice works just fine. I do still think this would be worth it just for the channel case and the empty map case, though, and, if implemented, it definitely should work for slices too just to be consistent.
DeedleFake commentedon Sep 8, 2022
I've found a workaround for some situations when using generics. Although the current generics implementation doesn't have inference for return values, it can do it for pointers:
I don't know if this has any problematic effects on, for example, optimization, but I recently had a very large number of initialization that I needed to do and each one had a repeated type from a struct definition, so I wrote a function like this and used it instead and it massively cleaned up the code.
[-]proposal: Go 2: extended type inference for make and new[/-][+]proposal: spec: extended type inference for make and new[/+]