How to use generics for creating self-referring interfaces

Interfaces cannot declare functions that use the interface type itself in their signature. Until now. Thanks to generics.

An interface that does not work

A busy Tuesday afternoon in your office. Your current project is about to be turned into code, and you spotted a process that needs to clone data in one of its steps. This seems no problem, you just need to add a Clone method to your types, and a Cloner interface, and then you can create a method CloneAll(c ...Cloner) Cloner that clones any datatype that implmenets the Cloner() interface.

You start writing a first prototype, using a Cloner interface...

type Cloner interface {
    Clone() Cloner
}

...and two types that implement a Clone method:

type CloneableSlice []int

func (c CloneableSlice) Clone() CloneableSlice {
    res := make(CloneableSlice, len(c))
    copy(res, c)
    return res
}

type CloneableMap map[int]int

func (c CloneableMap) Clone() CloneableMap {
    res := make(CloneableMap, len(c))
    for k, v := range c {
        res[k] = v
    }
    return res
}

Then, for the process step, you write a CloneAny() function that takes a Cloner and returns a clone of it.

func CloneAny(c Cloner) Cloner {
    return c.Clone()
}

Looks good... except that it does not work:

./prog.go:34:23: cannot use s (variable of type CloneableSlice) as type Cloner in argument to CloneAny:
    CloneableSlice does not implement Cloner (wrong type for Clone method)
        have Clone() main.CloneableSlice
        want Clone() main.Cloner
./prog.go:37:23: cannot use m (variable of type CloneableMap) as type Cloner in argument to CloneAny:
    CloneableMap does not implement Cloner (wrong type for Clone method)
        have Clone() main.CloneableMap
        want Clone() main.Cloner  
        
Go build failed.

(Try for yourself in the playground).

The error message does have a point: The Clone() method signature is different between the Cloner interface and the actual implementations. The interface declares the method as Clone() Cloner, whereas CloneableSlice declares the method as Clone() CloneableSlice.

Well, no problem, why not change the signature to Clone() Cloner for all of them? Like, e.g.:

func (c CloneableSlice) Clone() Cloner {
    res := make(CloneableSlice, len(c))
    copy(res, c)
    return res
}

This actually compiles, but then the returned value is a Cloner interface. This means that CloneAny returns an opaque value that you know nothing more about than that it implements a Clone method. You'd have to run type assertions to get the actual type back and do some serious work with it.

cloned := CloneAny(s)
    if cs, ok := cloned.(CloneableSlice); ok {
        fmt.Println(cs[3])
    }

(Playground)

At this point, the code gets really ugly.

Generics to the rescue

Thanks to Go 1.18, this problem is a thing of the past. Generics help us to model the Cloner interface and the CloneAny method in a way that compiles fine and does what we need.

The changes are minimal but there is a small knack to apply. But first things first. Let's start with the interface. We can turn it into a parameterized interface trivially:

type Cloner[C any] interface {
    Clone() C
}

Now the Clone method returns a quite unspecified type C - basically, it can be anything. This does not seem to be what we want - actually we want a clone method of a given type to return exactly that type, and nothing else.

The solution to this appears immediately when we adjust our CloneAny() function.

First, we turn it into a generic function.

func CloneAny[T ...](c T) T {
    return c.Clone()
}

That's not quite enough. We need to find a suitable constraint for T so that CloneAny() can call Clone().

This...

func CloneAny[T Cloner](c T) T {
    return c.Clone()
}

...is quite close, but remember we have parameterized the Cloner interface, so we must write something like CloneAny[T Cloner[sometype]](c T) T. What is sometype here?

Well, the Clone() method shall return the same type that it belongs to, right? And it turns out we can express this in a quite straightforward manner:

func CloneAny[T Cloner[T]](c T) T {
    return c.Clone()
}

To some of you, [T Cloner[T]] might look like a recursive type parameter declaration (and indeed I had the same feeling about that for a second), but in fact this declaration is not recursive at all.(*)

Rather, the expression [T Cloner[T]] simply says,

A type T that must implement the Cloner interface for type T - that is, for itself.

So CloneableSlice must define a Clone method that returns a CloneableSlice, and CloneableMap must define a Clone method that returns a CloneableMap, and so forth.

Every type T that CloneAny accepts must implement a Clone Method that returns the same type, T.

And that's all there is to it. Now we can clone anything.

func main() {
    s := CloneableSlice{1, 2, 3, 4}
    fmt.Println(CloneAny(s))

    m := CloneableMap{1: 1, 2: 2, 3: 3, 4: 4}
    fmt.Println(CloneAny(m))
}

And we get:

[1 2 3 4]
map[1:1 2:2 3:3 4:4]

Run the complete code in the Playground.

Happy coding!

Related: proposal: Go 2: spec: add `self` type for use within interfaces · Issue #28254 · golang/go

(*) To be clear – technically, it is a recursive declaration. But the way to read and understand it is quite linear. You don't need to tune your brains into recursive thinking to understand that declaration.

Background image by TheresaMuth from Pixabay

Categories: : Generics