gogenericsslicetype-parameter

What's the difference between a generic slice argument and an argument constrained to slice types?


Consider the experimental package slices. The package is experimental, so I understand the signatures may change; I'm using it to illustrate the issue.

Consider the signatures of two functions from this package, slices.Contains and slices.Grow:

The first argument to Contains has type []E (slice of Es) with E constrained by comparable (types that are comparable).

The first argument to Grow instead has type S (just S), with S constrained by ~[]E (types whose underlying type is a slice of E)

However it looks like there isn't any practical difference between what operations are allowed inside functions with such type params. If we declare some fake funcs with the same type parameters, we can see that both compile just fine:

As expected, in both functions we can len/cap, append, range, allocate with make, and index with [ ].

func fakeContains[E comparable](s []E, v E) {
    fmt.Println(len(s), cap(s))

    var e E
    fmt.Println(append(s, e))
    fmt.Println(make([]E, 4))

    for _, x := range s {
        fmt.Println(x)
    }
    fmt.Println(s[0])
    
    fmt.Println(reflect.TypeOf(s).Kind())
}

func fakeGrow[S ~[]E, E any](s S, n int) {
    fmt.Println(len(s), cap(s))

    var e E
    fmt.Println(append(s, e))
    fmt.Println(make(S, 4))

    for _, x := range s {
        fmt.Println(x)
    }
        fmt.Println(s[0])
    
    fmt.Println(reflect.TypeOf(s).Kind())
}

Even reflect.TypeOf(s).Kind() gives reflect.Slice in all cases.

The functions can also be tested with different types, and all compile:

// compiles just fine
func main() {
    type MyUint64 uint64
    type MyUint64Slice []uint64

    foo := []uint64{0, 1, 2}
    fakeContains(foo, 0)
    fakeGrow(foo, 5)

    bar := []MyUint64{3, 4, 5}
    fakeContains(bar, 0)
    fakeGrow(bar, 5)

    baz := MyUint64Slice{6, 7, 8}
    fakeContains(baz, 0)
    fakeGrow(baz, 5)
}

The only actual difference in my understanding is that in slices.Grow the argument s S is not a slice. It's just constrained to slice types. And as a matter of fact reflect.TypeOf(s) gives a different output when the arg is an instance of type MyUint64Slice []uint64:

However it's not immediately apparent to me what's the practical difference between the two.

Playground with the code: https://gotipplay.golang.org/p/zg2dGtSJwuI

Question

Are these two declarations equivalent in practice? If not, when should I choose one over the other?


Solution

  • It matters if you have to return a slice of the same (possibly named) type as the argument.

    If you do not have to return a slice (just some other info e.g. a bool to report if the value is contained), you do not need to use a type parameter that itself constraints to a slice, you may use a type parameter for the element only.

    If you have to return a slice of the same type as the input, you must use a type parameter that itself constraints to a slice (e.g. ~[]E).

    To demonstrate, let's see these 2 implementations of Grow():

    func Grow[S ~[]E, E any](s S, n int) S {
        return append(s, make(S, n)...)[:len(s)]
    }
    
    func Grow2[E any](s []E, n int) []E {
        return append(s, make([]E, n)...)[:len(s)]
    }
    

    If you pass a slice of a custom type having a slice as its underlying type, Grow() can return a value of the same type. Grow2() cannot: it can only return a value of an unnamed slice type: []E.

    And the demonstration:

    x := []int{1}
    
    x2 := Grow(x, 10)
    fmt.Printf("x2 %T len=%d cap=%d\n", x2, len(x2), cap(x2))
    
    x3 := Grow2(x, 10)
    fmt.Printf("x3 %T len=%d cap=%d\n", x3, len(x3), cap(x3))
    
    type ints []int
    y := ints{1}
    
    y2 := Grow(y, 10)
    fmt.Printf("y2 %T len=%d cap=%d\n", y2, len(y2), cap(y2))
    
    y3 := Grow2(y, 10)
    fmt.Printf("y3 %T len=%d cap=%d\n", y3, len(y3), cap(y3))
    

    Output (try it on the Go Playground):

    x2 []int len=1 cap=12
    x3 []int len=1 cap=12
    y2 main.ints len=1 cap=12
    y3 []int len=1 cap=12
    

    As you can see Grow2(y, 10) receives a value of type main.ints and yet it returns a value of type []int. This is not what we would want from it.