Description
Proposal
I propose to change the slices function signatures to always add an extra type parameter S ~[]E
, even when the slice type does not appear as a return type. When a function has multiple slice arguments, it should have multiple type parameters, for full generality.
Notably, this should happen before Go 1.21 is released, if at all, as I think it would be a breaking change otherwise.
Concretely, I propose to change these function signatures:
func BinarySearch[S ~[]E, E cmp.Ordered](x S, target E) (int, bool)
func BinarySearchFunc[S ~[]E, E, T any](x S, target T, cmp func(E, T) int) (int, bool)
func Compare[S1, S2 ~[]E, E cmp.Ordered](s1 S1, s2 S2) int
func CompareFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, cmp func(E1, E2) int) int
func Contains[S ~[]E, E comparable](s S, v E) bool
func ContainsFunc[S ~[]E, E any](s S, f func(E) bool) bool
func Equal[S1, S2 ~[]E, E comparable](s1 S1, s2 S2) bool
func EqualFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, eq func(E1, E2) bool) bool
func Index[S ~[]E, E comparable](s S, v E) int
func IndexFunc[S ~[]E, E any](s S, f func(E) bool) int
func IsSorted[S ~[]E, E cmp.Ordered](x S) bool
func IsSortedFunc[S ~[]E, E any](x S, cmp func(a, b E) int) bool
func Max[S ~[]E, E cmp.Ordered](x S) E
func MaxFunc[S ~[]E, E any](x S, cmp func(a, b E) int) E
func Min[S ~[]E, E cmp.Ordered](x S) E
func MinFunc[S ~[]E, E any](x S, cmp func(a, b E) int) E
func Reverse[S ~[]E, E any](s S)
func Sort[S ~[]E, E cmp.Ordered](x S)
func SortFunc[S ~[]E, E any](x S, cmp func(a, b E) int)
func SortStableFunc[S ~[]E, E any](x S, cmp func(a, b E) int)
Rationale
On Twitter, @joncalhoun asked why the proposal for slices.Reverse uses the ~[]E
trick. Though notably the commited version does not. That conversation prompted this proposal, that maybe it should.
The current approach is for the slices
package to use the extra S ~[]E
whenever the slice appears in a return type. This allows a statement like s = slices.Compact(s)
to work with defined slice types. Functions where it does not appear in a return type use []E
directly, as a value of a defined slice type is assignable to []E
. So slices.Reverse(s)
works fine, even if s
is a defined slice type.
However, there is still a case where it makes a difference: When instantiating slices.Reverse
you can only get a func([]int)
, not a func(MySliceType)
. This matters when passing them to higher level functions:
package main
func main() {
type NumberList []int
nl := NumberList{4, 5, 6}
ApplyAll(nl, ReverseV1) // Compiles fine
ApplyAll(nl, ReverseV2) // Does not compile
}
func ReverseV1[S ~[]E, E any](s S) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
func ReverseV2[E any](s []E) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
func ApplyAll[T any](v T, fs ...func(T)) {
for _, f := range fs {
f(v)
}
}
I'm not entirely certain the difference is important to cover, but I thought it's probably useful to talk about this while we could still change it (even though it's past the freeze).
Cost
The main cost of this proposal is visual clutter and complexity of the function signatures. When looking at them the first time, it might not be obvious if and why this is needed (as demonstrated by @joncalhoun asking that question, even though he is a Go veteran).
A secondary cost is that explicit instantiation of these functions becomes a tiny bit more cumbersome. Currently, you write slices.Reverse[T]
, in the future you might have to write slices.Reverse[[]T]
. The element type itself can always be infered, I think. But I might be missing a corner case, in which case the instantiation will be slices.Reverse[[]T, T]
. Either way, functions like Equal
would definitely become more cumbersome, with slices.Equal[T]
vs. slices.Equal[[]T, []T]
.
Notably #59338 specifically mitigated this second cost a lot, by at least applying better type-inference when passing them directly to higher-level functions. Further improvements to type-inference can reduce this cost further, by reducing the number of cases where explicit instantiation is necessary.
Alternatives considered
- Do nothing. This requires people who need higher-level functions with custom slice types to write wrappers in those cases. Wouldn't be the end of the world. I consider this a corner case anyways and keeping that corner case more cumbersome is maybe okay.
- Solve it in the future, with better type-inference. I don't think this works, as this is not about a failure of type-inference. A
func([]int)
is not afunc(IntSlice)
, no matter what we infer. That is, given that you can't instantiateslices.Reverse
to get afunc(IntSlice)
, inference obviously can't help you. - Solve it in the future with co/contravariance. If Go had co/contravariance for
func
, afunc([]int)
could be made assignable tofunc(IntSlice)
(asIntSlice
is assignable to[]int
(and vice-versa)). Type-inference could then solve whatever friction is left. Personally, I'd honestly prefer this, but I'm not optimistic it would ever happen. And even if, that seems like a far more significant change.