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 <i...@golang.org> wrote:

> On Thu, Aug 7, 2025 at 10:46 AM John Wilkinson <john.wilkin...@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+unsubscr...@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/CAEkBMfFVgvHbDyAishzqHYQyjq9VROs8RFxMXdwL44bx5pPGDA%40mail.gmail.com.

Reply via email to