Allowing uninstantiated generic types as function parameters

343 views
Skip to first unread message

John Wilkinson

unread,
Aug 7, 2025, 1:46:13 PMAug 7
to golang-nuts
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.


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

Ian Lance Taylor

unread,
Aug 7, 2025, 2:05:09 PMAug 7
to John Wilkinson, golang-nuts
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

John Wilkinson

unread,
Aug 7, 2025, 3:25:03 PMAug 7
to golang-nuts
I think the two examples you give would be great. It would be really nice to be able to rely on known properties of a given Test[T].

But, I don't feel like I understand the implications of allowing that. I think understand those implications would be useful, and a good discussion to have. I'm certainly up for that.

On the other hand, I don't see any negative implications for allowing "Slice" or "Test" if they then must be type asserted before they can be used.

I like the code-first, functionality-first approach of Go. But I also think clarity is important, and I think Go values clarity very highly. 
Very practically, this proposal is just pointing out that "interface{} says nothing", and that's not great, and we could do better in this circumstance.

And I realize that I'm not in as good a position as you to comment on what Go is about, but my experience has been that Go eschews "power" in favor of clarity.


> The only goal here seems to be to use types to better communicate what a function can accept

Isn't that an improvement? That feels aligned with Go's objectives. 
The idea that is a step towards writing types rather than code is impossible to quantify; the same could be said of type aliases or generics.
I think the idea that writing types is a priori bad is wrong. Writing types in the service of nothing useful is bad, but writing types to clarify proper usage is really helpful.

Although I agree (I think- assuming you are more in favor of this) that making it so "t would be an interface type that implements Test[T] for an arbitrary T" would be preferable.
---- 

-JM

Ian Lance Taylor

unread,
Aug 7, 2025, 4:51:59 PMAug 7
to John Wilkinson, golang-nuts
On Thu, Aug 7, 2025 at 12:25 PM John Wilkinson <john.wi...@rocky.edu> wrote:
>
> > The only goal here seems to be to use types to better communicate what a function can accept
>
> Isn't that an improvement? That feels aligned with Go's objectives.

Among Go's objectives is that the language be simple and easy to learn
and understand. The current type system is very simple. A change like
this makes it more complicated, leading to questions like the ones I
posed earlier: "it seems logical that this should work, so why doesn't
it work?" But if we do make it work, the type system and the language
become more complicated.

> The idea that is a step towards writing types rather than code is impossible to quantify; the same could be said of type aliases or generics.

I don't agree. Both type aliases and generics were driven by a lack of
power in the language: "there is code that we can't write today, and
adding these features will make it possible to write that code". I
don't see a similar argument for your proposal.

Every language change is a cost/benefit decision. All language
proposals have benefits; if they didn't have benefits, nobody would
propose them. All language proposals have costs. Are the costs worth
the benefits?

Ian

John Wilkinson

unread,
Aug 7, 2025, 8:01:57 PMAug 7
to golang-nuts
> A change like this makes it more complicated, leading to questions like the ones I posed earlier: "it seems logical that this should work, so why doesn't it work?"

Isn't this the situation today? It seems logical I could use the uninstantiated type in a function definition (and for the compiler to restrict calls to just to implementers of the type), but it doesn't work.
It is not obvious to me that the language becomes more complicated. The current approach (passing "any") would remain possible, but it would be clearer to be more specific and use the actual required type, so that would likely be preferred.

I appreciate your explanation of lack of power in the language re type aliases and generics.
From a completely literal standpoint, this proposal qualifies: there is code I cannot write today, and adding this feature would make it possible.
But I understand that's not the point, the point is not about the syntax, rather it's about behaviors.

I pretty strongly believe that anything that improves the ability of the programmer to be more clear about the nature of the program and their intentions is definite win. I think there's a lot of cost to the lack of clarity in programs, and I think if we can improve that we should. But I also think I'm just repeating myself on that point, I've said as much in my earlier comments.

Given that, let's think about the idea that "type of t would be an interface type that implements Test[T] for an arbitrary T".
This makes sense to me: I should be able to write your slice example. If I have a function "func Pop(s Slice) Slice { return s[1:] }".
The fact that I can't feels surprising; although I've gotten used to it and I know I can't do that, if I were newly introduced to Go and ran across this I would find it confusing.

A big part of being easy to learn and understand is aligning with user expectations. 
Obviously, this can't be done perfectly 100% of the time, but in general use builds a mental model of a thing, and then deviations of that mental model are a large part of making something hard to learn and difficult to understand.
My mental model of a slice starts out as, "I have a slice and I can get elements from it."
My mental model of type definitions says "Anything I can do with the underlying, I can do with my  defined type)". 
I can get the "i" element of a slice, therefore, I should be able to get the "i" element of my type.
But I can't, because it's generic and uninstantiated and I cannot even pass the type into the function.
This is surprising.

So there's a fairly obvious benefit: the language more closely aligns with expectations, and is thus easier to learn and understand. (And it is easier to communicate intent)
The compiler can catch some errors that it couldn't before.
I now have access to the properties of T within my function without needing to specify a precise type

The cost is more complexity in the compiler. I don't think there would be a significant performance hit to compilation time, but I have no data to back that up.

The type system and language become more complicated in a literal sense, in that they have more components that can interact.
Like earlier, I think the literal sense misses the point; from a user perspective they are less complicated, because their behavior better aligns with expectations.

-JM

P.S. Appreciate the conversation. I really do feel I need to think more about the implications of introducing the rule that generic types would be interface types implementing T for all of T.



Ian Lance Taylor

unread,
Aug 8, 2025, 5:36:41 PMAug 8
to John Wilkinson, golang-nuts
On Thu, Aug 7, 2025 at 5:02 PM John Wilkinson <john.wi...@rocky.edu> wrote:
>
> > A change like this makes it more complicated, leading to questions like the ones I posed earlier: "it seems logical that this should work, so why doesn't it work?"
>
> Isn't this the situation today? It seems logical I could use the uninstantiated type in a function definition (and for the compiler to restrict calls to just to implementers of the type), but it doesn't work.
> It is not obvious to me that the language becomes more complicated. The current approach (passing "any") would remain possible, but it would be clearer to be more specific and use the actual required type, so that would likely be preferred.

Perhaps, but personally I don't see it. A generic type without type
arguments is an abstract type that doesn't, in fact by definition
can't, exist at run time. I don't think many people expect to be able
to use an abstract type.


> I appreciate your explanation of lack of power in the language re type aliases and generics.
> From a completely literal standpoint, this proposal qualifies: there is code I cannot write today, and adding this feature would make it possible.

I don't think that's correct, except in a pedantic sense. You can
write the same code today using the "any" type. This new feature adds
additional compiler type checking. It prevents you from writing
certain kinds of code, by detecting that that code is erroneous. It
doesn't permit you to write anything that you can't write today.


> A big part of being easy to learn and understand is aligning with user expectations.
> Obviously, this can't be done perfectly 100% of the time, but in general use builds a mental model of a thing, and then deviations of that mental model are a large part of making something hard to learn and difficult to understand.
> My mental model of a slice starts out as, "I have a slice and I can get elements from it."
> My mental model of type definitions says "Anything I can do with the underlying, I can do with my defined type)".
> I can get the "i" element of a slice, therefore, I should be able to get the "i" element of my type.
> But I can't, because it's generic and uninstantiated and I cannot even pass the type into the function.
> This is surprising.

I would be curious to know whether anybody else finds it surprising
(and posting here on golang-nuts is a good way to ask). I have not
encountered this myself.


> The cost is more complexity in the compiler. I don't think there would be a significant performance hit to compilation time, but I have no data to back that up.

To be clear, compiler complexity is a minor cost here. The cost is
complexity in the language. We need to define a new kind of abstract
type, define what operations are permitted on it, and so forth.

Ian

Kevin Chowski

unread,
Aug 9, 2025, 3:14:18 PMAug 9
to golang-nuts
I'm intentionally not touching on Ian's replies because I don't really have the background knowledge here to add anything in that direction, but I do have something to add that might help unblock you today with a compromise option.

I have an Option 3 which has served me well for situations like this. Remember that type parameters and interfaces are (at least, theoretically) orthogonal; you should use both in this case! It simply turns it into the other standing question of "Why doesn't Go have sum types?" or "How do I create a sealed interface with no external implementations?" Basically, for a generic `Test[T]` type, create a `AnyTest` type that has a hidden method that only the real Test type implements. e.g. https://play.golang.com/p/eUDLalvEpbg . It can be especially powerful when you don't even need to type-assert to Test explicitly - e.g. if you have some useful methods on type Test then you actually can just type-assert to some interface type with the method you want, rather than needing to go all the way back to a concrete struct type to access the data in a type-safe way.

Note that you can still bypass this by embedding an arbitrary Test[whatever] in a struct, since that will promote the unexported `isTest` method; you can get around this using some tricks that have runtime cost (https://play.golang.com/p/YT72Eib97vB), though I'd be interested to hear from anyone on the list who knows of a compile-time way to prevent this issue.

Mike Schinkel

unread,
Aug 9, 2025, 6:25:37 PMAug 9
to Ian Lance Taylor, John Wilkinson, GoLang Nuts Mailing List
> On Aug 8, 2025, at 5:35 PM, Ian Lance Taylor <ia...@golang.org> wrote:
> A generic type without type arguments is an abstract type that doesn't, in fact by definition can't, exist at run time. I don't think many people expect to be able to use an abstract type.

Speaking for myself — I may not have *expected* it, but I was certainly disappointed to learn it was not possible.

> You can write the same code today using the "any" type. This new feature adds additional compiler type checking. It prevents you from writing certain kinds of code, by detecting that that code is erroneous. It doesn't permit you to write anything that you can't write today.

One of the main reasons I moved to Go from dynamic languages was the compiler’s ability to catch breakage during refactor. Every time I must fall back to `any` and a type assertion, I lose that robustness guarantee.

So while it is technically true that we can already write the same code with `any`, the real issue is that doing so reduces type safety. This proposal is not about “clearer communication”, it is about preserving robustness.

>> Both type aliases and generics were justified by the fact that “there is code we can’t write today, and adding this feature will make it possible.” I’d argue this is the same: there is code we *can’t write robustly* today, and adding this feature would make that possible.
>
> I can get the `i`th element of a slice, so it’s surprising I can’t do the same with my defined type if it’s generic and uninstantiated.

Do others find that surprising? Here is one data point: I do.

Regarding complexity: I don’t find the “complexity in the language” argument compelling here. Supporting this would make the language *easier* to use for most people. For the majority, this would just work as expected; no need to understand the complexity of *why* it doesn’t.

The minority who do understand the edge cases are already capable of handling that complexity. In other words, the cost in learnability is low, while the gain is useful type safety.

The only case where I’d agree complexity is a real problem is if it caused measurable compiler slowdowns or made the compiler significantly harder to maintain. But you have said compiler complexity is a minor cost here, so I assume that’s not the case.

-Mike
#jmtcw

Axel Wagner

unread,
Aug 10, 2025, 5:06:58 AMAug 10
to Ian Lance Taylor, John Wilkinson, golang-nuts
Hi,

I would like to try to steel-man the proposal, i.e. try my best at interpreting it in a way that works. This got very long, as I want to be thorough. Feel free to skim sections that seem too boring (I separated them with ---).

On Thu, 7 Aug 2025 at 20:05, Ian Lance Taylor <ia...@golang.org> wrote:
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.

I would instead phrase my own understanding of the proposal as:

For any generic type declaration `type X[T any] TypeLiteral`, `X` is an interface type implemented by all instantiations of `X`".

I think for the case of TypeLiteral being an interface, your definition is equivalent (at least, if I understand correctly what you meant - I think there might be a grammatical ambiguity there). This phrasing is more general, as it also applies to non-interface types. It also answers many follow-up questions, I believe.

In particular, this could be implemented by 1. making only instantiations of `X` and `X` itself assignable to `X`, 2. making the runtime representation of `X` equivalent to `any` and 3. adding a field to the type descriptor that uniquely identifies the generic type declaration that it is an instantiation of (for example, an opaque singleton pointer). See also #54393. Type-assertions (including interface type assertions) on X would work the same as if `X` was `any` at runtime, but the compiler would be able to reject some impossible assertions. For example:

type X[T any] struct{}
func (X[T]) Get() T
type Y[T any] struct{}
func F(x X) {
    x.(X[int]) // allowed
    x.(X[string]) // allowed
    x.(string) // disallowed: impossible type-assertion: string is not an instantiation of X
    x.(X) // allowed, but pointless
    x.(Y) // disallowed: impossible type-assertion: X and Y are different type declarations
    x.(interface{ Get() int }) // allowed
    x.(interface{ Set(int) }) // disallowed: impossible type-assertion: X has no method Set
    any(x).(interface{ Set(int) }) // allowed, but would always fail
    any(x).(X) // allowed, would always succeed
    any(x).(Y) // allowed, would always fail
}

The zero value of `X` would be `nil`, i.e. an interface without dynamic type or value.

For simplicity, we could disallow all other operations on values of type `X` (except `==`/`!=`) just as we do for `any`. The bulk of the complication (and this E-Mail) comes from trying to allow more operations.

Now as for Ian's questions:

Let's suppose I write

    type Slice[T] []T

Can I then write

    func Pop(s Slice) Slice { return s[1:] }

This would be disallowed, in the simple version.

From a type safety perspective (I talk about implementation later), we could try to allow it. We could say something like

With `type Slice[T] []T`, given a value `s` of static type `Slice` and dynamic type `Slice[E]`:
1. The index expression `s[i]` is allowed and evaluates to an `any` with dynamic type `E`. `s[i]` is not addressable (so in particular is not a valid left-hand side in an assignment).
2. The slice expressions `s[i:j]` and `s[i:j:k]` are allowed and evaluate to a `Slice` with dynamic type `Slice[E]`
3. Passing `s` as the first argument of `copy` or `append` would be disallowed.
4. Using `s` as a vararg-expression is disallowed.
5. `clear` could be allowed, as every element type has a zero value, so it has well-defined semantics.
6. `len`, `cap` are allowed.

The restrictions are in place, because the element type of the dynamic type of `Slice` is unknown, so it would not be type-safe to allow writing to it. Essentially, `Slice` would act as a read-only `[]any`. The restriction on 4 in particular, exists because vararg slices share the backing array, so allowing it to be used as an `...any` would then create problems, as the callee could write to it.

That would make `Pop` work. Notably, it would also be possible for `Pop(Slice[int]{0})` to return a `Slice[string]`, if it so chose (I believe that is possibly where your question was going). That is, from the signature `func (Slice) Slice`, there is no guarantee that the dynamic type of argument and return are the same, just as with `func(io.Reader) io.Reader`. If the author of `Pop` wanted to provide that guarantee, they could instead write

func Pop[S Slice](s S) S { return s[1:] }

Note that this is different from what is possible today, as it doesn't require the second type parameter for the element type.

We could do similar things for
- `type Map[K comparable, V any] map[K]V`: `Map` allows non-addressable index-expressions, evaluating to `any`, and `range`, yielding `any, any`. Also `len`. We *might* allow `delete(m, k)`, if `k` is `any` (or, arguably, even for all types, which would just be a no-op, if the key type of the dynamic type of `m` is non-identical to the type of `k`).
- `type Chan[E any] chan E`: `Chan` allows channel-reads (including in `select`) evaluating to `any`, `close` and `==nil`. With `<-chan E`, only reading would be allowed. With `chan<- E` only `close` would be allowed.
- `type Pointer[T any] *T`: `Pointer` allows (non-addressable) pointer-indirections, evaluating to `any`. `*p` is not addressable.
- `type Struct[F any] struct{ A F; B int }`: `Struct` could allow the (non-addressable) selector expression `.A`, evaluating to `any`. It also allows the (non-addressable) selector expression `.B` evaluating to `int`.
- `type Int[T ~constraints.Integer] int: `Int` allows unary operators, evaluating to `Int` with the same dynamic type as the operand. Binary operators are disallowed.
- `type F[A any] func() A` allows call-expressions, evaluating to `any`. `type F[A any] func(A)` dose not allow call-expressions (could not be made type-safe).
- `iter.Seq[A]` and `iter.Seq[A, B]` (or other equivalent type definitions) allow `range`, yielding `any` and `any, any` respectively.
- `type J[A any] interface{ M() A }` allows call expressions `.M()`, evaluating to any`. `type J{A any] interface{ M(A) }` does not allow call expressions `.M(v)` (could not be made type-safe). Both allow selector-expressions `.M`, evaluating to `any` with dynamic value being the corresponding method value of the dynamic value of the interface.
- `type X[T any] Y; func(X[T]) M1() T; func(X[T]) M2(T)` would allow `.M()`, evaluating to `any` with dynamic type the type argument of the dynamic type of the receiver, would disallow `.M(v)` and would allow `.M`, evaluating to `any` with the dynamic type `func() T`, where `T` is type argument of the dynamic type of the receiver.

In all of these cases, comparison is allowed, as long as the static types are identical or one type is assignable to the others:
- If the compared values have different dynamic types, the comparison evaluates to `false`
- If the compared values have the same dynamic type and that type is non-comparable, the comparison panics
- If the compared values have the same dynamic type and that type is comparable, the comparison is `true` if the dynamic values are equal and `false` otherwise.

In all of these cases, comparison to `nil` would be allowed, checking if the interface value has a dynamic type. In particular, `Slice(([]int)(nil)) == nil` would evaluate to `false`, just like with `any` today. Just like with `any`, there is no way to check if the dynamic *value* of a `Slice` is `nil`, unless the dynamic type is known (that is, `Slice(([]int)(nil)) == ([]int)(nil)` would evaluate to `true`, while `Slice(([]int)(nil)) == ([]string)(nil)` would evaluate to `false`.

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

I'll note that as written, this code doesn't make sense as `Get` is not a generic type in the example. There are two possible ways you might have typoed this:

1. You meant `interface{ Get() string }`. This would be allowed and succeed if and only if the dynamic type of `t` has a `Get() string` method, just as if `t` had type `any`. In particular, it would succeed for `Test[string]`, but not for e.g. `Test[int]`.
2. You meant `interface{ Test[string] }`. This would also be allowed and succeed if and only if the dynamic type of `t` implemented `Test[string]`. In this example, that means the same as 1, because `Test[string]` is an interface type and embedding an interface type is the same as if we included all its methods in the surrounding interface.
3. If `Test` was not an `interface` declaration, but instead e.g. a `struct`, then writing `t.(interface{ Test[string] })` would not be allowed, as `interface{ Test[string] }` could not be used outside of type-constraints (it contains a union element).

---

Is this implementable?

The most limited suggestion (do not allow any operations on the interface types) is, I think, easily implementable. It is really no different to using `any`. Every time the compiler needs to know the actual type arguments to compile an operation, the value must first be type-asserted to a fully instantiated static type.

The extended version is more dubious. The easiest way to see that is with function types. As described, this would be allowed:

type Func[A any] func() A
func F() int { return 42 }
func G() {
    var f Func = F
    v := f() // v is any(int(42))
}

For this example, the compiler would implicitly create a wrapper when assigning to `f`, i.e:

var f Func = func() any { return F() }
v := f()

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

In this example, there is no statically known place to generate the wrapper: when assigning to `any`, the compiler doesn't know we eventually need a `func() any` and when type-asserting, the compiler doesn't know what the actual return type of the dynamic type of x would be. That is, the dynamic value of `x` would have to effectively act as both `func() int` and `func() any`.

You could argue the compiler should just generate both when assigning a function value to an interface type and then unpack it, depending on which version it is type-asserted into. But that would slow down all dynamic function calls (a `func` would have to represent a table of function pointers and the code would have to index into that table). It also quickly explodes once you consider the case of `All() iter.Seq[int]`, because now the compiler needs to generate all of `func() iter.Seq[int]`, `func() any` and `func() iter.Seq[any]`.

What this demonstrates, is that allowing to use this with function types actually comes down to the general problem of passing around uninstantiated generic functions (kind of unsurprising), which we disallow for a reason: it would slow down all programs and/or require runtime code generation, and generally is very complex.

For the non-function types, this might be easier. That's because to generate operations like an index into a slice, the compiler really doesn't need to know more than the element size (and probably the GCData) and the length/cap of the slice, which are already in the type descriptor of slices.

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.

Methods, would also have to be completely excluded, for the same reasons as functions.

This means, we would allow some intuitively reasonable operations, but not others. That seems like a confusing mix (we already have that problem with type parameters), so it's probably best to leave it out altogether and only implement the simplest version. That won't prevent people from filing issues, asking for all the other operations, of course.

---

Now, I think the simple version would create fairly consistent and mostly easy to understand semantics. And, personally, I don't think the complication to the language would be prohibitive.

I also agree, that this would be useful. It doesn't allow to write programs you couldn't write today, but I do think it could add type-safety for some libraries and I do think that is useful. In particular, we discussed some use cases of this in #54393, except that issue focuses on `reflect`. Today, all of these use cases require using `reflect` and some might not even be implementable with that. With this proposal, some of them could be implemented without. But it turns out, even that is pretty limited.

As a concrete example, we talked about whether the proposed `*hash.Map[K, V]` should be marshaled to JSON. My argument was that implementing `json.Marshaler` on `*hash.Map[K, V]` requires every user of `*hash.Map` to import `encoding/json` which seems very hefty. On the other hand, `encoding/json` could explicitly special-case on `*hash.Map`. That is, today, not possible to write. I did argue, that `encoding/json` should really specail-case on `interface{ All() iter.Seq2[K, V] }` instead, which can be written using `reflect`.

But this would seem like an ideal use case for this proposal, as `encoding/json` could instead type-assert on `*hash.Map`. But, the problem is that it then can't do anything useful with it:

func marshalHashMap(v any) (string, error) {
    m, ok := v.(*hash.Map)
    if !ok { return "", errors.New("not a hash map") }

    b := new(strings.Builder)
    b.WriteByte('{')
    // Whoops, method call is disallowed
    for k, v := range m.All() {
        // …
    }
}

On the other hand, this would still allow to write this code using `reflect`, as you at least have a concrete way to special-case on the generic type definition. And presumably, accepting this proposal would also imply accepting  #54393 (in fact, you could say this proposal is a pre-condition to #54393, because `reflect` generally only allows operations the static type system has - this proposal is a static version of #54393), so any use cases that covered, would probably also benefit from this proposal.

But I think this shows that we really need some concrete, real-world use cases for this proposal, that actually benefit from the simplistic, implementable version of the proposal. The original example from the proposal seems pretty artificial. And it isn't actually all that type-safe either, because there still needs to be a `default` case (it is forced to accept all `Test[T]`, but can only handle some).

---

Also, all of that being said: I disagree that people would always expect things to work like this. I agree that it is a self-consistent expectation that is intuitive. But a different, in my view equally consistent expectation (and, in fact, my expectation) would be that an uninstantiated generic type would act as a "type-factory", meaning that this would be allowed:

type X[T any] struct { F T }
func (x X[T]) Get() T { return x.F }

func F(x X) {
    var a x[int]
    var b x[string]
    a.F = 42
    b.F = "foo"
    fmt.Println(a.Get(), b.Get()) // 42, foo
}

I think this would be far more useful and in particular, if it could work, would be able to cover all of the use cases of the design I describe above. However, this was left out of the original generics design intentionally. It creates confusing semantics in the presence of interface type-assertions. And generally doesn't really seem implementable.

It speaks (in my opinion) against this proposal, that there are at least two completely reasonable ways to expect it to behave. It also refutes the argument that "novices need to understand why this doesn't work": under this proposal, novices need to understand why this works in one way, but not the other. Which really comes down to the same thing: understanding why uninstantiated generic types can't be used as types today.



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.

John Wilkinson

unread,
Aug 13, 2025, 12:29:29 PMAug 13
to golang-nuts
re Kevin: Thank you for the workaround! I'm going to try it out. It's clever.

re Axel:

I really appreciate this analysis. I'm still kinda processing it, trying to make sure I understand.

There are a couple of thoughts I have on it, that I suppose I can add now.

I want to start at the end, and then maybe work backwards a bit.


> But a different, in my view equally consistent expectation (and, in fact, my expectation) would be that an uninstantiated generic type would act as a "type-factory"

That's interesting, and not a behavior I had considered.
It doesn't feel consistent to me with how Go works.
If I have a function argument, and I want to refer to its type, currently I would need to use the type parameter:

func PrintDefault[T any]() {
v := new(T)
fmt.Println(*v)
}

func main() {
PrintDefault[int]()
}

This way I can access the underlying type data.

If I wanted to get access to a sort of type factory and Go was going to support that, I would imagine the same syntax would hold:

type X[T any] struct { F T }
func (x X[T]) Get() T { return x.F }

func F[x X]() {
    var a new(x[int])
    var b new(x[string])

    a.F = 42
    b.F = "foo"
    fmt.Println(a.Get(), b.Get()) // 42, foo
}

But I understand the point is that it's another possible interpretation of my proposed syntax, which means not everyone would recognize the change as expected and intuitive.

I am unsure what the critical mass of expectations for a syntax change looks like, or if there even is one.

----


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

However, if we have some set of intuitively reasonable operations that are currently disallowed, and we reduce the size of the set, that seems like the right call. I suppose the argument would be that we are subtracting op A from the set but adding ops B, C, and D. But I don't think I agree with that reasoning, it seems more likely that B, C, and D have been in the set but express themselves as A, since A is the blocker for their use.

---
> But I think this shows that we really need some concrete, real-world use cases for this proposal, that actually benefit from the simplistic, implementable version of the proposal. The original example from the proposal seems pretty artificial. And it isn't actually all that type-safe either, because there still needs to be a `default` case (it is forced to accept all `Test[T]`, but can only handle some).

I think we would need some sort of union types to perfectly express what would and would not be allowed. So yeah, there would need to be a default case. The discussion around union types has been going on for a long time, and I don't think this proposal is at odds with an implementation there.

I will spend some time coming up with a more complete example of a real-world use case. I think the simple version demonstrates the ask, but perhaps not the need- at least not in a way obvious to folks who haven't encountered the difficulty and are already on board with the proposal.

----

Thanks for all the responses and insight.
-JM



Axel Wagner

unread,
Aug 13, 2025, 2:47:25 PMAug 13
to John Wilkinson, golang-nuts
> 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.

Correct, sorry.
 
> 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.

No, not equivalent. `Func` is a different type from `Func[any]`. The type-checker would treat them differently. They would just have to have effectively the same run time implementation.

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.

The point is that, hypothetically, it *would be type-safe* to allow reading fields. That is, this has well-defined semantics:

type X[T any] struct {
    F1 T
    F2 int
}
func F(x X) {
    var f1 any = x.F1
    var f2 int = x.F2
}

But it can't possibly be implemented, as the offset of `F2` (the address of the field relative to the address of `x`) depends on the size of `T`.

So, struct types are another class of things that can't really be made to work with uninstantiated generic types (beyond treating them as `any`).

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

As far as I am concerned, limitations of Go generics fall into four classes:
1. We haven't really gotten around to it/haven't seen the need yet, but there is no intrinsic reason why we wouldn't (selecting common fields of type parameters constrained to union types is an example. Arguably, not being able to use interfaces with union elements as types is as well).
2. We think they would undermine the simplicity of the language (Hindley-Milner like type inference is a good example of this)
3. Parts of them can't be implemented, so we leave them out entirely (for example, we *could* allow type parameters on methods, as long as interfaces using them can only be used in constraints and they don't participate in interface satisfaction. But we think that's worse than not having them at all)
4. They where a design mistake, which we realized too late (not allowing interfaces with methods in union elements is such a restriction. I hope that if we had realized that limitation earlier, we would have tried to do something else instead of union elements)
 
Basically, I think the idea of allowing any operations on uninstantiated types that are not allowed on `any` falls into 3. It would be very weird to be able to allow to index into a generic slice type, but not access a field of a generic struct type. So it seems much better to not allow any of these and we would have to treat uninstantiated generic types as `any`. To be clear, I think that was your original proposal anyways. the conversation just opened up the possibility to do more.

We can then have differing opinions on whether or not the idea of uninstantiated generic types *generally* falls under 3 - that is, if we find that if we can't allow to e.g. reslice uninstantiated generic slices, we should leave out uninstantiated generic types altogether.

I think we would need some sort of union types to perfectly express what would and would not be allowed.

To me, that suggests strongly, that these use cases have nothing at all to do with generics and are instead about just having union or sum types.
 

atd...@gmail.com

unread,
Aug 15, 2025, 10:26:29 AMAug 15
to golang-nuts
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 for instance.
But anyway it is akin to bolting a type system on top of a type system.
Not necessarily that straightforward.




Reply all
Reply to author
Forward
0 new messages