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 defined types, 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:
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. Either way, the T
refers to the name of a type parameter which is:
- constrained by
any
- in scope in that type parameter list
The key is that any
is used as a constraint here, which is satisfied by all types. 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. Just like T
can vary at each instantiation, so can S
. When T
is string
, ~[]T
is ~[]string
In the other case, the constraint ~[]any
doesn't refer to a type parameter. any
is used a static type in the type literal []any
. 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.
You can instantiate it also with []interface{}
because any
is a type alias of interface{}
.
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
.