Interfaces cannot declare functions that use the interface type itself in their signature. Until now. Thanks to generics.
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])
}
At this point, the code gets really ugly.
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 theCloner
interface for typeT
- 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