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
intoShape[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