Skip to content

slices: Use ~[]E throughout #60546

Closed
Closed
@Merovius

Description

@Merovius

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 a func(IntSlice), no matter what we infer. That is, given that you can't instantiate slices.Reverse to get a func(IntSlice), inference obviously can't help you.
  • Solve it in the future with co/contravariance. If Go had co/contravariance for func, a func([]int) could be made assignable to func(IntSlice) (as IntSlice 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions