Created
June 23, 2023 08:20
-
-
Save rhomel/a124a8c695ead5fc43bee6d19b8abbe7 to your computer and use it in GitHub Desktop.
Go generics factory function
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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