Search code examples
gogenericsmethods

Go: How to specify a type constraint in which a method's argument type is the same as that of the receiver


I would like to specify a type constraint like the following:

type Comparer interface {
    Compare(another Comparer) int
}

But I want the implementing type to pass in the concrete type of itself into the method Compare instead of the interface Comparer, like the following (I know the following doesn't implement Comparer):

func (a MyInt) Compare(b MyInt) int {
    xxxx
    return xxxx
}

I tried to use generic interface like this :

type Comparer[T any] interface {
    Compare(T) int
}

But this doesn't force the receiver of the method Compare to be also the type T.

Is there any way to force the receiver type and the argument type of method Compare to the be same?


Solution

  • When you talk about constraints, you are inherently referring to the specific usage of interface types as restrictions on type parameter sets.

    A type constraint is an interface that defines the set of permissible type arguments for the respective type parameter and controls the operations supported by values of that type parameter.

    Therefore when you (correctly) define your interface as:

    type Comparer[T any] interface {
        Compare(T) int
    }
    

    you are telling only half of the story. As a matter of fact, the above isn't a constraint. It's just an interface.

    In order to actually be a type constraint, that interface must be used as one.

    func Foo[T Comparer[T]](t1, t2 T) int {
        return t1.Compare(t2)
    }
    
    type Thing[T Comparer[T]] struct {
        Value T
    }
    

    And only there, in a type parameter list, you can mandate that the receiver of Compare(T) be T itself, by instantiating the constraint with its type parameter.

    When it is not used as a constraint, the interface is just a definition of a method set, which by design imposes no restrictions on which type can implement it.


    Now, it is possible to specify which types must implement a certain interface by using type terms. However type parameters can’t be used directly as type terms. You have to use an unnamed type, such as a pointer to T:

    type Comparer[T any] interface {
        *T
        Compare(T) int
    }
    

    Note that this forces you do declare the method on the pointer receiver such as *MyInt, which may or may not be desirable.

    Anyway, this can’t be instantiated with its own type parameter as T Comparer[T] because whatever T is, the constraint imposes an additional level of pointer indirection. The function arguments won’t ever satisfy it.

    The trick to make this work is to instantiate Comparer with a different type parameter.

    func test[T any, V Comparer[T]](a, b T) int {
        return V(&a).Compare(b)
    }
    

    And declare the method as:

    type MyInt int
    
    func (t *MyInt) Compare(other MyInt) int {
        // implementation
    }
    

    Though if you use interface constraints as they are intended, this convoluted workaround becomes simply unnecessary.

    Playground https://go.dev/play/p/fM5c_XYZqDZ