Skip to content

proposal: Go 2: Const generics #65555

Closed as not planned
Closed as not planned
@dawidl022

Description

@dawidl022

Introduction

#44253 proposes to introduce generic parameterization of array sizes. The issue has been open for 3 years, with seemingly no recent progress.

The new proposal differs in that it introduces an explicit numerical const type parameter, and restricts the kinds of expressions that can be used to instantiate a const type parameter to existing constant expressions evaluating to non-negative integers, and lone const type parameters, in the same fashion as Rust.

Having this restriction in place limits the problems const generics can solve, but it also eliminates a lot of complexity associated with implementing const generics, which were discussed in the issue of the previous proposal.

If the community is happy to proceed with this proposal, I would like to have a go at implementing the feature. As such, any concerns regarding potential implementation difficulties would be appreciated!

Proposal

The basis of this proposal is to introduce a new set of types that can be used as type arguments. This new type set would be made up of constant non-negative integers. Since interfaces represent type sets in Go, we can (conceptually) define such a type as:

type const interface {
    0 | 1 | 2 | 3 | ...
}

where the ... denotes that the pattern continues for all non-negative integers supported by Go. The const interface can be used to constrain a type parameter, and since it is a non-basic interface it cannot be used as the type of a variable (we can already use const declarations to hold values, but they are broader than this proposed interface).

Array types accept two "type" parameters - the length and the element type. The constraint of the length parameter is exactly the const interface. This proposal generalises the const interface to be applicable to user-defined type parameters - those we know from generics.

Since all type parameters also implement their own constraints (i.e. a type parameter constrained by T can be used as a type argument constrained by the same type, or a supertype of T), it means that const type parameters can be used to instantiate other const type parameters, including array lengths.

Expressions involving type parameters, other than a lone const type parameter N, cannot be used as const type arguments. This approach is used by Rust and can make the implementation much more feasible. E.g. the following would not be allowed:

func foo[N const]() {
    // compile-time error: the expression `N + 1` cannot be used as a type argument
    foo[N + 1]()
}

In addition, despite const type parameters implementing const themselves, when used as a value, as opposed to a type parameter, they would evaluate to a non-constant int. E.g. the following would not be allowed:

func foo[N const]() {
    // compile-time error: type parameter N cannot be used as constant value
    const n = N
}

This is to avoid n being used as part of another constant expression that is then used as a const type argument, in effect enforcing the previous limitation of no "no complex generic expressions in const arguments".

The above restriction also resolves the issue that was heavily discussed in #44253 (comment) of exploiting const type parameters to perform complex compile-time computation, such as SAT solving.

Expressions that are formed from a const type parameter, would follow the same rule, i.e. evaluate to a non-constant value, even if their non-generic counterparts would be computable at compile time. E.g. len of a generic array would evaluate to a non-constant integer.

func foo[N const]() {
    // compile-time error: expression containing type parameter N cannot be used as constant value
    const n = len([N]int{})
}

The const interface would be a predeclared type.

Examples

One of the simplest useful examples is declaring a square matrix type:

type Matrix[T any, N const] [N][N]T

which can then be instantiated as follows:

myMatrix := Matrix[int, 2]{{1, 2}, {3, 4}}

Of course, const type parameters can also be used directly in functions:

func reversed[T any, N const](arr [N]T) [N]T {
    for i := 0; i < N/2; i++ {
        arr[i], arr[n-i-1] = arr[n-i-1], arr[i]
    }
    return arr
}

Who does this proposal help, and why?

This proposal is aimed at experienced Go users, who have justified use cases for using arrays over slices, and would enjoy the benefits of reduced code duplication with const generics, or would like to expose some generic functionality using underlying arrays as part of a library.

Language Spec Changes

Array types would also accept TypeNames, which (in my understanding) type parameters fall under.

ArrayLength = Expression | TypeName .

Types would also include constant expressions, more precisely constant expressions that evaluate to non-negative integers. They would be used to instantiate const type parameters.

Type = TypeName [ TypeArgs ] | TypeLit | Expression | "(" Type ")" .

Type constraints would accept the keyword const to denote the interface containing all non-negative integer types.

TypeConstraint = TypeElem | "const" .

Other considerations

Is this change backward compatible?

Yes, this change is backwards compatible. It is not possible to name a regular interface the keyword const, so no existing code would break.

Orthogonality: How does this change interact or overlap with existing features?

Explicit numerical type parameters would make generic arrays a first-class feature of Go, consistent with the rest of the language. All the existing compound data types in Go can already be fully type parameterised (slices: []T, maps: map[K]V and channels: chan T), except for arrays, so this feature would bridge that gap with [N]T.

Numerical type parameters would also become first class. Currently, arrays are the only types that accept a numerical type parameter, to parameterize the length of an array type. The const interface would allow any type or function to accept a constant integer (or another const-bounded type parameter) as a type argument.

Would this change make Go easier or harder to learn, and why?

Seeing that this is an additional language feature, it would make Go more difficult to learn simply because there is one feature more. The rest of the language will be unaffected in terms of easiness of learning.

Cost Description

  • What is the compile time cost?

The cost should be approximately the same as the existing cost of compiling generic code.

  • What is the run time cost?

This depends on how the feature will be implemented. Using a full monomorphisation model, there would be no runtime costs over using non-generic arrays. Using the GC shape stencilling approach would likely have the same runtime cost as existing generic code.

Prototype

I am working on a paper in the style of Featherweight Go, that formalises arrays and numerical type parameters that would be implemented as part of this proposal. It will come with prototype interpreters (with static type checking) for the new language feature in the style of https://github.com/rhu1/fgg (i.e. for a subset of Go). I hope to make this publicly available, sometime post-May this year.

Has this idea, or one like it, been proposed before?

In this new proposal, the const type parameter is explicit, which is consistent with the existing generics system in Go, and makes it immediately obvious to the programmer when a type is parameterised on an integer value.

In addition, this new proposal places restrictions on how const type parameters can be used, in particular when used as values they evaluate to non-constant integers, and cannot be part of a more "complex" expression used as a type argument. This means that the example presented in the previous proposal's design document cannot be implemented using const generics as presented in this new proposal. The feasibility of lifting some of the restrictions to make constructs as in the example possible is a topic for a potential future language extension.

Go Programming Experience

Experienced

Other Languages Experience

Rust, Kotlin, Java, TypeScript, Python

Related Idea

  • Has this idea, or one like it, been proposed before? Yes
  • Does this affect error handling? No
  • Is this about generics? Yes
  • Is this change backward compatible? Yes

Is this about generics?

Yes, this change fits into the existing generics de# Go by making the const constraint an interface type.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions