golang/go

spec: generics: permit type parameters on aliases

mdempsky opened this issue ยท 84 comments

The generics proposal says "A type alias may refer to a generic type, but the type alias may not have its own parameters. This restriction exists because it is unclear how to handle a type alias with type parameters that have constraints."

I propose this should be relaxed and type aliases allowed to have their own type parameters. I think there's a clear way to handle type aliases with constrained type parameters: uses of the type alias need to satisfy the constraints, and within the underlying type expression those parameters can be used to instantiate other generic types that they satisfy.

I think it's fine to continue allowing type VectorAlias = Vector as in the proposal, but this should be considered short-hand for type VectorAlias[T any] = Vector[T]. More generally, for generic type B with type parameters [T1 C1, T2 C2, ..., Tn Cn], then type A = B would be the same as type A[T1 C1, T2 C2, ..., Tn Cn] = B[T1, T2, ..., Tn].

In particular, something like this would be an error:

type A[T comparable] int
type B[U any] = A[U]   // ERROR: U does not satisfy comparable
type C B[int]

As justification for this, analogous code in the value domain would give an error:

func F(x int) {}
func G(y interface{}) { F(y) }  // ERROR: cannot use y (type interface{}) as int
func H() { G(42) }

I suspect if TParams is moved from Named to TypeName and type instantiation is similarly changed to start from the TypeName instead of the Type, then this should work okay.

/cc @griesemer @ianlancetaylor @findleyr @bcmills

If this proposal were accepted, would the following code be valid?

type A[T any] int
type B[U comparable] = A[U]

I.e. would it be possible to define an alias which tightens the constraints of the aliased type?

IMO the example in the value domain is more analogous to defining a new named type, which already behaves as expected:

type A[T comparable] int
type B[U any] A[U] // ERROR: U does not satisfy comparable

Aliasing seems more analogous to function value assignment, for which we don't redeclare parameters:

func F(x int) {}
var G = F

I think you're right about how this could be implemented, but I wonder if it is conceptually coherent. Specifically, I wonder about whether we should think of the declaration as parameterizing the type, or as defining a parameterized type, and whether it still makes sense to call the example with additional restrictions above an alias.

I'll also note that as you point out, our decisions with respect to the go/types API have real consequences for how easy it would be to relax this restriction on aliases in the future, so it is good to talk about this now. Thanks for raising this issue!

I.e. would it be possible to define an alias which tightens the constraints of the aliased type?

Yes. U (type parameter with bound comparable) satisfies the constraint any, so that's a valid type declaration in my mind. But similarly, trying to instantiate B[[]int] would be invalid, because []int does not satisfy comparable, even though it satisfies the underlying any.

I would expect that the type checker would see B[[]int], resolve B to the TypeName and check it against the type parameters, and then reject it as invalid, before proceeding to instantiating/substituting its Type with the type argument []int.

Aliasing seems more analogous to function value assignment, for which we don't redeclare parameters:

Note that var G = F is really shorthand there for var G func(int) = F. You're not allowed to write var G func(interface{}) = F, for example, even if you only ever call G with int arguments.

But this is also why I suggest still allowing type A = B as shorthand for explicitly writing out type parameters for the alias declaration.

There is a reason why we didn't do this in the first place.

I don't have any principal objections to this proposal. If we accept this, I wonder whether we should still permit the type A = B form as it does deviate from the current design which requires that every use of a generic type requires an instantiation.

I'm inclined to proceed in one of two ways:
1) Disallow (not implement) the form type A = B for Go1.17. It's not crucial and we can always add it later.
2) Implement this proposal instead of permitting type A = B.

I seem to recall @rogpeppe raising a similar point in various conversations.

@findleyr

Aliasing seems more analogous to function value assignment, for which we don't redeclare parameters:

Note that we do allow function value assignment to strengthen (but not weaken) a type via assignability, which IMO is analogous to strengthening type constraints on a type declaration.

Consider this program:

package main

import "context"

func cancel() {}

type thunk func()

var f = cancel
var g context.CancelFunc = cancel

In that program, var f = cancel is shorthand for var f func() = cancel.

The declaration var g context.CancelFunc = cancel refers to the exact same function value, but with a stronger type (one that is not assignable to thunk).

It looks like it could fall out of the definition but, to be explicit, partial application would also be useful:

type Named[T any] = Map[string, T]

@griesemer If we proceed with this proposal, I think it could be a nice convenience to keep type A = B as short-hand. But as it's not essential, I'd similarly be fine with just removing it altogether. We can always re-add it in the future if appropriate.

And yes, the deviating from the norm of requiring instantiation is what threw me off. I had written some code that was working under the assumption that if I only started from non-generic declarations, then I would never see a non-instantiated type. But that doesn't hold for the type A = B form. (Fortunately though, it's not hard to special case this one instance either.)

@bcmills

Note that we do allow function value assignment to strengthen (but not weaken) a type via assignability, which IMO is analogous to strengthening type constraints on a type declaration.

The example from #46477 (comment) made the analogy of function parameters with type parameters (which makes sense). In that analogy, we don't allow changing function parameter types when assigning [example], i.e. we don't support covariant function assignment.

In that analogy โ€ฆ we don't support covariant function assignment.

Sure, but pretty much the entire point of type parameters is to support variance in types. ๐Ÿ˜‰

neild commented

What is the use case for permitting parameters on type aliases?

@neild The same reason for which type aliases were introduced in the first place, which is to make refactoring across package boundaries easier (or possible, depending on use case).

I misread this comment. See below.

Going through my notes I remember now why we didn't go this route in the first place: Note that an alias is just an alternative name for a type, it's not a new type. Introducing a smaller set of type arguments (as suggested above), or providing stronger type constraints seems counter that idea. Such changes arguably define a new type and then one should do that: declare a new defined type, i.e., leave the = away. I note that @findleyr pointed out just that in the 2nd comment on this proposal.
This would mean that the respective methods also have to be redefined (likely as forwarders) but that seems sensible if the type constraints are narrowed or partially instantiated.

In summary, I am not convinced anymore that this is such a good idea. We have explored the generics design space for the greater part of two years and the devil really is in the details. At this point we should not introduce new mechanisms until we have collected some concrete experience.

I suggest we put this on hold for the time being.

neild commented

@griesemer I don't see what the refactoring case is for changing the constraints of a type. As you say, an alias is just an alternative name for a type, but an alternative name with altered constraints is a subtler concept that I struggle to see the use for.

I may be missing something. A concrete example of when you'd use this would be useful.

@neild Agreed - I misread your comment as "what is the use of allowing alias types for generic types" - my bad. See my comment just before your reply.

I don't see what the refactoring case is for changing the constraints of a type.

Under this proposal, you can do more with parameterized type aliases than just change the constraints. E.g., see #46477 (comment) for using type parameters to provide default arguments to other generic types. I called out the constraint change to clarify the semantics, not because I expect that's something people are likely to do in practice.

I anticipate analogous to how we added type aliases to facilitate large-scale refactorings while maintaining type identity, we're going to face situations where generic types need to be refactored to add, remove, or change parameters while also maintaining type identity. Having parameterized type aliases would facilitate that. I think if just "declaring a new defined type" was always an adequate solution, we could have skipped adding type aliases too.

I think it's fine though if Go 1.18 doesn't have parameterized type aliases. But I at least think we should try to ensure the go/types APIs are forward compatible with adding parameterized type aliases.

@bcmills

Sure, but pretty much the entire point of type parameters is to support variance in types. ๐Ÿ˜‰

FWIW, I don't follow this argument. We still support variance in types no matter what we decide about this proposal, just like we allow variance in function arguments whether or not we allow covariant assignment of function values. I think we're dipping in and out of the 'meta' realm. The point I was trying to make is that if we're trying to argue by analogy with the value domain, wrapping a function is more like defining a new named type (or perhaps more correctly like struct embedding), and aliasing is more like assignment. Since we don't allow covariant assignment for functions, it's arguably a bit inconsistent to allow covariant assignment for "meta functions" (if that's how we think about generic declarations).

@griesemer

This would mean that the respective methods also have to be redefined (likely as forwarders) but that seems sensible if the type constraints are narrowed or partially instantiated.

Or use embedding, which might be more analogous to wrapping a function in the value domain.

I suggest we put this on hold for the time being.

Independent of whether we relax the restriction on aliases, this proposal indirectly makes the point that it matters whether we think of the "type" as generic or the "declaration" as generic, both in the current APIs and for future extensions of the language. For example, thinking of the type declaration as generic allows relaxing this restriction on aliases. Thinking of the function type as generic allows for generic interface methods and generic function literals. If we put this proposal on hold, we will still need to make API decisions that affect its feasibility.

[re: value vs type domain analogies]

I want to clarify that I made this analogy initially to help explain how I intuit the relationships here. Go's values and types operate sufficiently distinctly and irregularly that I think trying to read too far into the analogy is going to hit rough edges and become more philosophical than actionable. E.g., the value domain has no analog to defined types and type identity, because it's impossible to create a copy of a Go value that's distinguishable from the original. (Emphasis: I'm talking specifically about values here, not variables.)

Certainly we should revisit these discussions when it comes time to add dependent types to Go 3 though. :)

Note that an alias is just an alternative name for a type, it's not a new type.

I'm not sure that this is entirely true. What about this, which is currently allowed?

type S1[V any] struct { .... }

type S2 = S1[int]

S2 neither an alternative name for an existing type nor an entirely new type. More of a composite type, perhaps. Also, it does have some identity of its own (its name is used when it's embedded)

Introducing a smaller set of type arguments (as suggested above), or providing stronger type constraints seems counter that idea. Such changes arguably define a new type and then one should do that: declare a new defined type, i.e., leave the = away

Sometimes defining a new type isn't possible. For example, if a type is specifically mentioned in a type signature, it's not possible to use a new type - you have to use the same type as the original. Also, the fact that all methods are lost when you define a new type is a real problem and embedding doesn't always work either.

For non-generic code, it might usually be possible to define a fully-qualified type alias like S2 above, but in generic code that's often not possible because a type parameter might be free.

An example:

Say some package defines an OrderedMap container that allows an arbitrary comparison operation for keys:

package orderedmap

type Map[K any, V any, Cmp Comparer[K]] struct {
    ...
}

func (m *Map[K, V, Cmp]) Clone() *Map[K, V, Cmp]

func (m *Map[K, V, Cmp]) Get(k K) (V, bool)

type Comparer[K any] interface {
    Cmp(k1, k2 K) int
}

I want to implement a higher level container in terms of orderedmap.Map. In my implementation, only the value type is generic:

package foo

type Container[V any] struct {
}

func NewContainer[V any]() *Container[V] {
    ...
   var m *orderedmap.Map[internalKey, V, keyComparer]
}

type internalKey struct {
    ...
}

type keyComparer struct{}

func (keyComparer) Cmp(k1, k2 internalKey) int {
    ...
}

In the above code, whenever I wish to pass around the orderedmap.Map[internalKey, V, keyComparer] type, I have to do so explicitly in full. This could end up very tedious (and annoying to change when refactoring the code). It would be nice to be able to do:

type internalMap[V any] = orderedmap.Map[internalKey, V, keyComparer]

Then we can avoid duplicating the type parameters everywhere.

Defining a new type wouldn't be great here - you'd either have to explicitly forward all the methods (if you did type internalMap[V any] orderedmap.Map[...]) or reimplement some of the methods (if you did type internalMap[V any] struct {orderedmap.Map[...]}).

In short, I'm fairly sure that generic type aliases are going to be a much requested feature when people start using generics in seriousness, and that they're definitely worth considering now even if they're not implemented, so that the type checker isn't implemented in a way that makes it hard to add them later.

rsc commented

This proposal has been added to the active column of the proposals project
and will now be reviewed at the weekly proposal review meetings.
โ€” rsc for the proposal review group

I think we have to consider what exactly an alias is in Go language:

Alias declarations
An alias declaration binds an identifier to the given type.
AliasDecl = identifier "=" Type .
Within the scope of the identifier, it serves as an alias for the type.

Since an alias is an identifier, that is, in essence, a name, it does not make sense for such a name to have type parameters. An alias is simply a name for a type, not a type in itself. And names should not be parameterizable.

If it is desirable to define a new generic type based on an other generic type, this should be done using different syntax than a type alias.

Therefore I, respectfully oppose this proposal.

I think we have to consider what exactly an alias is in Go language:

We're discussing amending the Go language spec here, so I think referring to the current wording is somewhat begging the question. If we decide to amend the spec to allow type-parameterized aliases, then I think it's within scope to amend those sentences too.

If it is desirable to define a new generic type based on an other generic type, this should be done using different syntax than a type alias.

We have existing syntax for type aliases and for type parameters, and conveniently they're compatible. I don't see why we'd want to use a new syntax for type-parameterized aliases.

Well, the reason I refer to the current spec is because that explains what the current concept of am alias is. If we were to change the spec, the concept of what an alias is will also change quite radically. An alias will not be just a name for a type anymore. And I feel this will make Go quite a bit harder to learn.

Furthermore, I would say that the changing concept of an alias as a name to something else is at least not conceptually backwards compatible. With this proposal an alias is not just a name any more, but also a way to define types. Since the concept of this proposal is different, i feel the syntax should be different as well. Maybe just using := in stead of = for example.

This would not let you define any new types: it would let you give a name to a subfamily of a family of types.

rsc commented

What is the concrete benefit that this would bring?
And is it necessary to have in Go 1.18, or should we wait until a future release?

@jimmyfrasche I don't know what "let you give a name to a subfamily of a family of types." even means. There is no such concept as "a family of types" AFAIK in Go. Definitely confusing, hence my opposition to it. And certainly we can wait until 1.18 and see if this turns out to be necessary after all.

What is the concrete benefit that this would bring?

The concrete benefit is it gives a way to evolve code bases that use parameterized types. The same concern as why we added type aliases in the first place.

Otherwise, what happens when you have a large code base that relies on type type Foo[T any] ... and then you realize you need to change the type parameter list? How do you 3-step migrate code to use Bar[T, int] instead without breaking type identity in the process?

And is it necessary to have in Go 1.18, or should we wait until a future release?

I don't think they need to be available in Go 1.18, as there's no code yet to migrate. But as @findleyr points out (#46477 (comment)), it does affect the go/types API (i.e., #47916).

@mdempsky I have to admit that this feature would be the only way to do clean refactoring as you describe. However, I think it should be only used for that. Otherwise we may get this confusing notions of "families of types". An "normal alias" and a "generic alias" should be seen as two separate features, because they do something fundamentally different.

@beoran I'll try to clarify my terminology.

A generic type F[T any] isn't a type: you can't have a value of type F. You can have a value of type F[int] or type F[string] and so on. One way to talk about this is to say that F is the family of types F[int], F[string], and so on for every T that satisfies F's constraints.

If we allow type Fint = F[int] that's not creating a new type it's giving a name to F instantiated with int.

Similarly type Fc[T comparable] = F[T] isn't creating a new type or even a new generic type: it's describing the members of the family F where T is further restricted to the comparable types. All the types in Fc are still types in the family F, so Fc can be described as a subfamily of F.

That does expand what are now called type aliases beyond what they do today but all existing type aliases would still work exactly the same. If a new syntax were for given for "generic aliases", regular type aliases would be the special case where there are no type parameters involved, so then you'd have two ways to create a type alias. That seems more confusing than expanding the existing mechanism.

Thanks for that explanation. What I understood from it is that in Go, a generic type is not a type. Which does make the name "generic type" a bit unfortunate. Anyway, as you say, only fully-specified instances of generic types are types themselves. I also don't know if I like the name "type family", but your explanation does clarify it somewhat.

Now that I see the value of this for refactoring, I am not opposed anymore to the proposal as such, provided that is is well explained and based on well-documented concepts. If the concept of type family will be used as the foundation for these aliases, then the this concept of type families should be added to the Go specs and documentation in the appropriate places.

One aspect that hasn't been discussed yet is the ability to add methods to a type through aliases. Presumably, the following declarations would be valid under this proposal:

type T[P any] struct{ ... }
func (T[P]) m() { ... }

type A0 = T[int]
func (A0) m0() { ... }

type A1[P C] = T[P]
func (A1[P]) m1(x P) { ... }

type A2[P C, Q D] = T[P]
func (A2[P, Q]) m2(x P, y Q) { ... }

A0 would be identical to T[int, and A1[P] would be identical to T[P] for a suitable P. What about A2? Presumably it would not be permissible to add type parameters because otherwise different A2[P, Q] (with different Q) would be identical to T[P] (which in turn would mean those different A2[P, Q] would be identical to each other which seems odd given that the method m2 might depend on the 2nd type parameter).

Similarly, what about

type A3[P any] = T[int]
func (A3[P]) m3(x P) { ... }

Different A3 instantiations can't possibly all be identical to T[int] since the m3 method will be different for each P.

It seems that a parameterized alias declaration would not be allowed to introduce new type parameters, and constraints for type parameters could only be tightened, not loosened. With that later rule, there may be a co/contra-variant issue coming into play as well: for instance, if A1 is an alias for T (above), A1 is still not "completely" identical to T because we cannot simply use an A1[Arg] where a T[Arg] is permitted because the constraint for the A1 type argument may be tighter than for the T type argument. This also means that we can't just drop type arguments in an alias. We have to keep them, and they have to have the same constraints.

If that is all true, then the only thing that can be achieved is a renaming of the type parameters, but then we might as well leave things alone.

we cannot simply use an A1[Arg] where a T[Arg] is permitted because the constraint for the A1 type argument may be tighter than for the T type argument.

We can use an A1[Arg] where a T[Arg] is permitted, provided that T[Arg] is permitted at all. (What we cannot do is assume that T[Arg] is permitted in the first place based on the validity of A1[Arg].)

Presumably it would not be permissible to add type parameters because otherwise different A2[P, Q] (with different Q) would be identical to T[P] (which in turn would mean those different A2[P, Q] would be identical to each other โ€ฆ).

I think it would have to be the case that the different A2[P, Q] are indeed identical to each other. That seems degenerate at first glance, but I don't think it actually is entirely degenerate: you could write a function in terms of A2[P, Q] that uses the same Q in a different part of the function signature, which would at least force the caller to be explicit about which Q they want (because otherwise type unification would fail).

Moreover, it might actually be useful (for API migration purposes) to redefine an A2[P, Q] that turned out not to need the Q after all so that it is an alias for some A3[P].

Different A3 instantiations can't possibly all be identical to T[int] since the m3 method will be different for each P.

Honestly, I'm astonished that methods can be added via alias-names today, although I suppose that already only works within the same package. ๐Ÿ˜…

That being the case, we already have this problem as it stands today: @mdempsky's original post quotes, โ€œA type alias may refer to a generic type, but the type alias may not have its own parameters.โ€ If I am reading that correctly, that allows this part of the problematic example:

type T[P any] struct{ ... }
func (T[P]) m() { ... }

type A0 = T[int]
func (A0) m0() { ... }

So I think it would be best to disallow adding a method via an alias entirely (regardless of generics). Perhaps that could be a removal accompanying the addition of generics in Go 1.18, regardless of what we decide for the rest of type aliases?

Interesting. Certainly the compiler doesn't like it. For:

package main

type T[P any] struct{}
func (T[P]) m() { println("T[P].m") }

type A0 = T[int]
func (A0) m() { println("A0.m") }

func main() {
	var x T[int]
	x.m()
	var y A0
	y.m()
}

we get

$ go tool compile x.go2
<autogenerated>:1: InitTextSym double init for "".T[int].m

which makes sense (even though the code type-checks - probably it shouldn't).

And

package main

type T[P any] struct{}
func (T[P]) m() { println("T[P].m") }

type A0 = T[int]
func (A0) m1() { println("A0.m") }

func main() {
	var x T[int]
	x.m()
	var y A0
	y.m1()
}

doesn't work at all (y.m1 undefined (type T[int] has no field or method m1)) which is to say that we cannot add a method to an instantiated type (somewhat expectedly, but the type checker should complain).

It may be possible to resolve all these issues in a non-contradictory manner, but given that even the current simple alias handling is broken, I am leaning ever more towards disallowing alias declarations for parameterized types, at least for now.

One aspect that hasn't been discussed yet is the ability to add methods to a type through aliases. Presumably, the following declarations would be valid under this proposal:

type A0 = T[int]
func (A0) m0() { ... }

No, A0 here is an alias to an instantiated type. I don't think it's appropriate to add methods to a particular instantiated method. I'm surprised that's accepted. That seems like a bug: https://go2goplay.golang.org/p/0h7rWPyr9OS

type A1[P C] = T[P]
func (A1[P]) m1(x P) { ... }

I would lean towards disallowing this for implementation simplicity, but I'm not strongly opposed to allowing it.

Edit: I missed that C differs from T's original type parameter constraint, any. I only think it should possibly be allowed if C is identical to any. It should always be an error if C is different from any.

type A2[P C, Q D] = T[P]
func (A2[P, Q]) m2(x P, y Q) { ... }

I'd say this should be an error, at least if Q is actually used.

Edit: Same concerns from A1 about C vs any apply here too.

type A3[P any] = T[int]
func (A3[P]) m3(x P) { ... }

As with A0, I don't think we should allow adding methods to instantiated types.

If that is all true, then the only thing that can be achieved is a renaming of the type parameters, but then we might as well leave things alone.

To be clear, you're talking here just within the context of declaring generic methods using type aliases? If so, I agree.

In retrospect, I feel like we probably shouldn't have allowed declaring methods on (non-generic) type aliases. But that ship has sailed, and it's not that problematic in practice to support. I think it's reasonable to be inconsistent here and disallow declaring methods on generic type aliases.

Put differently: the design principle of type aliases is that you can rewrite any code that uses type aliases into an equivalent bit of code without them. (Edit: Almost: type aliases do allow renaming of embedded fields.)

So something like:

type T[X any] struct{}
type U[Y any] = T[Y]
func (U[Z]) m() {}

I think could be fine, because it has the equivalent alias-free code of:

type T[X any] struct{}
func (T[Z]) m() {}

That's not to say I think allowing it is important or even necessarily desirable. Just that it's consistent with the type alias design.

However, that doesn't apply to any of the other test cases from above. For example,

type U = T[chan int]
func (U) m1() {}

has no equivalent type-alias-free form, because we don't allow writing

func (T[chan int]) m1() {}

And if we don't allow declaring methods on the instantiated type T[chan int], then we shouldn't allow declaring them on T[int] either. (Note: there's an extra complication that the syntax func (T[int]) m1() {} isn't declaring a method on instantiated type T[int], but instead using int as the local bound identifier for T's type parameter.)

I'm writing a deque package, and it would nice if I could do

type Deque[T any] struct { ... }
type SortableDeque[T constraints.Ordered] = Deque[T]
func (sd SortableDeque[T]) Less(i, j int) bool { ... }

But I can also implement it by embedding a plain Deque in a SortableDeque, and I think the ability to add new methods onto an alias is just too powerful to add without a lot of headscratching first. For example if I have a function that takes a Deque, can I do a type assertion to try to upgrade it to a SortableDeque? What if someone adds conflicting methods on different aliases of the same concrete types? I dunno, there's just a ton of weirdness here and it makes me extremely nervous.

@carlmjohnson No, I think that's outside the scope of what this proposal is to allow. Methods are associated with defined types, and type aliases do not declare new defined types. If you have type SortableDeque[T C] = Deque[T] (for any C), it does not make sense that SortableDeque[T] should ever have a different method set than Deque[T].

I would say your func (sd SortableDeque[T]) Less(i, j int) bool { ... } declaration should be an error, for the same reason as gri's A1 example above (i.e., that the type parameter constraints are unequal).

rsc commented

I thought some more about this, and I'm starting to believe we should accept this proposal replacing the current definition for type aliases.


First, from a language design perspective:

If T[X] is a parameterized type, then the form type A = T is the only place in the entire grammar where T (also A) is allowed to appear without being followed by type arguments. That makes type alias a more special case than it needs to be: it's no longer an alias for a type and more like a #define. The type theorists would say (I believe) that T is a type schema while T[X] is a type. The problem is that type aliases have turned into type schema aliases when nothing else in the language allows direct manipulation of type schemas, only types.

I think the language definition ends up significantly more regular if we make the type alias definition parameterized the same as all other definitions and require it to refer to types the same way as the rest of the grammar.


Second, thinking about actual usage.

Aliases are not just for refactoring. They can be for shortening names too. For example, if we'd had aliases when we wrote filepath.Walk we'd probably have made WalkFunc an alias instead of a type.

As another example, I was writing a lexer the other day with functions of the form func(string, int) (T, int, bool). They take the input and an input offset and return the thing they lexed (T), the new input offset, and an ok bool. I'd like to write some tests independent of T, and one thing I'd like to be able to write is

type lexer[T any] = func(string, int) (T, int, bool)

This proposal would let me write that, whereas I can't write it today.

If there was API that used such types explicitly, then introducing the alias would let me shorten those names without changing the type signature of existing API, whereas a new type would not.

Getting back to more direct refactoring, though, I could see having defined type T1[X, Y] and then realize you want another parameter and generalize it to T2[X, Y, Z]. It would help to be able to write

type T1[X, Y any] = T2[X, Y, defaultZ]

to keep older code working.


Finally, let's leave discussions about aliases and methods to the side. They are what they are, we examined them carefully at the time, and if we are to reconsider a backwards-incompatible breaking change regarding the interaction between aliases and methods, that should be in its own issue (and would require significant new evidence, per the link).

I have been thinking about this a bit as well.

Getting back to more direct refactoring, though, I could see having defined type T1[X, Y] and then realize you want another parameter and generalize it to T2[X, Y, Z]. It would help to be able to write

 type T1[X, Y any] = T2[X, Y, defaultZ]

to keep older code working.

I like that!

To generalise the pattern of allowing more parameters on the right of the '=' than the left,
one form would be

type T1 = T2[defaultX]

(In my current attempts at using type params, I am finding that type arguments can become contagious and tedious, propagating through data structures and into places where the type parameter is impertinant. More generally, the existence of type parameters gives us a whole new set of examples of refactoring: making code take type parameters where it didn't before. Perhaps allowing type T1 = T2[defaultX] would facilitate exactly that.)

[Edit: Oh wait, it actually works already if defaultX is instantiated.. sorry. But in any event I like the extension to T1[X,Y any] = T2[X,Y,defaultZ]; it would help reduce type parameter viral spread ]

Change https://golang.org/cl/346294 mentions this issue: cmd/compile/internal/types2: disallow aliases for generic types for now

rsc commented

CL 346294 disabled aliases entirely for Go 1.18. That seems like the right choice given the ambiguity here about which path we want to take (we don't want to take both!).

It seems like we should continue the discussion here and reach a resolution, but either way, we can postpone the implementation until Go 1.19 in case there are more surprises.

rsc commented

The general consensus at this point seems to be:

  1. It sounds like a good plan to make this the one definition of aliases for generics (replacing the old one that is now removed).
  2. Thinking through what it would mean to define a method on a receiver written as a type alias (instead of the actual name) is very confusing - it might have substituted fixed types for the original type's parameters, rearranged them, and so on - so we should probably just disallow entirely defining methods with receivers that are parameterized type aliases. (If we had it to do over, we might also disallow receivers that are ordinary aliases, but nothing to do about that now.)

Do I have that right?

(And no matter what we decide we will wait until Go 1.19 to implement it.)

(And no matter what we decide we will wait until Go 1.19 to implement it.)

I don't object to waiting until Go 1.19 to release parameterized type aliases, if there are subtleties we're worried about implementing correctly. But I think prototyping it for Go 1.18 (and hiding behind an experiment or compiler flag) to make sure we get the go/types APIs right makes sense (#47916).

E.g., there's no Named instance for type aliases, only the TypeName; so I think the type parameters need to be added to the latter instead.

But I think prototyping it for Go 1.18 (and hiding behind an experiment or compiler flag) to make sure we get the go/types APIs right makes sense (#47916).

Agreed. As mentioned in #47916 (comment), I don't think we can resolve the go/types API proposal until we have a plan for how we'll support this proposal. Moving TypeParams to TypeName is one way to do this (albeit not fully worked out), and @griesemer thought perhaps now was the time to introduce a proper alias type, which could intermediate between the TypeName and Type. This latter idea might not work out pragmatically, due to our compatibility constraints.

I am doubtful that we can get away w/o introducing a proper Alias "proxy" type: without such a type that is used when we use an alias, we will lose the alias information in error messages because an alias type, after resolving it, will simply point to the actual type. This will be super-confusing in error messages.

Also, having a proper Alias type representation will allow us to resolve several ancient issues related to aliases.

rsc commented

Seems fine to prototype and plan.

I'm not hearing any objections to accepting this proposal, so will mark it likely accept.

rsc commented

Based on the discussion above, this proposal seems like a likely accept.
โ€” rsc for the proposal review group

rsc commented

No change in consensus, so accepted. ๐ŸŽ‰
This issue now tracks the work of implementing the proposal.
โ€” rsc for the proposal review group

Change https://go.dev/cl/390315 mentions this issue: cmd/compile: remove unneeded type alias code in unified IR

Given that the Go 1.19 release freeze has started, I guess this won't make it into Go 1.19?

I found this issue because I would like to have a temporary package with copies of the new types in sync/atomic to be able to use them with Go 1.18. For Go 1.19 onwards, they would become type aliases. This doesn't work now, because I can't have a generic alias to atomic.Pointer[T any]. So it would've been convenient if this would have been implemented for Go 1.19.

@Merovius, this meeting note implies that parameterized aliases won't appear in 1.19.

Certainly out for 1.19.

Implementing this particular feature may require an overhaul of how the type-checker deals with aliases in general, which has some non-trivial API implications that will affect tools. We may or may not get to it for 1.20, but marked for 1.20 for now.

The 1.19 dev cycle was barely 1.5 months, with additional (dev-unrelated) interruptions. We still have various loose ends related to generics and some technical debt that we're cleaning out. Before we add more new features, we need to further consolidate what we have.

This could be extremely useful for signatures like type Type[A] = GenericType[A,*A] which you need for methods defined on pointer receivers.

What are the chances this will make it into Go1.21?

@stephenafamo We have been concentrating on improved type inference this dev. cycle. At the moment improved aliases seem somewhat unlikely to happen for 1.21.

One case to consider: would this be legal or not?

type M[K comparable, V any] struct{ ... }
type A[K comparable, V any] = M[K, V]
func (A) Get(K) V { ... }

The spec says "the method receivers must declare the same number of type parameters as present in the generic type definition".

@alandonovan I think you mean func (A[K, V]) Get...?

I'd lean towards not allowing that. I argued for allowing type aliases in receiver parameters originally, but now I mostly regret that from an implementation complexity point of view

@adonovan I've been thinking about that, too. I'm not sure yet what the right approach is. Consider situations where the alias type has fewer type parameters then the aliased type. And what it we allow more type parameters in the alias than in the aliased type. There are lots of open questions here.

Change https://go.dev/cl/521956 mentions this issue: go/types, types2: introduce _Alias type node

We've been talking about this on the tools team, and think it would be good to land the support for Alias nodes in go/types one release before supporting parameterized aliases. This will give tools time to opt-in and handle the new node type (a potentially large change), before also supporting parameterized aliases (a likely smaller change). So we could land the new go/types node in 1.22, and parameterized aliases in 1.23.

Any chance of it going into 1.22 behind a GOEXPERIMENT or a GODEBUG?

This will be part of 1.23. We won't get this done for 1.22.

Is this still planned for Go 1.23? It seems now that Go 1.22 is out, having it available for experimentation early in the cycle would be good.

Yes it is. Some initial work has been done or is in progress. Some of us will be on vacation, too. So it may take some time before anything will be available. Thanks.

Can we expect generic type parameter aliasing in Go 1.23 release?
I wish I could alias generic types like:

type Blueprint[K comparable, V any] struct {
	items map[K]V
}
type Dictionary[K comparable, V any] = Blueprint[K, V]

golang generic type cannot be alias

Change https://go.dev/cl/566856 mentions this issue: go/types, types2: initial support for parameterized type aliases

Change https://go.dev/cl/567617 mentions this issue: types2: steps towards instantiation of generic alias types

Update: we have decided to delay parameterized aliases to Go 1.24.

This allows tools more time to support the new go/types.Alias node added in #63223. Specifically, we believe there will be less breakage for users if we set GODEBUG=gotypesalias=1 by default for 1.23, but do not allow type parameters on aliases until 1.24. That way, if tools break due to the new Alias representation, they have the option to set GODEBUG=gotypesalias=0 while working on support for the new node.

In the meantime, we believe it makes sense to go forward with the Alias API changes proposed in #67143 for 1.23, even though type parameters won't be allowed during type checking. This lets work commence on parameterized alias support in tools.

We plan to land parameterized alias support early in the 1.24 development cycle.

Change https://go.dev/cl/586955 mentions this issue: internal/goexperiment: add aliastypeparams GOEXPERIMENT flag

Change https://go.dev/cl/586956 mentions this issue: go/types, types2: use GOEXPERIMENT to enable alias type parameters

The following code snippet causes the Go compiler to panic (https://go.dev/play/p/PBvqgMjEs4S?v=gotip)

package main

type Seq[V any] = func(yield func(V) bool)

func f[E any](seq Seq[E]) {
	return
}

func main() {
	f(Seq[int](nil))
}

Thanks, @gazerro for reporting this. I filed #68054.

@griesemer Thanks, I wasn't sure if it was more appropriate to write the issue here or to create a new one.

@gazerro As this is an accepted proposal that's already implemented (as a GOEXPERIMENT), a separate issue makes it easier to track it, as it's a bug that needs to be fixed. But reporting in the first place is great, so thanks.

Change https://go.dev/cl/593715 mentions this issue: cmd/compile: support generic alias type

Change https://go.dev/cl/593797 mentions this issue: [release-branch.go1.23] cmd/compile: support generic alias type

bobg commented

For the record, I think it's a mistake to publish iter before generic type aliases are ready.

iter.Seq and iter.Seq2 really want to be aliases. Please see @dominikh's comment here, and @adonovan's followup observation that they cannot later be alias-ified.

There's also the issue I commented on here, which applies to iter.Pull (and iter.Pull2) in addition to the slices.Collect example I gave. It too arises from Seq and Seq2 being distinct named types instead of aliases.

This issue is currently labeled as early-in-cycle for Go 1.24.
That time is now, so a friendly reminder to look at it again.

Change https://go.dev/cl/601115 mentions this issue: types2, go/types: fix instantiation of named type with generic alias

Change https://go.dev/cl/601235 mentions this issue: go/types/objectpath: support parameterized type aliases

Change https://go.dev/cl/601116 mentions this issue: [release-branch.go1.23] types2, go/types: fix instantiation of named type with generic alias

Change https://go.dev/cl/603935 mentions this issue: x/tools: updates for parameterized type aliases

Change https://go.dev/cl/604615 mentions this issue: go/types: remove GOEXPERIMENT=aliastypeparams

Change https://go.dev/cl/614638 mentions this issue: internal/aliases: remove Alias and Unalias