diff options
-rw-r--r-- | content/posts/2022-01-17-generics-in-go-1.18.md | 316 |
1 files changed, 316 insertions, 0 deletions
diff --git a/content/posts/2022-01-17-generics-in-go-1.18.md b/content/posts/2022-01-17-generics-in-go-1.18.md new file mode 100644 index 0000000..5379ab7 --- /dev/null +++ b/content/posts/2022-01-17-generics-in-go-1.18.md @@ -0,0 +1,316 @@ +--- +slug: generics-in-go-1.18 +title: "Generics in Go 1.18" +date: "2022-01-17T00:49:28-04:00" +--- +I spent some time trying the [generics types and generic +functions][generics] in [Go 1.18][], and I like what they've got so far. + +Below is a [Go 1.17][] program which prints the results of the following +functions: + +* `SumInts()`: calculate the the sum of of a slice of `int` values. +* `SumFloats()`: calculate the the sum of of a slice of `float32` + values. +* `SumComplexes()`: calculate the the sum of of a slice of `complex64` + values. + +```go +package main + +import "fmt" + +// Return sum of all values in slice of ints. +func SumInts(m []int) int { + var r int + + for _, v := range(m) { + r += v + } + + return r +} + +// Return sum of all values in slice of float32s. +func SumFloats(m []float32) float32 { + var r float32 + + for _, v := range(m) { + r += v + } + + return r +} + +// Return sum of all values in slice of complex64s. +func SumComplexes(m []complex64) complex64 { + var r complex64 + + for _, v := range(m) { + r += v + } + + return r +} + +var ( + // test integers + ints = []int { 10, 20, 30 } + + // test floating point numbers + floats = []float32 { 10.0, 20.0, 30.0 } + + // test complex numbers + complexes = []complex64 { complex(10, 1), complex(20, 2), complex(30, 3) } +) + +func main() { + // print sums + fmt.Printf("ints = %d\n", SumInts(ints)) + fmt.Printf("floats = %2.1f\n", SumFloats(floats)) + fmt.Printf("complexes = %g\n", SumComplexes(complexes)) +} +``` + + +Here's the same program, written using [Go 1.18][] [generics][]: + +```go +package main + +import "fmt" + +// Return sum of all numeric values in slice. +func Sum[V int|float32|complex64](vals []V) V { + var r V + + for _, v := range(vals) { + r += v + } + + return r +} + +var ( + // test integers + ints = []int { 10, 20, 30 } + + // test floating point numbers + floats = []float32 { 10.0, 20.0, 30.0 } + + // test complex numbers + complexes = []complex64 { complex(10, 1), complex(20, 2), complex(30, 3) } +) + +func main() { + // print sums using generics w/explicit types + fmt.Printf("ints = %d\n", Sum[int](ints)) + fmt.Printf("floats = %2.1f\n", Sum[float32](floats)) + fmt.Printf("complexes = %g\n", Sum[complex64](complexes)) +} +``` + + +You can use [type inference][] to drop the type parameters in many +instances. For example, we can rewrite `main()` from the previous +example like this: + +```go +func main() { + // print sums using generics w/explicit types + fmt.Printf("ints = %d\n", Sum(ints)) + fmt.Printf("floats = %2.1f\n", Sum(floats)) + fmt.Printf("complexes = %g\n", Sum(complexes)) +} +``` + + +[Generics][] can also be used in type definitions. Example: + +```go +package main + +import "fmt" + +// Fraction +type Frac[T int|int32|int64] struct { + num T // numerator + den T // denominator +} + +// Add two fractions. +func (a Frac[T]) Add(b Frac[T]) Frac[T] { + return Frac[T] { a.num + b.num, a.den * b.den } +} + +// Multiple fractions. +func (a Frac[T]) Mul(b Frac[T]) Frac[T] { + return Frac[T] { a.num * b.num, a.den * b.den } +} + +// Return inverse of fraction. +func (a Frac[T]) Inverse() Frac[T] { + return Frac[T] { a.den, a.num } +} + +// Return string representation of fraction. +func (a Frac[T]) String() string { + return fmt.Sprintf("%d/%d", a.num, a.den) +} + +func main() { + // test fractions + fracs = []Frac[int] { + Frac[int] { 1, 2 }, + Frac[int] { 3, 4 }, + Frac[int] { 5, 6 }, + } + + // print fractions + for _, f := range(fracs) { + fmt.Printf("%s => %s\n", f, f.Mul(f.Add(f.Inverse()))) + } +} +``` + + +Interface type declarations can now be used to specify type unions. For +example, the `Frac` type declaration from the previous example could be +written like this instead: + +```go +// Integral number type. +type integral interface { + int | int32 | int64 +} + +// Fraction +type Frac[T integral] struct { + num T // numerator + den T // denominator +} +``` + + +There are two new keywords: + +* `any`: Alias for `interface {}`. +* `comparable`: Any type which can be compared for equality with `==` + and `!=`. Useful for the parameterizing map keys. + +There is a new `constraints` package, which (not yet visible in the +[online Go documentation][go-docs] as of this writing) that provides a +couple of useful unions, but it's relatively anemic at the moment: + +```bash +$ go1.18beta1 doc -all constraints +package constraints // import "constraints" + +Package constraints defines a set of useful constraints to be used with type +parameters. + +TYPES + +type Complex interface { + ~complex64 | ~complex128 +} + Complex is a constraint that permits any complex numeric type. If future + releases of Go add new predeclared complex numeric types, this constraint + will be modified to include them. + +type Float interface { + ~float32 | ~float64 +} + Float is a constraint that permits any floating-point type. If future + releases of Go add new predeclared floating-point types, this constraint + will be modified to include them. + +type Integer interface { + Signed | Unsigned +} + Integer is a constraint that permits any integer type. If future releases of + Go add new predeclared integer types, this constraint will be modified to + include them. + +type Ordered interface { + Integer | Float | ~string +} + Ordered is a constraint that permits any ordered type: any type that + supports the operators < <= >= >. If future releases of Go add new ordered + types, this constraint will be modified to include them. + +type Signed interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 +} + Signed is a constraint that permits any signed integer type. If future + releases of Go add new predeclared signed integer types, this constraint + will be modified to include them. + +type Unsigned interface { + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +} + Unsigned is a constraint that permits any unsigned integer type. If future + releases of Go add new predeclared unsigned integer types, this constraint + will be modified to include them. +``` + + +Using the `constraints` package, the `Frac` type from the previous +example could be written like this: + +```go +import "constraints" + +// Fraction +type Frac[T constraints.Signed] struct { + num T // numerator + den T // denominator +} +``` + + +And with `constraints`, the `Sum()` function from the first example +could be defined like this: + +```go +// Numeric value. +type Number interface { + constraints.Integer | constraints.Float | constraints.Complex +} + +// Return sum of all numeric values in slice. +func Sum[V Number](vals []V) V { + var r V + + for _, v := range(vals) { + r += v + } + + return r +} +``` + + +Other useful tidbits: + +* No [type erasure][]. +* The standard library is still backwards compatible, so there is no + need to rewrite your existing code. +* There are [two new tutorials][] which explain generics and fuzzing. + +[go]: https://go.dev/ + "Go programming language" +[go 1.18]: https://tip.golang.org/doc/go1.18 + "Go 1.18" +[go 1.17]: https://go.dev/doc/go1.17 + "Go 1.17" +[generics]: https://en.wikipedia.org/wiki/Generic_programming + "Generic programming" +[type inference]: https://en.wikipedia.org/wiki/Type_inference + "Automatic detection of variable types." +[go-docs]: https://pkg.go.dev/ + "Online Go package documentation." +[type erasure]: https://en.wikipedia.org/wiki/Type_erasure + "Type erasure." +[two new tutorials]: https://go.dev/blog/tutorials-go1.18 + "New Go tutorials which explain generics and fuzzing." |