Last active
October 10, 2024 02:32
-
-
Save jackstine/7e57049b4c87cbc7d72793bc438cbe97 to your computer and use it in GitHub Desktop.
Golang Generics Explained
This file contains hidden or 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 | |
| import ( | |
| "cmp" | |
| "errors" | |
| "fmt" | |
| "math" | |
| "sort" | |
| "sync" | |
| ) | |
| // a Generic is a programming concept where algorithms, functions, procedures, and data structures | |
| // are designed to work with many types for the same parameters, and fields. This allows for DRY (dont repeat yourself) | |
| // principles. | |
| // PlusOperator will show how the primitive types in golang can all use the + operator. | |
| func PlusOperator() { | |
| // these three functions will produce the following output. | |
| // 3 | |
| // string1string2 | |
| // 3.1 | |
| // they all use ints, strings, and floats | |
| // they all have the ability to use the + operator. | |
| x := 1 | |
| y := 2 | |
| fmt.Println(x + y) | |
| string1 := "string1" | |
| string2 := "string2" | |
| fmt.Println(string1 + string2) | |
| f_x := 1.1 | |
| f_y := 2.0 | |
| fmt.Println(f_x + f_y) | |
| } | |
| // Add is the using the + operator but only for the int type | |
| // this does not include the int8, int16, int32, int64 types. | |
| func Add(a, b int) int { | |
| return a + b | |
| } | |
| // UsingAdd complains that we are not able to use the floats or the string values | |
| func UsingAdd() { | |
| fmt.Println(Add(5, 8)) | |
| // but we canot use floats, or strings with it. This is when Generics becomes a useful tool. | |
| // fmt.Println(Add(5.9, 8.9)) | |
| } | |
| // Generic use something called a type parameter, in the following [T cmp.Ordered] is a Type Parameter. | |
| // GenericAdd shows how we can create a Generic of type T, where T is | |
| // any type in cmp.Ordered IE: int, float, complex, or string | |
| // this allows for all types that support the < <= >= > operators | |
| // also allows for +, - operators as well. Does not support *,/,% and bitwise operators. | |
| // T here is the same as int Add(x, y int) int | |
| // we are always returning the value that was passed, so no need to change anything. | |
| func GenericAdd[T cmp.Ordered](x, y T) T { | |
| return x + y | |
| } | |
| // UsingGenericAdd is the same functionality of PlusOrator | |
| // it is just using the same generic method to make the compuations. | |
| func UsingGenericAdd() { | |
| // explicitly implying the type of the generic function | |
| fmt.Println(GenericAdd[int8](1, 2)) | |
| fmt.Println(GenericAdd[string]("string1", "string2")) | |
| fmt.Println(GenericAdd[float32](1.1, 2.0)) | |
| fmt.Println(GenericAdd[float64](1.1, 2.0)) | |
| // not explicitly implying the type, and leaving it up to the compiler | |
| fmt.Println(GenericAdd(1, 2)) | |
| fmt.Println(GenericAdd("string1", "string2")) | |
| fmt.Println(GenericAdd(1.1, 2.0)) | |
| } | |
| // Another type used for Generics in go is `comparable`, it is used for anything that is comparable, IE == or !=. | |
| // Equals is a genric function that uses the comparable types to tell us if the values are equal, or not. | |
| func Equals[T comparable](info string, x, y T) { | |
| fmt.Printf("%v:: %s :: type:[%T] values:[%v, %v]\n", x == y, info, x, x, y) | |
| } | |
| // this means some pretty strict guidelines for what is comparable | |
| // These are not things that can use the "+" operator like the cmp.Ordered type above. | |
| // for now we will introduce the idea of interfaces because they are a comparable type | |
| // here we have the Shape interface, it defines a method Area() float64 | |
| // this is called its method set (the set of all methods denoted to an interface) | |
| // any type that has this method set conforms to the interface. | |
| // Such as Circle and Square below, which are types of structs that implement the interface method set. | |
| type Shape interface { | |
| Area() float64 | |
| } | |
| type Circle struct { | |
| Radius float64 | |
| } | |
| func (c Circle) Area() float64 { | |
| return c.Radius * float64(math.Pi) * c.Radius | |
| } | |
| type Square struct { | |
| Side float64 | |
| } | |
| func (s Square) Area() float64 { | |
| return s.Side * s.Side | |
| } | |
| func UsingEquals() { | |
| // showing the explicit implementations for the generic types used | |
| Equals[bool]("booleans", true, false) | |
| Equals[int]("integers", 1, 1) | |
| Equals[float32]("float32", 1.1, 2.34) | |
| Equals[string]("strings", "this", "that") | |
| var f, g complex128 | |
| f = 1.3354124i | |
| g = 1.3354124i | |
| Equals[complex128]("complex128", g, f) | |
| // pointers | |
| Equals[*complex128]("pointers", &f, &f) | |
| c1 := make(chan string, 1) | |
| c2 := make(chan string, 1) | |
| Equals[chan string]("channels empty", c1, c2) | |
| wg := sync.WaitGroup{} | |
| wg.Add(1) | |
| // two channels without data | |
| go func() { | |
| c2 <- "a value" | |
| wg.Done() | |
| defer func() { close(c1); close(c2) }() | |
| }() | |
| wg.Wait() | |
| Equals[chan string]("channels with differnet data loaded", c1, c2) | |
| <-c2 | |
| c2 = c1 | |
| Equals[chan string]("channels equal eachother", c1, c2) | |
| // interface types | |
| var a, b Shape | |
| Equals[Shape]("interface types that equal nil", a, b) | |
| a = Circle{Radius: 1.5} | |
| b = Square{Side: 4} | |
| // here the interface type is Shape | |
| Equals[Shape]("interface types that are instantiated without the same value", a, b) | |
| // change the value of b so that it is of the same type of a and value | |
| b = Circle{Radius: 1.5} | |
| // structs of interface are comparable | |
| Equals[Shape]("interface types that are instantiated with the same values", a, b) | |
| // structs are comparable | |
| Equals[Circle]("circles", a.(Circle), b.(Circle)) | |
| b = Circle{Radius: 4} | |
| Equals[Circle]("circles but b is larger radius", a.(Circle), b.(Circle)) | |
| array1 := [3]int{1, 2, 3} | |
| array2 := [3]int{1, 2, 3} | |
| // arrays are comparable | |
| Equals[[3]int]("arrays with same info", array1, array2) | |
| array2 = [3]int{0, 0, 0} | |
| Equals[[3]int]("arrays without same info", array1, array2) | |
| // array of Interface values works as well | |
| arrayOfShapes1 := [3]Shape{Circle{1}, Square{1}, Circle{1}} | |
| arrayOfShapes2 := [3]Shape{Circle{1}, Square{1}, Circle{1}} | |
| Equals[[3]Shape]("arrays of shapes same info", arrayOfShapes1, arrayOfShapes2) | |
| arrayOfShapes2 = [3]Shape{Circle{4}, Square{4}, Circle{1}} | |
| Equals[[3]Shape]("arrays of shapes different info", arrayOfShapes1, arrayOfShapes2) | |
| // type paramaters that are strictly comparable. | |
| type MyBool bool | |
| Equals[MyBool]("MyBool Types", true, true) | |
| // A value x of non-interface type X and a value t of interface type T can be compared if type X is comparable and X implements T. | |
| // They are equal if t's dynamic type is identical to X and t's dynamic value is equal to x. | |
| // is an example of. | |
| x := X{Value: 10} | |
| var t T | |
| t = X{Value: 10} | |
| Equals[T]("X and T", T(x), t) | |
| t = X{Value: 20} | |
| Equals[T]("X and T, where T is larger", T(x), t) | |
| // now t is of a different type but still the interface type. | |
| t = Y{SomeValue: 10} | |
| Equals[T]("X and T, where T is Y of same value", T(x), t) | |
| } | |
| // T and X are used in UsingEquals() above | |
| type T interface { | |
| Display() | |
| } | |
| // Define a type X that implements T | |
| type X struct { | |
| Value int | |
| } | |
| // Implement the Display method to satisfy the T interface | |
| func (x X) Display() { | |
| fmt.Println("X Display:", x.Value) | |
| } | |
| type Y struct { | |
| SomeValue int | |
| } | |
| func (y Y) Display() { | |
| fmt.Println("Y Display:", y.SomeValue) | |
| } | |
| // what about a sorting function | |
| // we will use Ordered because Ordered is the only generic type that only works with == and != | |
| // and uses < > variants, which is needed for Less(i,j int) | |
| type SortAny[T cmp.Ordered] struct { | |
| Values []T | |
| } | |
| func (s *SortAny[T]) Len() int { return len(s.Values) } | |
| // Less takes in 2 index values i and j, and tells us if index i is less than index j | |
| func (s *SortAny[T]) Less(i, j int) bool { return s.Values[i] < s.Values[j] } | |
| // Swap will swap the two indexes i and j. | |
| func (s *SortAny[T]) Swap(i, j int) { s.Values[i], s.Values[j] = s.Values[j], s.Values[i] } | |
| func UsingTheGenericSorting() { | |
| integers := SortAny[int]{Values: []int{19, 15, 8, 13, 2, 24556, 1, 7, 5, 3, 88, 10012, 1}} | |
| sort.Sort(&integers) | |
| fmt.Println(integers) | |
| floats := SortAny[float32]{Values: []float32{19.1234, 15.234, 8.124, 13.1234, 2.21345, 24556.231, 1.134, 7.2345, 5.3235, 3.215, 88.235, 10012.235, 1.92}} | |
| sort.Sort(&floats) | |
| fmt.Println(floats) | |
| stringSlice := SortAny[string]{Values: []string{"z", "f", "u", "a", "j", "g", "c", "k", "r", "v"}} | |
| sort.Sort(&stringSlice) | |
| fmt.Println(stringSlice) | |
| } | |
| // Now what about interfaces that have type constraints? | |
| // What are type constraints anyways | |
| // it helps to understand what underlying types are first | |
| // a underlying type is any primative type or underlying (~) primitive literal type. | |
| // IE: ~T the underlying type of T must be itself, and T cannot be an interface. | |
| type ( | |
| A1 = string | |
| A2 = A1 | |
| B1 = string | |
| B2 B1 | |
| B3 []B1 | |
| B4 B3 | |
| ) | |
| // string, A1, A2, B1, and B2 all have the same underlying type (string). | |
| // []B1, B3, and B4 have the same underlying type ([]B1 or []string) | |
| // From https://go.dev/ref/spec#Satisfying_a_type_constraint:~:text=type%20parameter%20list-,Type%20constraints%C2%B6,-A%20type%20constraint | |
| // a type constraint is an interface that defines the set of permissible type | |
| // arguments for the respective type parameter and controls the operations supported by the values of that type parameter | |
| // comparable denotes the set of all non-interface types that are strictly comparable | |
| type TypeConstraintInt interface { | |
| ~int | |
| } | |
| // This is a union, and means that the type can be an int or a string | |
| type TypeConstraintIntAndString interface { | |
| int | string | |
| } | |
| // Type Constraints can only be used to define new Type Constraints or used as Generics. | |
| // Please see type_constraints.go for informaiton about the type constraints used here. | |
| // we need to do the following. | |
| // 1. we need to create a demo of type constraints not working. | |
| type Integer int | |
| // Adding is the type constraint of type int | |
| type Adding interface { | |
| int | |
| } | |
| type FromAddingType interface { | |
| Adding | |
| } | |
| type OtherAdding Adding | |
| type IntegerStruct int | |
| // GenericAddingTypeAdd is a generic function that takes a type constraint Adding | |
| func GenericAddingTypeAdd[T Adding](x, y T) T { | |
| return x + y | |
| } | |
| func GenericFromAddingTypeAdd[T FromAddingType](x, y T) T { | |
| return x + y | |
| } | |
| // works on both of the constraint types above. | |
| func thisWorksWithConstraintTypes() { | |
| fmt.Println(GenericAddingTypeAdd[int](1, 2)) | |
| fmt.Println(GenericFromAddingTypeAdd[int](5, 8)) | |
| // Cannot use the following because of the following error | |
| // there is also an error with gopls | |
| // cannot use type Adding outside a type constraint: interface contains type constraintscompilerMisplacedConstraintIface | |
| // fmt.Println(GenericAddingTypeAdd[Adding](1, 2)) | |
| // fmt.Println(GenericFromAddingTypeAdd[FromAddingType](5, 8)) | |
| } | |
| // How can we use constraint types at all with other types other than the type that they are constrainting? | |
| // in the example that we had the underlying type is (int) but the type is not an underlying constraint type. | |
| // | |
| // Here is an example that will not work when using other types that have the underlying type that is used in the | |
| // constraint type. | |
| // because Adding and FromAddingType do not allow for constraint underlying types, IE: they use a (~) to define the underlying type | |
| // | |
| // DoesNotSattisfyAddingsTypeConstraintBecauseItisNotUnderlyingTypes does not work | |
| // this will not work, because the type Integer is of type int, and GenericAddingTypeAdd | |
| // uses Adding which means it must be of type int and or Adding in order to work. | |
| // func DoesNotSattisfyAddingsTypeConstraintBecauseItisNotUnderlyingTypes() { | |
| // var x, y Integer | |
| // x = 18 | |
| // y = Integer(182) | |
| // fmt.Println(GenericAddingTypeAdd[Integer](x, y)) | |
| // fmt.Println(GenericAddingTypeAdd(x, y)) | |
| // } | |
| // So how can these types be satisfied | |
| // there are only two conditions, where a type T satisfies a constraint C iff | |
| // 1. T implements C, or | |
| // 2. C can be written in the form interface {comparable; E} where E is a basic interface and T is comparable and implements E | |
| // lets introduce a new type called the underlying type denoted by the ~ symbol in the syntax. | |
| // this means that the interface created can be any type whos root type is T. | |
| // in this case T is int | |
| type MySatisfiedIntConstraintType interface { | |
| ~int | |
| } | |
| // GenericMySatisfiedIntConstraintTypeAdd is a generic function that takes a type constraint Adding | |
| func GenericMySatisfiedIntConstraintTypeAdd[T MySatisfiedIntConstraintType](x, y T) T { | |
| return x + y | |
| } | |
| // lets use the Integer type created before that did not work with the interface{int} Adding before. | |
| func UsingUnderlyingTypes() { | |
| var x, y Integer | |
| x = 18 | |
| y = Integer(182) | |
| fmt.Println(GenericMySatisfiedIntConstraintTypeAdd(x, y)) | |
| fmt.Println(GenericMySatisfiedIntConstraintTypeAdd(x, y)) | |
| // even works on the GenericAdd that used cmp.Ordered | |
| fmt.Println(GenericAdd(x, y)) | |
| // even works with Equals that used comparable, because comparable is int underlying | |
| Equals("Integer", x, y) | |
| } | |
| // I have shown examples of using generics in Go only for functions, but its scope is not limited to Functions, and Methods. | |
| // It can also be used in structures as well. | |
| // here we have a Collection type that has a delegate to a slice. | |
| // The slice can now be of any type | |
| // OofNSortingAlgo is a fictional O(N) sorting algorithm | |
| // it has two generics, K, and V. | |
| // V is the values that will be sorted. | |
| // K is the type used to comparare the values of V, it is used in GetSortValue. | |
| // | |
| // K is the value generated by the GetSortValue function that takes the type V, one of the sorted values. | |
| type OofNSortingAlgo[K cmp.Ordered, V any] struct { | |
| sortedValues []V | |
| // GetSortValue is the function used to generate the value used for sorting. | |
| // the return value must be of type cmp.Ordered. | |
| GetSortValue func(V) K | |
| // UnsortedValues are the list of values that we want to sort. | |
| UnsortedValues []V | |
| } | |
| // Do will sort the UnsortedValues, then perform do on all the values of type V, in sorted order. | |
| // V is the type that the function is performed on. | |
| func (c OofNSortingAlgo[K, V]) Do(do func(V) error) error { | |
| var er error | |
| c.sortValues() | |
| for _, v := range c.sortedValues { | |
| if err := do(v); err != nil { | |
| er = errors.Join(er, err) | |
| } | |
| } | |
| return er | |
| } | |
| // SortingNode is a Node for sorting. | |
| // Key of type K is value used for sorting. | |
| // Value is the Value that we want to sort, given the list of values of type V. | |
| type SortingNode[K cmp.Ordered, V any] struct { | |
| Value V | |
| Key K | |
| } | |
| func (c OofNSortingAlgo[K, V]) sortValues() { | |
| nodes := make([]SortingNode[K, V], len(c.UnsortedValues)) | |
| for _, v := range c.UnsortedValues { | |
| kValue := c.GetSortValue(v) | |
| nodes = append(nodes, SortingNode[K, V]{Value: v, Key: kValue}) | |
| } | |
| // Your O(n) sorting algorithm goes here... | |
| // sort nodes via Key, to get sorted list | |
| c.sortedValues = make([]V, 0, len(c.UnsortedValues)) | |
| for _, n := range nodes { | |
| c.sortedValues = append(c.sortedValues, n.Value) | |
| } | |
| } | |
| type Person struct { | |
| Name string | |
| Address string | |
| Phone string | |
| } | |
| func UsingStructuresWithGenerics() { | |
| stringToIntMap := OofNSortingAlgo[string, int]{} | |
| // attach a GetSortValue from a service or something | |
| stringToIntMap.Do(func(i int) error { fmt.Println(i); return nil }) | |
| intToString := OofNSortingAlgo[int, string]{} | |
| // attach a GetSortValue from a service or something | |
| intToString.Do(func(i string) error { fmt.Println(i); return nil }) | |
| idsOfPeople := OofNSortingAlgo[int, Person]{} | |
| // attach a GetSortValue from a service or something | |
| idsOfPeople.Do(func(p Person) error { Process(p); return nil }) | |
| } | |
| func Process(p Person) { | |
| // do something with P | |
| } | |
| // now that we have discussed generics, lets talk about what generics are not. | |
| // so given this, what are we getting from Generics. Is there a way to get the more out of Generics besides what GoLang already offers? | |
| // Since Go has the original design or making interfaces global to any struture that implements the method set of a interface | |
| // the use of Generics is kind of useless when dealing with Interfaces, since it already existed before 1.18 update of Go. | |
| // Action is an action... | |
| type Action interface { | |
| /// action does some cool method set.. | |
| } | |
| // Runnable interface will run anything | |
| type Runable interface { | |
| Run() | |
| BeforeRun(a ...Action) <-chan error | |
| AfterRun(a ...Action) <-chan error | |
| DuringRun(a ...Action) <-chan error | |
| IfRunFails(a ...Action) <-chan error | |
| } | |
| func showingInterfaces() { | |
| runners := make([]Runable, 0) | |
| for _, r := range runners { | |
| r.Run() | |
| } | |
| } | |
| func FunctionUsingInterface(r Runable) { | |
| // do stuff | |
| r.Run() // maybe place it in a go routine | |
| // do more stuff | |
| } | |
| // all we need to is implement the Runable interface in some structs for this. | |
| // no one concept that can come to mind is non complete method sets. This might be an area of GoLang that might be useful. | |
| // interfaces can have embedded interface in them | |
| type EmailAutomater interface { | |
| WriteEmail(s string) error | |
| SendEmail() error | |
| ReSendEmail() error | |
| AddSubject(s string) error | |
| AddTo(s ...string) error | |
| // etc. | |
| } | |
| // here we embed the Runable and EmailAutomater so that we have an interface with all of them in it | |
| type EmailAutomaterRunner interface { | |
| Runable | |
| EmailAutomater | |
| } | |
| // So thats cool, but this is not using Generics. | |
| // What if this becomes legacy code. and after some time, we say hey I want | |
| // to do something with these EmasilAutomaterRunners but I want them to work with | |
| // this new interface system that we use for TextMessages | |
| type TextMessageAutoMater interface { | |
| WriteTextMessage(s string) error | |
| AddTo(s ...string) error | |
| SendTX() error | |
| ReSendTX() error | |
| // etc. | |
| } | |
| // but TextMessage does not have any implentation for runner, because it is provided by third parties. | |
| // so we dont need all this implementation, or maybe we just dont need to do those things, and it will not | |
| // work with the current RunnerManager that we have somewhere in the code. | |
| // So we want to comprise a new thing so that we can aggregate TextMessages and EmailAutomaterRunners together into one. | |
| // well that is where Generics come into play then. | |
| // it will introduce a couple of new methods to the method set, but that is ok, because we | |
| // can handle it its just going to act as wrappers on our EmailAutomater and TextMessageAutomater implementations | |
| // IE: func (e Email...) Send() error { return e.SendEmail() } | |
| type SendAbleMaterial interface { | |
| Send() error | |
| WriteTo(s string) error | |
| AddSender(s ...string) error | |
| } | |
| // So now we extened the funcitonality of your implementations of the legacy interfaces, and have a new interface that provides | |
| // a clear visibility of our new actions and operations. We can refactor our code to allow | |
| // changes for TextMessageAutoMater and EmailAutomaterRunner and EmailAutomater in the places that we collect | |
| // and need to change in our system to Generics | |
| type ReplyQueue struct { | |
| ReplyOperations []SendAbleMaterial | |
| // other things | |
| } | |
| type ReplyService struct { | |
| } | |
| // where before it was only func GatherReplyMessages(replyTo EmailAutomater) {} | |
| // if we were going to write a generic for this it would look like | |
| func GatherReplyMessagesForSendableMaterial[T SendAbleMaterial](replyTo T) { | |
| // stuff.... | |
| } | |
| // there is no actual need for using Generics in this example, because Golang uses and supports interfaces. | |
| // just make your life a little easier, before Generics came onto the scene. | |
| func GatherReplyMessages(replyTo SendAbleMaterial) { | |
| // call a service to get the reply messages | |
| // do other things, like translate | |
| objectsToReplyTo := []string{} | |
| for _, s := range objectsToReplyTo { | |
| err := replyTo.AddSender(s) | |
| if err != nil { | |
| // log.... | |
| } | |
| // etc... | |
| } | |
| // finish up and stuff... | |
| } | |
| // So now Generics in this case would provide an easier plug in to the code. | |
| // generally you want to use Generics for the following | |
| // 1. Algorithms and Collections that are type agnostic | |
| // a. IE: Structs that use Algos of Arrays, Slices, Trees, Nodes, PriorityQueues, etc.. | |
| // 2. Functions that work on any type, or comparable type | |
| // 3. Functions that have the same implementation again and again for any | |
| // type. (Happy Path for Code Reusability) | |
| // | |
| // Generally you do not want to use Generics for the following... | |
| // 1. when an interface will suffice | |
| // 2. When each type in a function or set of procedures is entirely | |
| // different (and you are not using an interface to enapsulate these details) | |
| // 3. When you use a different operation for the type | |
| // | |
| // Don't forget the generic types `cmp.Ordered`, `comparable`, and `any` when using Generics. | |
| // best case is, never plan on using generics, unless you need to find that you need to use the same code for | |
| // some other type. Then change that code so that it uses Type Parameters. | |
| // Use reflection or type checking to for cases that you should not use Generics for | |
| // | |
| // If you find yourself using type checking or reflect when using Generics, you | |
| // propably should not be using the Generic properties. | |
| func main() { | |
| // https://gist.github.com/jackstine | |
| PlusOperator() | |
| UsingAdd() | |
| UsingGenericAdd() | |
| UsingEquals() | |
| UsingTheGenericSorting() | |
| thisWorksWithConstraintTypes() | |
| UsingUnderlyingTypes() | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment