Skip to content

Commit

Permalink
_content/blog: add blog post about generic alias types
Browse files Browse the repository at this point in the history
This is a 1.24 feature but we already support it as a
GOEXPERIMENT in 1.23. The sooner people play with it
and provide feedback, the smoother the rollout with
1.24.

Change-Id: I600410565dfbb259ecbf761c2b913135300a540b
Reviewed-on: https://go-review.googlesource.com/c/website/+/612575
Reviewed-by: Tim King <[email protected]>
Reviewed-by: Jonathan Amsterdam <[email protected]>
LUCI-TryBot-Result: Go LUCI <[email protected]>
Reviewed-by: Eli Bendersky <[email protected]>
Reviewed-by: Robert Griesemer <[email protected]>
  • Loading branch information
griesemer authored and Robert Griesemer committed Sep 17, 2024
1 parent 03edfca commit f3e0e9d
Showing 1 changed file with 366 additions and 0 deletions.
366 changes: 366 additions & 0 deletions _content/blog/alias-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,366 @@
---
title: What's in an (Alias) Name?
date: 2024-09-17
by:
- Robert Griesemer
tags:
- type aliases
- type parameters
- generics
summary: A description of generic alias types, a planned feature for Go 1.24
---

This post is about generic alias types, what they are, and why we need them.


## Background

Go was designed for programming at scale.
Programming at scale means dealing with large amounts of data, but
also large codebases, with many engineers working on those codebases
over long periods of time.

Go's organization of code into packages enables programming at scale
by splitting up large codebases into smaller, more manageable pieces,
often written by different people, and connected through public
APIs.
In Go, these APIs consist of the identifiers exported by a package:
the exported constants, types, variables, and functions.
This includes the exported fields of structs and methods of types.

As software projects evolve over time or requirements change,
the original organization of the code into packages may turn out to be
inadequate and require _refactoring_.
Refactoring may involve moving exported identifiers and their respective
declarations from an old package to a new package.
This also requires that any references to the moved declarations must be
updated so that they refer to the new location.
In large codebases it may be unpractical or infeasible to make such
a change atomically; or in other words, to do the move and update all clients
in a single change.
Instead, the change must happen incrementally: for instance, to "move"
a function `F`, we add its declaration in a new package without
deleting the original declaration in the old package.
This way, clients can be updated incrementally, over time.
Once all callers refer to `F` in the new package, the original declaration
of `F` may be safely deleted (unless it must be retained indefinitely, for
backward compatibility).
Russ Cox describes refactoring in detail in his 2016 article on
[Codebase Refactoring (with help from Go)](/talks/2016/refactor.article).

Moving a function `F` from one package to another while also retaining it
in the original package is easy: a wrapper function is all that's needed.
To move `F` from `pkg1` to `pkg2`, `pkg2` declares a new function `F`
(the wrapper function) with the same signature as `pkg1.F`, and `pkg2.F`
calls `pkg1.F`.
New callers may call `pkg2.F`, old callers may call `pkg1.F`, yet in both
cases the function eventually called is the same.

Moving constants is similarly straightforward.
Variables take a bit more work: one may have to introduce a pointer to the
original variable in the new package or perhaps use accessor functions.
This is less ideal, but at least it is workable.
The point here is that for constants, variables, and functions,
existing language features exist that permit incremental refactoring as
described above.

But what about moving a type?

In Go, the [(qualified) identifier](/ref/spec#Qualified_identifiers),
or just _name_ for short, determines the _identity_ of types:
a type `T` [defined](/ref/spec#Type_definitions) and exported by a package
`pkg1` is [different](/ref/spec#Type_identity) from an _otherwise identical_
type definition of a type `T` exported by a package `pkg2`.
This property complicates a move of `T` from one package to another while
retaining a copy of it in the original package.
For instance, a value of type `pkg2.T` is not [assignable](/ref/spec#Assignability)
to a variable of type `pkg1.T` because their type names and thus their
type identities are different.
During an incremental update phase, clients may have values and variables
of both types, even though the programmer's intent is for them to have the
same type.

To solve this problem, [Go 1.9](/doc/go1.9) introduced the notion of a
[_type alias_](/ref/spec#Alias_declarations).
A type alias provides a new name for an existing type without introducing
a new type that has a different identity.

In contrast to a regular [type definition](/ref/spec#Type_definitions)

```
type T T0
```

which declares a _new type_ that is never identical to the type on the right-hand side
of the declaration, an [alias declaration](/ref/spec#Alias_declarations)

```
type A = T // the "=" indicates an alias declaration
```
declares only a _new name_ `A` for the type on the right-hand side:
here, `A` and `T` denote the same and thus identical type `T`.

Alias declarations make it possible to provide a new name (in a new package!)
for a given type while retaining type identity:

```
package pkg2
import "path/to/pkg1"
type T = pkg1.T
```

The type name has changed from `pkg1.T` to `pkg2.T` but values
of type `pkg2.T` have the same type as variables of type `pkg1.T`.


## Generic alias types

[Go 1.18](/doc/go1.18) introduced generics.
Since that release, type definitions and function
declarations can be customized through type parameters.
For technical reasons, alias types didn't gain the same ability at that time.
Obviously, there were also no large codebases exporting generic
types and requiring refactoring.

Today, generics have been around for a couple of years, and large codebases
are making use of generic features.
Eventually the need will arise to refactor these codebases, and with that the
need to migrate generic types from one package to another.

To support incremental refactorings involving generic types, the future Go 1.24 release,
planned for early February 2025, will fully support type parameters on alias types
in accordance with proposal [#46477](/issue/46477).
The new syntax follows the same pattern as it does for type definitions and function declarations,
with an optional type parameter list following the identifier (the alias name) on the left-hand side.
Before this change one could only write:

```
type Alias = someType
```

but now we can also declare type parameters with the alias declaration:

```
type Alias[P1 C1, P2 C2] = someType
```

Consider the previous example, now with generic types.
The original package `pkg1` declared and exported a generic type `G` with a type parameter `P`
that is suitably constrained:

```
package pkg1
type Constraint someConstraint
type G[P Constraint] someType
```

If the need arises to provide access to the same type `G` from a new package `pkg2`,
a generic alias type is just the ticket [(playground)](/play/p/wKOf6NbVtdw?v=gotip):

```
package pkg2
import "path/to/pkg1"
type Constraint = pkg1.Constraint // pkg1.Constraint could also be used directly in G
type G[P Constraint] = pkg1.G[P]
```

Note that one **cannot** simply write

```
type G = pkg1.G
```

for a couple of reasons:

1) Per [existing spec rules](/ref/spec#Type_definitions), generic
types must be [instantiated](/ref/spec#Instantiations) when they
are _used_.
The right-hand side of the alias declaration uses the type `pkg1.G` and
therefore type arguments must be provided.
Not doing so would require an exception for this case, making the spec more
complicated.
It is not obvious that the minor convenience is worth the complication.

2) If the alias declaration doesn't need to declare its own type parameters and
instead simply "inherits" them from the aliased type `pkg1.G`, the declaration of
`A` provides no indication that it is a generic type.
Its type parameters and constraints would have to be retrieved from the declaration
of `pkg1.G` (which itself might be an alias).
Readability will suffer, yet readable code is one of the primary aims of the Go project.

Writing down an explicit type parameter list may seem like an unnecessary burden
at first, but it also provides additional flexibility.
For one, the number of type parameters declared by the alias type doesn't have to
match the number of type parameters of the aliased type.
Consider a generic map type:

```
type Map[K comparable, V any] mapImplementation
```

If uses of `Map` as sets are common, the alias

```
type Set[K comparable] = Map[K, bool]
```

might be useful [(playground)](/play/p/IxeUPGCztqf?v=gotip).
Because it is an alias, types such as `Set[int]` and `Map[int, bool]` are
identical.
This would not be the case if `Set` were a [defined](/ref/spec#Type_definitions)
(non-alias) type.

Furthermore, the type constraints of a generic alias type don't have to match the
constraints of the aliased type, they only have to
[satisfy](/ref/spec#Satisfying_a_type_constraint) them.
For instance, reusing the set example above, one could define
an `IntSet` as follows:

```
type integers interface{ ~int | ~int8 | ~int16 | ~int32 | ~int64 }
type IntSet[K integers] = Set[K]
```

This map can be instantiated with any key type that satisfies the `integers`
constraint [(playground)](/play/p/0f7hOAALaFb?v=gotip).
Because `integers` satisfies `comparable`, the type parameter `K` may be used
as type argument for the `K` parameter of `Set`, following the usual
instantiation rules.

Finally, because an alias may also denote a type literal, parameterized aliases
make it possible to create generic type literals
[(playground)](/play/p/wql3NJaUs0o?v=gotip):

```
type Point3D[E any] = struct{ x, y, z E }
```

To be clear, none of these examples are "special cases" or somehow require
additional rules in the spec. They follow directly from the application of
the existing rules put in place for generics. The only thing that changed in the
spec is the ability to declare type parameters in an alias declaration.


## An interlude about type names

Before the introduction of alias types, Go had only one form of type declarations:

```
type TypeName existingType
```

This declaration creates a new and different type from an existing type
and gives that new type a name.
It was natural to call such types _named types_ as they have a _type name_
in contrast to unnamed [type literals](/ref/spec#Types) such as
`struct{ x, y int }`.

With the introduction of alias types in Go 1.9 it became possible to give
a name (an alias) to type literals, too. For instance, consider:

```
type Point2D = struct{ x, y int }
```

Suddenly, the notion of a _named type_ describing something that is different from
a type literal didn't make that much sense anymore, since an alias name clearly is
a name for a type, and thus the denoted type (which might be a type literal, not a type name!)
arguably could be called a "named type".

Because (proper) named types have special properties (one can bind methods to them,
they follow different assignment rules, etc.), it seemed prudent to use a new
term in order to avoid confusions.
Thus, since Go 1.9, the spec calls the types formerly called named types _defined types_:
only defined types have properties (methods, assignability restrictions, etc) that are
tied to their names.
Defined types are introduced through type definitions, and alias types are
introduced through alias declarations.
In both cases, names are given to types.

The introduction of generics in Go 1.18 made things more complicated.
Type parameters are types, too, they have a name, and they share rules
with defined types.
For instance, like defined types, two differently named type parameters
denote different types.
In other words, type parameters are named types, and furthermore, they
behave similarly to Go's original named types in some ways.

To top things off, Go's predeclared types (`int`, `string` and so on)
can only be accessed through their names, and like defined types and
type parameters, are different if their names are different
(ignoring for a moment the `byte` and `rune` alias types).
The predeclared types truly are named types.

Therefore, with Go 1.18, the spec came full circle and formally
re-introduced the notion of a [named type](/ref/spec#Types) which now
comprises "predeclared types, defined types, and type parameters".
To correct for alias types denoting type literals the spec says:
"An alias denotes a named type if the type given in the alias declaration
is a named type."

Stepping back and outside the box of Go nomenclature for a moment, the correct
technical term for a named type in Go is probably
[_nominal type_](https://en.wikipedia.org/wiki/Nominal_type_system).
A nominal type's identity is explicitly tied to its name which is exactly what
Go's named types (now using the 1.18 terminology) are all about.
A nominal type's behavior is in contrast to a _structural type_ which has
behavior that only depends on its structure and not its name
(if it has one in the first place).
Putting it all together, Go's predeclared, defined, and type parameter types are
all nominal types, while Go's type literals and aliases denoting type literals
are structural types.
Both nominal and structural types can have names, but having a name
doesn't mean the type is nominal, it just means it is named.

None of this matters for day-to-day use of Go and in practice the details
can safely be ignored.
But precise terminology matters in the spec because it makes it easier
to describe the rules governing the language.
So should the spec change its terminology one more time?
It is probably not worth the churn: it is not just the spec that would need
to be updated, but also a lot of supporting documentation.
A fair number of books written on Go might become inaccurate.
Furthermore, "named", while less precise, is probably intuitively clearer
than "nominal" for most people.
It also matches the original terminology used in the spec, even if it now
requires an exception for alias types denoting type literals.


## Availability

Implementing generic type aliases has taken longer than expected:
the necessary changes required adding a new exported `Alias` type
to [`go/types`](/pkg/go/types) and then adding the ability to record type parameters
with that type.
On the compiler side, the analogous changes also required modifications to
the export data format, the file format that describes a package's
exports, which now needs to be able to describe type parameters for aliases.
The impact of these changes is not confined to the compiler, but affects
clients of `go/types` and thus many third-party packages.
This was very much a change affecting a large code base; to avoid
breaking things, an incremental roll-out over several releases was necessary.

After all this work, generic alias types will finally be available by default in Go 1.24.

To allow third-party clients to get their code ready, starting with
Go 1.23, support for generic type aliases can be enabled by setting
`GOEXPERIMENT=aliastypeparams` when invoking the `go` tool.
However, be aware that support for exported generic aliases is still
missing for that version.

Full support (including export) is implemented at tip, and the default
setting for `GOEXPERIMENT` will soon be switched so that generic type
aliases are enabled by default.
Thus, another option is to experiement with the latest version of Go
at tip.

As always, please let us know if you encounter any problems by filing an
[issue](/issue/new);
the better we test a new feature, the smoother the general roll-out.

Thanks and happy refactoring!

0 comments on commit f3e0e9d

Please sign in to comment.