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.

Reply via email to