Home > Net >  Generics mechanism for interface types
Generics mechanism for interface types

Time:06-17

Below is a generic split function that splits a slice into several equal sized (except maybe the last partition) slices based on input size:

func split[S ~[]T, T any](slc S, size int) []S {
    slices := make([]S, 0, len(slc)/size 1)
    for len(slc) > 0 {
        if size > len(slc) {
            size = len(slc)
        }
        slices = append(slices, slc[:size])
        slc = slc[size:]
    }
    return slices
}

The generic type parameter S has type ~[]T where T is any. This works as expected.

The ~ is required to handle type aliases, for example:

type X []string

Without the ~ in S ~T[], split wouldn't work on an argument of type X (it would still work on a []string).

Then there is this other splitAny function:

func splitAny[S ~[]any](slc S, size int) []S {
    slices := make([]S, 0, len(slc)/size 1)
    for len(slc) > 0 {
        if size > len(slc) {
            size = len(slc)
        }
        slices = append(slices, slc[:size])
        slc = slc[size:]
    }
    return slices
}

This function works on a []interface{} or anything that is type []interface{}.

My question is, what exactly is the mechanism that the compiler is taking here to produce type safe code? Why are the two split function not equivalent. More generally, why can't splitAny work with, for example, a []string?

CodePudding user response:

In split() the type parameter T is the element type of the slice, so it accepts slices of varying element types, such as []string, []int etc.

In splitAny() the type parameter S is not the element type but the slice type itself! Which means the type of the slice passed can be []any or a type that has this as its underlying type, but the element type must be any (which is an alias to interface{}), and it can't be anything else, it can't be string for example, so the slice type used for S can't be []string.

CodePudding user response:

tl;dr — the constraint ~[]T refers to a type parameter, the constraint ~[]any refers to the static type any.


The syntax S ~[]T is a shorthand notation for S interface { ~[]T }, where the constraint is an anonymous interface type. In both cases T refers to the name of a type parameter which is:

  • constrained by any
  • in scope in that type parameter list

If you used a named constraint instead, that would be a parametrized interface as:

type Foo[T any] interface {
    ~[]T
}

and you would rewrite the function signature as:

func split[S Foo[T], T any](slc S, size int) []S {}

This perhaps makes it easier to see that the T in ~[]T is itself a type parameter, therefore will be instantated with whatever T is. Just like T can vary at each instantiation, so can ~[]T. When T is string, ~[]T is ~[]string

In the other case, the constraint ~[]any doesn't refer to a type parameter. If you used a named constraint instead it would be:

// no type parameter needed
type Bar interface {
    ~[]any
    // same as ~[]interface{}
}

And you would rewrite the function signature as:

func splitAny[S Bar](slc S, size int) []S {}

In this latter case, the constraint isn't parametrized and clearly can only be instantiated with types with underlying type []any and nothing else.

  • Related