I am working with a family of types that integrate with an existing Go project that follow a couple of patterns, and I'd like to manipulate them uniformly by introducing a generic function. My example uses a contrived, whittled-down set of struct fields and interfaces to demonstrate the challenge.
Here's all the code together in the Go Playground.
Suppose there's an interface called Object. I'm not responsible for the name or the idea; it comes from a public project.
type Object interface {
GetKind() string
GetName() string
}
I have a couple of types (named X and Y here) that both implement this interface via pointer receiver. The project has many integration points that expect to manipulate pointers to structs, so even though I could implement these methods by value, adhering to this project's conventions has me writing these methods with pointer receivers. Each has a companion type named with the suffix List that contains a slice of values of the basic type.
type X struct {
Name string
}
func (*X) GetKind() string {
return "X"
}
func (x *X) GetName() string {
return x.Name
}
type XList struct {
Items []X
}
func showXs(xs XList) {
fmt.Println("Items:")
for i := range xs.Items {
o := &xs.Items[i]
fmt.Printf("- Item %d of kind %s (type %T): %q\n", i, o.GetKind(), xs.Items[i], o.GetName())
}
}
type Y struct {
Name string
}
func (*Y) GetKind() string {
return "Y"
}
func (y *Y) GetName() string {
return y.Name
}
type YList struct {
Items []Y
}
func showYs(ys YList) {
fmt.Println("Items:")
for i := range ys.Items {
o := &ys.Items[i]
fmt.Printf("- Item %d of kind %s (type %T): %q\n", i, o.GetKind(), ys.Items[i], o.GetName())
}
}
Observe that the showXs and showYs functions are nearly identical. I'd like to write one function that could consume an XList or a YList—or any other similar type. At least as of Go 1.18, I can't constrain a type parameter structurally by requiring that it have an "Items" field, so I introduce an interface with a method to retrieve the items.
type HasItems[E any] interface {
GetItems() []E
}
Now, I adapt XList to play along with this interface.
type AugmentedXList struct {
*XList
}
func (l AugmentedXList) GetItems() []X {
return l.Items
}
Now, when I attempt to write a generic show function that consumes a HasItems and uses each item as an Object, I run into this challenge: How do I express that some item type E implements the Object interface, not directly by by pointer receiver?
I wrote this show function using a projection function callback, converting from *E to Object:
func show[E any](is HasItems[E], project func(*E) Object) {
fmt.Println("Items:")
items := is.GetItems()
for i := range items {
o := project(&items[i])
fmt.Printf("- Item %d of kind %s (type %T): %q\n", i, o.GetKind(), items[i], o.GetName())
}
}
Finally, here's a sample invocation of the showXs, showYs, and show functions:
func main() {
xs := XList{
Items: []X{
X{"one"},
X{"two"},
},
}
showXs(xs)
ys := YList{
Items: []Y{
Y{"three"},
Y{"four"},
},
}
showYs(ys)
show[X](AugmentedXList{&xs}, func(x *X) Object { return x })
}
Note that the function I'm passing to the show function is the identity projection. That is, a *X is an Object.
Should it be possible to write a type constraint that would allow me to eliminate this projection function parameter, and instead have the compiler mandate and ensure that for a given type E, the related type *E implements Object?