Home > Software engineering >  Go Generics - Unions
Go Generics - Unions

Time:10-31

I'm playing around with go generics by modifying a library I created for working with slices. I have a Difference function which accepts slices and returns a list of unique elements only found in one of the slices.

I modified the function to use generics and I'm trying to write unit tests with different types (e.g. strings and ints) but am having trouble with the union type. Here's what I have, now:

type testDifferenceInput[T comparable] [][]T
type testDifferenceOutput[T comparable] []T
type testDifference[T comparable] struct {
    input testDifferenceInput[T]
    output testDifferenceOutput[T]
}

func TestDifference(t *testing.T) {
        for i, tt := range []testDifference[int] {
            testDifference[int]{
                input: testDifferenceInput[int]{
                    []int{1, 2, 3, 3, 4},
                    []int{1, 2, 5},
                    []int{1, 3, 6},
                },
                output: []int{4, 5, 6},
            },
        } {
            t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
                actual := Difference(tt.input...)

                if !isEqual(actual, tt.output) {
                    t.Errorf("expected: %v %T, received: %v %T", tt.output, tt.output, actual, actual)
                }
        })
    }
}

I would like to be able to test both int's or string's in the same table test. Here's what I've tried:

type intOrString interface {
    int | string
}
type testDifferenceInput[T comparable] [][]T
type testDifferenceOutput[T comparable] []T
type testDifference[T comparable] struct {
    input testDifferenceInput[T]
    output testDifferenceOutput[T]
}

func TestDifference(t *testing.T) {
        for i, tt := range []testDifference[intOrString] {
            testDifference[int]{
                input: testDifferenceInput[int]{
                    []int{1, 2, 3, 3, 4},
                    []int{1, 2, 5},
                    []int{1, 3, 6},
                },
                output: []int{4, 5, 6},
            },
            testDifference[string]{
                input: testDifferenceInput[string]{
                    []string{"1", "2", "3", "3", "4"},
                    []string{"1", "2", "5"},
                    []string{"1", "3", "6"},
                },
                output: []string{"4", "5", "6"},
            },
        } {
            t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
                actual := Difference(tt.input...)

                if !isEqual(actual, tt.output) {
                    t.Errorf("expected: %v %T, received: %v %T", tt.output, tt.output, actual, actual)
                }
        })
    }
}

However, when running this, I get the following error:

$ go version
go version dev.go2go-55626ee50b linux/amd64

$ go tool go2go test
arrayOperations_unit_test.go2:142:6: expected ';', found '|' (and 5 more errors)

Why is it complaining about my intOrString interface?

EDIT #1 - I can confirm, with @Nulo's help, that gotip does work, and I now understand why I can't use intOrString as a type - it's supposed to be a constraint.

However, it would still be nice to find some way to mix ints and strings in my table test...

$ gotip version
go version devel go1.18-c812b97 Fri Oct 29 22:29:31 2021  0000 linux/amd64

$ gotip test
# github.com/adam-hanna/arrayOperations/go2 [github.com/adam-hanna/arrayOperations/go2.test]
./arrayOperations_unit_test.go:152:39: interface contains type constraints
./arrayOperations_unit_test.go:152:39: intOrString does not satisfy intOrString
./arrayOperations_unit_test.go:155:6: incompatible type: cannot use []int{…} (value of type []int) as []intOrString value
./arrayOperations_unit_test.go:156:6: incompatible type: cannot use []int{…} (value of type []int) as []intOrString value
./arrayOperations_unit_test.go:157:6: incompatible type: cannot use []int{…} (value of type []int) as []intOrString value
./arrayOperations_unit_test.go:159:13: incompatible type: cannot use []int{…} (value of type []int) as testDifferenceOutput[intOrString] value
./arrayOperations_unit_test.go:163:6: incompatible type: cannot use []string{…} (value of type []string) as []intOrString value
./arrayOperations_unit_test.go:164:6: incompatible type: cannot use []string{…} (value of type []string) as []intOrString value
./arrayOperations_unit_test.go:165:6: incompatible type: cannot use []string{…} (value of type []string) as []intOrString value
./arrayOperations_unit_test.go:167:13: incompatible type: cannot use []string{…} (value of type []string) as testDifferenceOutput[intOrString] value
./arrayOperations_unit_test.go:152:39: too many errors
FAIL    github.com/adam-hanna/arrayOperations/go2 [build failed]

CodePudding user response:

I've passed your code through gotip that uses a more evolved implementation of the proposal and it does not complain about that part of the code, so I would assume that the problem is with the go2go initial implementation.

Please note that your implementation will not work since you can definitely use parametric interfaces in type assertion expressions, but you can't use interfaces with type lists as you are doing in testDifference[intOrString]

CodePudding user response:

The Go2 playground and the go2go commands are out of date and are explicitly left behind, as the Go team is focusing on the actual implementation. When go2go doesn't work, you must use gotip.

using intOrString as a type

You can't do that because, by including a type set, it is an interface constraint, and using it as a type is explicitly not supported. Permitting constraints as ordinary interface types:

This is a feature we are not suggesting now, but could consider for later versions of the language.

So the first thing to do is to use intOrString as an actual constraint, hence in a type parameter list. Below I replace comparable with intOrString:

type testDifferenceInput[T intOrString] [][]T
type testDifferenceOutput[T intOrString] []T
type testDifference[T intOrString] struct {
    input testDifferenceInput[T]
    output testDifferenceOutput[T]
}

With that said, you understand why you can't use the constraint to instantiate a concrete type as your test slice like in []testDifference[intOrString].

constructing the test slice

The second problem you have is that the test slice contains two structs of unrelated types. One is testDifference[int] and one is testDifference[string]. Even though the type testDifference itself is parametrized, its concrete instantiations are not the same type. To make this clearer, just think about it without type params:

// bad
for i, tt := range [] /* what type to use here?? ?*/ {
    []int{1,2,3},
    []string{"a","b","c"},
}

The reason why the above fails even with type params is similar to what I explained here. If you need a slice holding different types, your only option is []interface{}.

...or, you just separate the slices:

ttInts := []testDifference[int]{
        testDifference[int]{
            input: testDifferenceInput[int]{
                []int{1, 2, 3, 3, 4},
                []int{1, 2, 5},
                []int{1, 3, 6},
            },
            output: []int{4, 5, 6},
        },
}
ttStrs := testDifference[string]{ /* etc. */ }

constraint with int | string type set

This is technically possible, but you must keep in mind that a generic type with a type set allows operations supported by all types in the type set. Operations based on type sets:

The rule is that a generic function may use a value whose type is a type parameter in any way that is permitted by every member of the type set of the parameter‘s constraint.

So what are the operations permitted on both int and string? In short:

  • var declaration (var foo T)
  • conversions and assertions T(x) and x.(T)
  • comparison (==, !=)
  • ordering (<, <=, > and >=)
  • the operator

So you can have an intOrString constraint, but the functions that make use of it, including your func Difference, are limited to those operations. For example:

type intOrString interface {
    int | string
}

func beforeIntOrString[T intOrString](a, b T) bool {
    return a < b
}

func sumIntOrString[T intOrString](a, b T) T {
    return a   b
}

func main() {
    fmt.Println(beforeIntOrString("foo", "bar")) // false
    fmt.Println(beforeIntOrString(4, 5)) // true

    fmt.Println(sumIntOrString("foo", "bar")) // foobar
    fmt.Println(sumIntOrString(10, 5)) // 15
}
  • Related