Check interface implementation on generic type

Hello

I have a function that should be comparing two generic types. If they have Equal implemented, this should be preferred. Here is my code:

type EqualChecker interface {
	Equal(EqualChecker) bool
}

//AssertEq adds error to test with stack trace when provided arguments are not equal
//If values of Pointers should be compared use AssertEqPtr
func AssertEq[C comparable](left C, right C, t *testing.T) {
	leftE, ok :=  left.(EqualChecker)
	if ok {
		rightE, _ := right.(EqualChecker)
		if !leftE.Equal(rightE) {
			t.Errorf("Not equal: \n left: %+v\nright: %+v\n%s", left, right, debug.Stack())
		}
		return
	}
	if left != right {
		t.Errorf("Not equal: \n left: %+v\nright: %+v\n%s", left, right, debug.Stack())
	}
}

But I get the compilation error:

  • invalid operation: cannot use type assertion on type parameter value left (variable of type C constrained by comparable)

Any idea how to solve this?

I don’t know anything about generics, but type assertions only work when the expression is an interface type. For example, you can’t do:

var a string = "test"
b := a.(interface{})

Because a is a concrete string type. Perhaps the issue is that there’s nothing in the type constraint to ensure C is an interface type. Again, I don’t know anything about generics, so I’m not sure how to fix it. Is there a constraint to require that a type be an interface type?

The only requirement for AssertEq is that both parameters implement the EqualChecker interface.

No generics needed.

(See below)

1 Like

Ok, my answer above was much too fast and posted without thinking deeper. Please ignore.

Here is a better solution that (a) works and (b) does make use of type parameters a.k.a. generics.

Playground

The code explained:

  1. Create a parametric interface, so that we can define a function Equal(T) without having to name its parameter.
type Equaler[T any] interface {
	Equal(T) bool
}
  1. Function AssertEq gets parametrized as AssertEq[T Equaler[T]], because:
    1. Type constraints are interfaces, hence we can use Equaler here, and
    2. The Equaler interface itself is a parametrized interface, hence we use Equaler[T] here.
func AssertEq[T Equaler[T]](left, right T, t *testing.T) {
	if !left.Equal(right) {
		t.Errorf("Not equal:\nleft: %+v\nright: %+v\n", left, right)
	}
	return
}
  1. The example struct type AmIEqual implements interface Equaler[T] by defining a method Equal().
type AmIEqual struct {
	value int
}

func (e AmIEqual) Equal(other AmIEqual) bool {
	return e.value == other.value
}
  1. Now we can instantiate AmIEqual values and pass them to AssertEq().
    Thanks to type inference, there is no need to call AssertEq like AssertEq[AmIEqual](a, b, t), although the latter would work, too.
func TestAssert(t *testing.T) {
   a := AmIEqual{value: 1}
   b := AmIEqual{value: 1}
   AssertEq[AmIEqual](a, b, t) // PASS

   // comment the two lines below to get a PASS
   c := AmIEqual{value: 2}
   AssertEq(a, c, t) // FAIL
}

Thanks for your very good reply but sadly your solution is missing my main point.
I explicitly want to accept any type that is comparable. If in addition the Equal method is implemented, I’d like to prefer that one.
Example:

  • I want to be able to just pass two int types and compare them with == or !=
  • If I pass a Time for example, I want to check if the dates are equal independent of the timezone. So I need to use .Equal there instead of == or !=

Your solution works if I want to restrict use of function to just types that have Equal implemented.

If the type is not that hard limited you could ask what generic types are used for in this example. The reason is, that the function only makes sence if both passed arguments have the same type and are comparable in some way

Indeed I missed that part.

I briefly thought of using a union like so:

func AssertEq[T Equaler[T] | comparable](left, right T, t *testing.T) {

However, an interface that declares a method cannot be part of a union. So this does not work.

If no other solution pops up, I guess you might need to use reflection as a last resort. (reflect.DeepEqual comes to mind.)

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.