aboutsummaryrefslogtreecommitdiff
path: root/content/posts/2022-01-17-generics-in-go-1.18.md
blob: 8ba610efd8cfaa4b143d2354241abf463164126f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
---
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 define the constraints
that a matching type must satisfy.  In addition to the ability to
specify methods that a matching type must implement, a type constraint
specified as an interface may also specify a union of terms indicating
the set of matching types.

Type union terms can be tilde-prefixed (example: `~int`), which
indicates that the *underlying type* must match the given type.

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 predeclared identifiers:

* `any`: An 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.
```
&nbsp;

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
}
```
&nbsp;

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
}
```
&nbsp;

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.
* [Type constraints section][] of [Go spec][].

**Update (2021-01-19):** Minor wording changes, add information about
tilde prefixes in type constraints.

[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."
[type constraints section]: https://tip.golang.org/ref/spec#Type_constraints
  "Type constraints section of Go specification."
[go spec]: https://tip.golang.org/ref/spec
  "Go language specification."