Skip to content

Instantly share code, notes, and snippets.

@creachadair
Last active July 26, 2023 14:00
Show Gist options
  • Save creachadair/aceea50475320a035971c0dc8c1375f3 to your computer and use it in GitHub Desktop.
Save creachadair/aceea50475320a035971c0dc8c1375f3 to your computer and use it in GitHub Desktop.
Constructing pointers to type parameters in Go

Constructing Pointers to Type Parameters

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 the Setuper 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment