Skip to content

Instantly share code, notes, and snippets.

@rhomel
Created June 23, 2023 08:20
Show Gist options
  • Save rhomel/a124a8c695ead5fc43bee6d19b8abbe7 to your computer and use it in GitHub Desktop.
Save rhomel/a124a8c695ead5fc43bee6d19b8abbe7 to your computer and use it in GitHub Desktop.
Go generics factory function
package main
// My notes on trying to comprehend Go's generics.
// https://github.com/golang/proposal/blob/master/design/43651-type-parameters.md#pointer-method-example
// A trivial example to show how to make factory functions that can produce
// typed things.
import "fmt"
type gizmo struct {
class string
}
func (g *gizmo) SetClass(class string) {
g.class = class
}
// A type constraint that allows us to reference pointers to any type "G" and
// the interface methods that "G"'s pointer recieve must implement. This has
// the effect of making these methods accessible within a generic func.
type typeConstraint[G any] interface {
*G
// Optionally define methods that should be available inside of the factory
// function. But if you had no methods here then you wouldn't need the
// generic function anyway because the factory function cannot access the
// fields on generic types. So lets set the class property to demonstrate
// some actual use-case
SetClass(string)
}
// makeGremlin creates the type of gremlin passed with the class field set to
// "gremlin". In this case the only thing that is required of a "gremlin" is
// that it's pointer reciever implements the SetClass method defined in the
// typeConstraint interface.
func makeGremlin[StructType any, PointerToStructType typeConstraint[StructType]]() PointerToStructType {
var It *StructType = new(StructType)
var pointerIt PointerToStructType = PointerToStructType(It)
pointerIt.SetClass("gremlin")
return pointerIt
}
// How to parse the generic:
//
// Suppose we want to make a call such as:
//
// g := makeGremlin[gizmo]()
//
// Starting with the generic function signature:
//
// func makeGremlin[StructType any, PointerToStructType gremlinConstraint[StructType]]() PointerToStructType
//
// 1. Replace `StructType` with `gizmo`:
// func makeGremlin[gizmo, PointerToStructType gremlinConstraint[gizmo]]() PointerToStructType
//
// 2. Evaluate the `type constraint` on PointerToStructType:
// Given:
// gremlineContraint[gizmo]
// And:
// type gremlinConstraint[G any] interface { *G }
// Replace G with `gizmo` to narrow the constraint:
// type gremlinConstraint interface { *gizmo }
//
// 3. Replace the constraints with the narrowed parameter type:
// func makeGremlin[StructType gizmo, PointerToStructType *gizmo]() PointerToStructType
//
// 4. A weird (but incorrect) way of now seeing the function body: replace the
// generic types with the narrowed contrain types:
// var It *gizmo = new(gizmo)
// var pointerIt *gizmo = *gizmo(It)
// pointerIt.SetClass("gremlin")
// return pointerIt
//
// 5. You may be wondering why we couldn't call the SetClass method on `It`.
// Well actually it is because Go does not actually do this. It still sees the
// types literally as they were written: *StructType and PointerToStructType.
// So `It.SetClass` fails because `func (*StructType) SetClass()` does not exist.
//
// Another way to think of it is the type constraint gives us a way
// to verify any type that is passed through generics satisfies these
// constraints (like method interfaces). Then within the generic function we
// only have access to the methods we defined in the type constraint interface.
//
// From that view Go allows us to take a *StructType as the same as
// PointerToStructType because PointerToStructType has *another* generic
// type constraint. PointerToStructType's type constraint takes any type as
// long as its pointer receiver implements the typeConstraint interface.
// [gizmo, *gizmo] satisfies these contraints:
// - `gizmo` is valid because StructType's contraint allows `any` type
// - `*gizmo` is valid because PointerToStructType's type constraint (an
// interface) requires StructType (in this case gizmo) to have a pointer
// receiver that implements SetClass: `func (*gizmo) SetClass` so it
// satisfies the typeConstraint interface.
//
// So now if we review the function body again:
//
// Create a new StructType on the heap and give me the pointer to it:
// var It *StructType = new(StructType)
//
// Cast *StructType to PointerToStructType; an alias to *StructType based on
// its type constraint, but the type constraint also guarantees that
// PointerToStructType implements the SetClass method.
// var pointerIt PointerToStructType = PointerToStructType(It)
//
// Now call PointerToStructType's interface method SetClass on `It`.
func main() {
// Go uses the recusrive inference rules to infer the second type:
// makeGremlin[gizmo, *gizmo]()
g := makeGremlin[gizmo]()
// g is a *gizmo type
fmt.Printf("%#v\n", g) // &main.gizmo{class:gremlin}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment