This topic has almost certainly been covered elsewhere; if that is the case, please direct me to the appropriate discussion and I'll read through it. I did try looking for something that covers this explicitly, and while I found somewhat similar discussions (such as those around creating discriminated unions), I didn't find this exact one.
OverviewTLDR; Disallowing the use of uninstantiated types as function parameters reduces clarity, is surprising, and leads to bugs. Allowing them improves communication within the codebase.
Definition of instantiation, from https://go.dev/blog/intro-generics#type-parameters
Providing the type argument to GMin, in this case int, is called instantiation. Instantiation happens in two steps. First, the compiler substitutes all type arguments for their respective type parameters throughout the generic function or type. Second, the compiler verifies that each type argument satisfies the respective constraint.
This process is required before a type may be used in a function definition. This restricts the ability of the programmer to communicate effectively the behaviors of their functions, and makes the use of generics as function argument types an all-or-nothing proposition: either the function must be "concrete"- that is, instantiated- or any must be used.
ExampleTake the following example:
package main import "fmt" type Test[T any] interface { Get() T } type TestImpl[T any] struct { value T } func (t *TestImpl[T]) Get() T { return t.value } // This is the problem func Print(t Test) { switch t := t.(type) { case Test[string]: fmt.Println(t.Get()) default: fmt.Println("not a supported type") } } func main() { Print(&TestImpl[string]{value: "test"}) }This will not compile, resulting in cmd/types/main.go:17:14: cannot use generic type Test[T any] without instantiation.
Instead, the programmer is left with 2 options:
Option 1The programmer may "color" the print function (if you're unfamiliar, it's the concept presented here but applied more generally).
func Print[T](t Test[T]) { // .... }This doesn't really "solve" anything, it means the caller is now faced with the same problem. Worse, if the Print function is attached to a struct, the struct must also be genericized, and the lack of union types means that it can't necessarily be done properly.
Consider a struct that can print Test things:
type Printer[T] struct {} func (p *Printer[T]) Print(t Test[T]) { fmt.Println(t.Get()) } func main() { stringPrinter := Printer[string]{} stringPrinter.Print(&TestImpl[string]{value: "test"}) intPrinter := Printer[int]{} intPrinter.Print(&TestImpl[int]{value: 1}) }Suddenly we need to create a new printer for every type. If the purpose of printer is to print Test things, this approach does not work.
Option 2Use any as the type parameter.
func Print(t any) { switch t := t.(type) { case Test[string]: fmt.Println(t.Get()) default: fmt.Println("not a supported type") } } func main() { Print(&TestImpl[string]{value: "test"}) }This is the common approach. The issues is that the any type communicates nothing, and provides no compiler guarantees. The programmer cannot communicate to the caller the expectations of the function, or even the approximate expectations.
ProposalIf the compiler allowed the use of uninstantiated types as function parameters, the programmer could better communicate the expectations of the function to the caller.
func Print(t Test) { switch t := t.(type) { case Test[string]: fmt.Println(t.Get()) default: fmt.Println("not a supported type") } }This would be backwards compatible since today it would simply result in a compile error.
Preemptive objections and responsesThe function parameter still contains no concrete type information, so it is not usable by the function code.
This is true; the programmer would need to use a type assertion to get the instantiated type. In that way, it behaves like any does today.
However, the compiler is still more effectively able to bound the inputs.
For example, the compiler could error like it does with other impossible type assertions:
type I interface { Get() string } // This is a compiler error in Go func Coerce(t I) { t2 := t.(string) // impossible type assertion: t.(string) // string does not implement I (missing method Get) } type Test[T any] interface { Get() T } // This could be a compiler error func Coerce2(t Test) { t2 := t.(string) // impossible type assertion: t.(string) // string is not of type Test }This wouldn't work on structs, since type assertions are only allowed on interfaces
Even if the compiler only allowed uninstantiated types for interface arguments, this would still be better than just using any today.
It is also not clear to me that this definitely wouldn't be possible for structs. In a sense, an uninstantiated generic struct is like an interface, so there might be a reasonable way to implement it, something like creating implicit interfaces for generic structs. Certainly, from a language perspective, it seems like it would be reasonable to allow uninstantiated struct types.
The programmer still needs to type assert within the function, and it may be non-exhaustive
This is no different from using any today, but still communicates more to the caller and allows additional compiler checks.
The ability to communicate more precisely add clarity with no additional mental cost and is a good thing.
ConclusionThis seems to me like a useful, backwards compatible change.
Because it seems so useful, and because the Go team tends to think very carefully about the language, I think I'm probably missing something.
So I would appreciate feedback on this proposal, including any previous proposals
that I may have missed and of course issues with doing this.
-----
Thanks,
John Mark Wilkinson
On Thu, Aug 7, 2025 at 10:46 AM John Wilkinson <john.wi...@rocky.edu> wrote:
>
> Proposal
>
> If the compiler allowed the use of uninstantiated types as function parameters, the programmer could better communicate the expectations of the function to the caller.
>
> func Print(t Test) { switch t := t.(type) { case Test[string]: fmt.Println(t.Get()) default: fmt.Println("not a supported type") } }
In effect, in Go terms, the type of t would be an interface type that
implements Test[T] for an arbitrary T.
Let's suppose I write
type Slice[T] []T
Can I then write
func Pop(s Slice) Slice { return s[1:] }
Going back to the original example, can I write
func Print(t Test) {
if g, ok := t.(interface { Get[string] }; ok {
fmt.Println(g.Get())
}
}
? That is, am I truly restricted to a type assertion to Test[T] for
some T, or can I rely on other properties of Test[T]?
In general, Go prefers that people write executable code, rather than
designing types. This is different from other languages, which often
suggest that you first design your types, then write your code. In Go
interface types can emerge from code, rather than the other way
around.
This proposal doesn't make the language more powerful. As you say, you
can already do everything using the type "any". The only goal here
seems to be to use types to better communicate what a function can
accept. As such this seems like a step toward writing types rather
than writing code. It doesn't seem like a good fit for Go.
Ian
--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/golang-nuts/CAOyqgcUPP3ENx9UDbd-Vbe6ypefsp1C0itGeU6d8M%3DE9jACa2g%40mail.gmail.com.
> However, I believe in general this would be infeasible to implement, because of type-assertions. That is, this couldn't work:> type Func[A any] func() A
> func F() int { return 42 }
> func G() {
> var x any = Func[int](F) // Stores as dynamic type Func[int], with underlying type func() int
> f := x.(Func[int]) // needs to effectively evaluate to func() int
> v := f() // would have to be int(42)
> g := x.(Func) // needs to effectively evaluate to func() any
> w := f() // would have to be any(int(42))
> }I feel a little lost here. I'm assuming that the line `w := f()` is intended to be `w := g()` but we're in the weeds enough that I want to double check that assumption.
> For any generic type declaration type X[T any] TypeLiteral`, `X` is an interface type implemented by all instantiations of `X`I'm going to refer to the above as "implied interfaces".Once we type assert to Func, we don't actually have a callable function, because an interface representing a function with type parameters would have no overlap within its implementations.I think the example code implies that doing "x.(Func)" is equivalent to doing "x.(Func[any])", but I don't believe that is allowed given the definition.
Effectively, using an implied interface for a function is not doable. Probably we could just prevent the type assertion in the first place, and give a useful compiler error.> However, there are still some problems: for example, field-selectors need to know the offset. These are not known, if the struct has type-parameter fields.Could you expand on this? I don't believe I follow. Field selection on interfaces is already not allowed, so I believe I'm misunderstanding this concern.
> This means, we would allow some intuitively reasonable operations, but not others.I think clarification on the previous point will let me better understand which operations would be restricted, but... I also think its quite likely that some seemingly-reasonable operations would be restricted, and I think that is Go generics in a nutshell.
I think we would need some sort of union types to perfectly express what would and would not be allowed.
To view this discussion visit https://groups.google.com/d/msgid/golang-nuts/056e3884-380c-4d6e-9b64-8a9f353ab5c1n%40googlegroups.com.