Constraining a type parameter to implement an interface via pointer receiver

1,565 views
Skip to first unread message

Steven Harris

unread,
Mar 28, 2022, 4:05:42 PM3/28/22
to golang-nuts
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?

Steven Harris

unread,
Mar 28, 2022, 4:29:16 PM3/28/22
to golang-nuts
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?

I realized that this desire may be addressed by the No way to express convertibility section of the Type Parameters Proposal document.

Steven Harris

unread,
Mar 28, 2022, 5:11:47 PM3/28/22
to golang-nuts
In parallel discussion in the "generics" channel of the "Gophers" Slack workspace, Roger Peppe was kind enough to point me to an example of the structural type constraint I was after.

Jason Phillips

unread,
Mar 29, 2022, 11:04:30 AM3/29/22
to golang-nuts
Note that the linked code operates on a copy of each DoImpl so mutations won't be visible outside of DoSlice(): https://go.dev/play/p/sgRDQMcLow8

You can fix that by indexing into the slice instead: https://go.dev/play/p/0eA_pQ12jzl

Reply all
Reply to author
Forward
0 new messages