Home > Software engineering >  Go generics: self-referring interface constraint
Go generics: self-referring interface constraint

Time:09-27

I have a couple custom types that I need to process in the same way. It seems like a perfect use for generics. In the process, I need to call methods on instances of the types, and those methods return different instances of the same types, and then I need to call methods on those returned instances, which I can't get to work. For the purpose of this question, I've fabricated a much simpler set of types and a process that exemplifies the problem I'm running in to.

Here's a working example without generics that shows the types (Circle and Square), and a process (.Bigger().Smaller()) I'll be trying to abstract into a generic function later (online demo):

package main

import (
    "fmt"
)

type Circle struct{ r float64 }

func NewCircle(r float64) *Circle  { return &Circle{r: r} }
func (c *Circle) Radius() float64  { return c.r }
func (c *Circle) Bigger() *Circle  { return &Circle{r: c.r   1} }
func (c *Circle) Smaller() *Circle { return &Circle{r: c.r - 1} }

type Square struct{ s float64 }

func NewSquare(s float64) *Square   { return &Square{s: s} }
func (s *Square) Side() float64     { return s.s }
func (s1 *Square) Bigger() *Square  { return &Square{s: s1.s   1} }
func (s1 *Square) Smaller() *Square { return &Square{s: s1.s - 1} }

func main() {
    fmt.Println(NewCircle(3).Bigger().Smaller().Radius()) // prints 3
    fmt.Println(NewSquare(6).Bigger().Smaller().Side())   // prints 6
}

The first thing I do to make a generic function is to define a type constraint:

type ShapeType interface {
    *Circle | *Square
}

I'll be passing a ShapeType to a process method, and I need to be able to call methods on the ShapeType instance, so I need to define another type constraint which specifies the methods that can be called on a ShapeType:

type Shape[ST ShapeType] interface {
    Bigger() ST
    Smaller() ST
}

With these, I can write a process method (online demo):

func process[ST ShapeType](s Shape[ST]) ST {
    return s.Bigger().Smaller()
}

This fails to compile however, as the return value of s.Bigger() is an ST, not a Shape[ST], so go doesn't know that it can then call Smaller() on the return value of s.Bigger(). In go's words:

s.Bigger().Smaller undefined (type ST has no field or method Smaller)

If Bigger() and Smaller() didn't return instances of their receiver types, I could write:

type Shape interface {
    *Circle | *Square
    Bigger()
    Smaller()
}

func process[S Shape](x S) S {
    x.Bigger().Smaller()
    return x // I guess we wouldn't even have to return x, but just for example's sake
}

Instead I would need to write:

type Shape interface {
    *Circle | *Square
    Bigger() Shape
    Smaller() Shape
}

and it appears go doesn't like self-referential type constraints.

If it were possible to assert/convert a concrete type to an interface it conforms to, then I could make it work, but it doesn't appear to be possible to do that (online demo):

func process[ST ShapeType](s Shape[ST]) ST {
    s1 := s.Bigger()
    s2 := s1.(Shape[ST]) // go is not happy here
    return s2.Smaller()
}

For this, go says:

cannot use type assertion on type parameter value s1 (variable of type ST constrained by ShapeType)

I don't know what else to try.

Is it possible to work with these kinds of types with generics? If so, how?

CodePudding user response:

Combine your two attempted interfaces together:

type Shape[ST any] interface {
    *Circle | *Square
    Bigger() ST
    Smaller() ST
}

And then instantiate the constraint of process with the type parameter itself:

func process[ST Shape[ST]](s ST) ST {
    return s.Bigger().Smaller()
}
  • Adding the union element *Circle | *Square into Shape[ST any] means that only those two types will be able to implement the interface
  • Then using the type parameter in the method signature, like Bigger() ST, means that whichever type is passed has a method that returns itself.

If you want to keep ShapeType as a separated interface, you can write Shape as:

type Shape[ST any] interface {
    ShapeType
    Bigger() ST
    Smaller() ST
}

You can also use process method with type inference, without any issue:

func main() {
    c1 := NewCircle(3)
    c2 := process(c1) 
    fmt.Println(c2.Radius()) // prints 3 as expected
    fmt.Printf("%T\n", c2) // *main.Circle

    s1 := NewSquare(6)
    s2 := process(s1)
    fmt.Println(s2.Side()) // prints 6 as expected
    fmt.Printf("%T\n", s2) // *main.Square
}

Final playground: https://go.dev/play/p/_mR4wkxXupH

  • Related