I am not quite sure what is it that you find problematic with either options. Seems fine to me.
Your Print function should really be: func Print[T any, V Test[T]](t V){...} Then you can type assert 't'? Print[int] and Print[string] will always be different since their type argument are different. Even if they were to end up not used in the body of the type definition (phantom types). You could use their zero values in the methods for instance. --- On the other hand, seems to me that you want some sort of Higher Kinded Typing, where a function can take a constrained type variable and return another constrained type variable. Here the type variable is constrained by the definition of Test. This is something that can impact the decidability type checking because of impredicativity. First, it is somewhat of a syntactic overloading. Instead of calling these elements 'types' or 'generic types', we should rather call them 'Shapes' for instance. That will already make clearer that shapes can't really be passed where 'types' are expected. So your Print function is not really a 'function' anymore as it takes as argument a shape aka generic type and returns another generic type that is constantly 'void'. Now that also means that it can't be passed where a 'function' is expected. Otherwise it would/could interact strangely with subtyping. Types might be shapes however (constant ones? wrt universal quantification?). This comes back to instantiation as Ian explained. An instantiated shape, is basically a type i.e. the result of the call of a higher-kinded function. That should allow some form of metaprogramming. Not sure how tractable it can be and whether it is really needed. As your example show, it goes back to being able to define generic methods on some types. This is slightly related indeed since it is partial instantiation of generic types. I say "Shape" because you want to avoid this: Typechecking is Undecidable when 'Type' is a Type <https://dspace.mit.edu/handle/1721.1/149683> for instance. But anyway it is akin to bolting a type system on top of a type system. Not necessarily that straightforward. On Thursday, August 7, 2025 at 7:46:13 PM UTC+2 John Wilkinson wrote: > I opened an issue to start a discussion around this, but perhaps it was > the wrong place, and it is better to begin with email- though formatting > seems quite a bit worse over email. > > https://github.com/golang/go/issues/74916 > > Here is the relevant contents of that issue: > > Preface > > 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. > Overview > > TLDR; 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. > Example > > Take 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 1 > > The programmer may "color" the print function (if you're unfamiliar, it's > the concept presented here > <https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/> > 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 2 > > Use 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. > 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") } } > > This would be backwards compatible since today it would simply result in a > compile error. > Preemptive objections and responses > > *The 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. > Conclusion > > This 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 > -- 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+unsubscr...@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/golang-nuts/02bd3c62-589e-4070-a4c0-6cfcf80dda93n%40googlegroups.com.