I wanted to have a function that could construct a value satisfying an interface, do some setup on that value, and then pass the constructed value to some other operation. Schematically:
func doAThing(fn func(*Input) (Output, error)) (Output, error) {
in := new(Input)
in.Setup()
return fn(in)
}
This works, fine, but now supposing there are multiple input and output types, I'd like to make this generic. Unfortunately, this doesn't quite work as you would hope:
// Does not work: in.Setup undefined (type *Input is pointer to type parameter, not type parameter)
func doAThingGeneric[Input Setuper, Output any](fn func(*Input) (Output, error)) (Output, error) {
in := new(Input)
in.Setup()
return fn(in)
}
The problem here is that it's the pointer to the input that implements the interface we want.
This turns out to be a pretty common problem, and there are a lot of complicated examples showing how to work around it in various cases. But fundamentally, the problem is we need to be able to talk both about the Input
type itself (so that we can construct a new one) and also the pointer to Input
(so that we can dispatch via the interface). To make this work, we have to parameterize both of these types.
Specifically, we want to say:
Input
can be any type, provided a*Input
implements theSetuper
interface.
In terms of type constraints, we can write the second half of that constraint like this:
// setupPtr[X] is any pointer type that implements the Setuper interface.
type setupPtr[X any] interface {
*X
Setuper
}
Now that we have this, we can provide a third type parameter with this constraint. Note that when we do so, we also have to change the argument to fn
, which wants the pointer as its input (written as *Input
in the original concrete-type example):
func doAThingGeneric[Input any, IP setupPtr[Input], Output any](fn func(IP) (Output, error)) (Output, error)
// ^^^^^ ^^--- pointer to base type, implements Setuper
// \----------- base type, for calling new
We must also change the body slightly too: We still call new(Input)
to allocate the argument, but we need to explicitly store that in a variable of type *Input
(a.k.a., IP
):
var in IP = new(Input)
in.Setup()
return fn(in)
Now we can actually call it:
type T struct{}
func (*T) Setup() {}
doAThingGeneric(func(v *T) (int, error) {
return 0, nil
})
This will correctly unify T
with Input
, *T
with IP
, and int
with Output
, and generate code.
Complete example here: https://go.dev/play/p/EomUVlApcNW