Search code examples
gogenerics

Recursive type constraint using a defined type rather than a type literal?


In Go2 generics, as of current draft, I can specify a type constraint on a generic type using an interface.

import "fmt"

type Stringer interface {
    String() string
}

func Print[T Stringer](value T) {
    fmt.Println(value.String())
}

This way, I can specify, that the type must implement a method. However, I don't see any way to force the implementation of a method, with itself having an argument of the generic type.

type Lesser interface {
    Less(rhs Lesser) bool
}

type Int int

func (lhs Int) Less(rhs Int) bool {
    return lhs < rhs
}

func IsLess[T Lesser](lhs, rhs T) bool {
    return lhs.Less(rhs)
}

func main() {
    IsLess[Int](Int(10), Int(20))
}

Exits with

Int does not satisfy Lesser: wrong method signature
    got  func (Int).Less(rhs Int) bool
    want func (Lesser).Less(rhs Lesser) bool

The original draft with contracts would make this possible, but not the new draft.

It can also be done with the following, but that would leave you with repeating the same constraint over and over again, braking DRY (and DRY code is the purpose of generics). It would also make the code much more unwieldy if the desired interfaces have multiple methods.

func IsLess[T interface { Less(T) bool }](lhs, rhs, T) bool {
    return lhs.Less(rhs)
}

Is there any way to do this with a predefined interface in the new draft?


Solution

  • Define interface type Lesser and function Isless as follows:

    type Lesser[T any] interface {
        Less(T) bool
    }
    
    func IsLess[T Lesser[T]](x, y T) bool {
        return x.Less(y)
    }
    

    Then, the following code compiles without mishap:

    type Apple int
    
    func (a Apple) Less(other Apple) bool {
        return a < other
    }
    
    type Orange int
    
    func (o Orange) Less(other Orange) bool {
        return o < other
    }
    
    func main() {
        fmt.Println(IsLess(Apple(10), Apple(20)))   // true
        fmt.Println(IsLess(Orange(30), Orange(15))) // false
    
        // fmt.Println(IsLess(10, 30))
        // compilation error: int does not implement Lesser[T] (missing method Less)
    
        // fmt.Println(IsLess(Apple(20), Orange(30)))
        // compilation error: type Orange of Orange(30) does not match inferred type Apple for T
    }
    

    (Playground)


    The constraint T Lesser[T] may be read as

    any type T that has a Less(T) bool method.

    Both of my custom types,

    • Apple with its Less(Apple) bool method, and
    • Orange with its Less(Orange) bool method,

    fulfil this requirement.

    For information, Java generics allow a similar trick via what is known as a recursive type bound. For more on this topic, see item 30 (esp. p137-8) in Josh Bloch's Effective Java, 3rd edition.


    Full disclosure: I was reminded of this unanswered question when I came across Vasko Zdravevski's solution to a similar problem on Gophers Slack.