Reputation: 3288
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]
Upvotes: 17
Views: 22534
Reputation: 45162
If you come across this Q&A because of its generic title (pun not intended), here's a quick primer about unions:
T
will be restricted to the types in the union~
For example:
type intOrString interface {
int | string
}
func Foo[T intOrString](x T) {
// x can be int or string
}
Now onto the OP's question, with some more details:
By including a type set, intOrString
becomes 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 use it 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]
}
This also means you can't use the constraint to instantiate a concrete type as your test slice:
// bad: using intOrString to instantiate a parametrized type
[]testDifference[intOrString]
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 with a union constraint, its concrete instantiations are not the same type. See also this for further details.
If you need a slice holding different types, your only option is []interface{}
(or []any
) ...or, you just separate the slices:
ttInts := []testDifference[int]{ testDifference[int]{...}, /* etc. */ }
ttStrs := []testDifference[string]{ testDifference[string]{...}, /* etc. */ }
Only the 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.
In case of a constraint like int | string
, what are the operations permitted on either int
or string
? In short:
var foo T
)T(x)
and x.(T)
, when appropriate==
, !=
)<
, <=
, >
and >=
)+
operatorSo 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
}
Upvotes: 33
Reputation: 1528
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]
Upvotes: 1