kota's memex

type parameters

Type parameters are one or more name-type parings that look visually similar to our standard parameters; the only difference being type params are surrounded by square brackets, not parentheses. The square brackets, thankfully, are a consistent syntax you’ll see used in struct declarations and variable initialization.
[a, b constraint1, c constraint2]

We can now replace our strongly typed numeric like int32 or float64 with a far more permissible type parameter T.

func Max[T constraints.Ordered](x,_y_T) T {
    if x > y {
        return x
    }
    return y
}

When we call this function, we have to explicitly pass the type argument as part of the functions instantiation. Instatiation is a two part process where the compiler...

  1. Substitutes the type argument for all instances of the respective type parameter. In our case, the two T arguments and one return value are swapped to be int specifically.
  2. Checks that the two function arguments implement the constraints. The compiler will fail to instantiate if this step fails. Again, in our case, the compiler checks that 3 and 4 satisfy the Ordered constraint.

max := Max[int](3,4)

It’s also worth pointing our that the function call above both instantiates and runs the function. We could instantiate the function separately, which might be a slight optimisation in some cases.

maxInt := Max[int]
max := maxInt(3,4)

As for data structures, these type parameters work the same way. Types can optionally have a type parameter list, and methods of that type must declare matching type lists in the receiver.

type Grid[T any] struct {
    values        []T
    height, width int
}

func (g *Grid[T]) At(x, y int) T {
    return g.Values[(g.height * y) + x]
}

var grid Grid[int]

Notice the any keyword? It’s now an alias for interface{}!

type sets and constraints

Constraints are a new package in the standard library that describe type sets. Type sets are just lists of types which satisfy some target behavior. For example, the Signed constraint is the set of all signed integer types, and the Integer constraint is the union of Signed and Unsigned. To check if a type satisfies a constraint, the compiler just checks if that type is an element in the constraint’s type set.

At the time of writing this, there are only six, simple constraints: Signed, Unsigned, Integer, Float, Complex, and Ordered. Ordered is the most permissive and includes all floats, integers, and strings.

// Signed is a constraint that permits any signed integer type.
// If future releases of Go add new predeclared signed integer types,
// this constraint will be modified to include them.
type Signed interface {
        ~int | ~int8 | ~int16 | ~int32 | ~int64
}

// Integer is a constraint that permits any integer type.
// If future releases of Go add new predeclared integer types,
// this constraint will be modified to include them.
type Integer interface {
        Signed | Unsigned
}

These constraints are actually interfaces under the hood. Traditionally, interfaces have defined a ‘method set’ and every type which implements those methods implements that interface.

The other perspective, and one which is more relevant to generics, is that interfaces describe a set of types and the method set is only a means by which we filter the set of all types – the empty interface. It seems only reasonable then that we should be able add a specific type to that list directly.

In summary, type constraints are just interfaces and the types which satisfy those constraints are those enumerated by the interface. When you’re defining a generic function with a constraint, you’re basically defining a big list of all possible argument types.