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 On Sunday, August 10, 2025 at 2:06:58 AM UTC-7 Axel Wagner wrote: 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 <https://github.com/golang/go/issues/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 <https://go.dev/play/p/vC17ODSNP6u>, so allowing it to be used as an `...any` would then create problems <https://blog.merovius.de/posts/2018-06-03-why-doesnt-go-have-variance-in/>, 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 <https://go.dev/ref/spec#Method_values> 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 <https://go.dev/doc/faq#nil_error>. 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 <https://github.com/golang/go/issues/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 <https://github.com/golang/go/issues/69559#issuecomment-3086751038>. 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` <https://go.dev/play/p/4q8H6Lc5J1E>. 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 <https://github.com/golang/go/issues/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 . -- 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/056e3884-380c-4d6e-9b64-8a9f353ab5c1n%40googlegroups.com.