Skip to content

Commit 891811b

Browse files
committed
Add Generics presentation
1 parent 5f7829a commit 891811b

19 files changed

+581
-0
lines changed

2022-03-08_generics/comparable.go

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package set // OMIT
2+
3+
type Set[T any] map[T]struct{} // Error: T is not comparable
4+
type Set[T comparable] map[T]struct{} // OK
5+
// INTERFACE OMIT
6+
var s Set[any] // Error: any does not implement comparable
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package constraints // OMIT
2+
3+
// Signed is a constraint that permits any signed integer type.
4+
type Signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 }
5+
6+
// Unsigned is a constraint that permits any unsigned integer type.
7+
type Unsigned interface { ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr }
8+
9+
// Integer is a constraint that permits any integer type.
10+
type Integer interface { Signed | Unsigned }
11+
12+
// Float is a constraint that permits any floating-point type.
13+
type Float interface { ~float32 | ~float64 }
14+
15+
// Complex is a constraint that permits any complex numeric type.
16+
type Complex interface { ~complex64 | ~complex128 }
17+
18+
// Ordered is a constraint that permits any type that supports the operators < <= >= >.
19+
type Ordered interface { Integer | Float | ~string }

2022-03-08_generics/generics.slide

+256
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
Generics in Go 1.18
2+
Introduced using a practical example
3+
4+
Axel Wagner
5+
Software Engineer, Infront Quant
6+
https://blog.merovius.de/
7+
@TheMerovius
8+
9+
* The example
10+
11+
* Providers
12+
13+
A service has its business logic split into _providers_
14+
15+
.code provider.go
16+
17+
: ID is used to route messages to the right provider
18+
: RequestType/ResponseType are used for allocating messages and dynamic type checking
19+
: Message is an interface embedding json.Marshaler etc.
20+
: Publisher allows to send multiple responses
21+
: ResponseIterator allows receiving multiple responses
22+
23+
* Implementing providers
24+
25+
An implementation of a provider is roughly
26+
27+
.code usage.go
28+
29+
: New might take dependencies etc.
30+
: dep is a different provider, this one depends on
31+
: Note the type-assertions for Request and subresp
32+
33+
* Problems
34+
35+
- Possible to pass wrong request type to Call/Stream/Publish
36+
- Request/Responses need type-assertions and extra variables
37+
- Slice request/responses use pointers
38+
- `RequestType()`/`ResponseType()` is boilerplate
39+
40+
: For simplicity, the reflect logic just assumes request/responses are pointers
41+
42+
* Generics
43+
44+
* Type parameters
45+
46+
.code provider_generic_1.go
47+
48+
: Package-scoped declarations can have an extra argument list using square brackets
49+
: These can be used as types in the declaration
50+
: The declaration has to be instantiated, substituting the specific types used
51+
: Publish now checks that ts argument matches the type declared by the provider
52+
: We don't need type-assertions anymore
53+
: No more need for Request/ResponseType methods
54+
55+
* Type parameters (cont)
56+
57+
Now an implementation is
58+
59+
.code usage_generic_1.go ,/INFER/
60+
61+
: Request/ResponseType methods are gone
62+
: We don't need a temporary variable for the sub response anymore
63+
: We can instantiate using non-pointers, if we want, for slices
64+
: Publish can infer its type argument
65+
: We could still accidentally instantiate Call with the wrong arguments
66+
67+
* Type-inference
68+
69+
It is a bit unwieldy having to add the instantiation to every type. Luckily,
70+
the compiler can sometimes _infer_ these types, allowing us to omit them:
71+
72+
.code usage_generic_1.go /INFER/,/NOINFER/
73+
74+
*Limitations*
75+
76+
- Type-inference only works based on the *arguments* of a function call:
77+
78+
.code usage_generic_1.go /NOINFER/,$
79+
80+
- Thus it only works on *calls*, not for generic types or storing in a variable
81+
82+
* Making Call safe
83+
84+
We can make `Call` even more type-safe, by using a little trick:
85+
86+
.code provider_generic_2.go
87+
88+
: Even though ID is just a string, we can add parameters to it
89+
: Now the ID also carries information what Request/Response is needed
90+
: That information can then be used by Call/Stream to type-check their instantiation and arguments (next slide)
91+
92+
* Making Call safe (cont)
93+
94+
And on the implementation side:
95+
96+
.code usage_generic_2.go ,/SPLIT/
97+
98+
.code usage_generic_2.go /SPLIT/,/INFER/
99+
100+
Bonus: `dep.ID` is an argument and "carries" request/response types. So we can
101+
now infer type arguments:
102+
103+
.code usage_generic_2.go /INFER/,$
104+
105+
: Now, if we instantiate Call with the wrong arguments, it can tell based on dep.ID and the compiler complains
106+
: The fact that type-inference only considers arguments is another benefit of the ID trick.
107+
: Otherwise, the response type would not appear in the call and could not get infered.
108+
109+
* Constraints
110+
111+
Remember the `Message` interface? In our new version, requests and responses no
112+
longer need to comply with it, we can use `any` type. We can fix that by adding
113+
it as a _constraint_:
114+
115+
.code provider_generic_3.go /type ID/,/func usage/
116+
117+
* Constraints (cont)
118+
119+
Constraints can be any interface type. At instantiation, the compiler checks
120+
that the type-arguments implement that interface:
121+
122+
.code provider_generic_3.go /func usage/,/ID/
123+
124+
The compiler allows a function to call exactly the methods defined by the constraints:
125+
126+
.code provider_generic_3.go /CALL IMPL/,$
127+
128+
`any` is just a new, predeclared alias for `interface{}`.
129+
130+
* Type sets
131+
132+
So far, constraints only allow calling _methods_. For using _operators_, we
133+
introduce _type_sets_:
134+
135+
- `T` is the set containing only `T`
136+
- `~T` is the set containing all types with _underlying_type_ `T`
137+
- `S|T` is the set of all types which are in the set `S` or the set `T`
138+
139+
An interface can now contain a type set:
140+
141+
.code type_sets.go
142+
143+
Interfaces containing such type-sets can _only_ be used as constraints.
144+
145+
* Type sets (cont)
146+
147+
The compiler allows *using*an*operation* in a generic function, if it is
148+
supported by all types in the type set of the constraint:
149+
150+
.code type_sets_use.go /func Concat/,/^}/
151+
152+
* Type sets (cont)
153+
154+
The compiler allows an *instantiation*, if the type argument is in the type set of the constraint:
155+
156+
.code type_sets_use.go /func usage/,/^}/
157+
158+
* Type sets (cont)
159+
160+
It is also possible to use type-set elements _directly_ in a constraint:
161+
162+
.code join_example.go ,/SPLIT/
163+
164+
.code join_example.go /SPLIT/,$
165+
166+
* The constraints package
167+
168+
There is a new package `golang.org/x/exp/constraints`, for commonly used type sets:
169+
170+
.code constraints_pkg.go
171+
172+
* comparable
173+
174+
There is one special predeclared interface `comparable`, implemented by
175+
anything that is (safely) comparable using `==` and `!=`:
176+
177+
- Any string/numeric/boolean/pointer/channel type
178+
- Any struct-type with only comparable fields
179+
- Any array-type with a comparable element type
180+
- *Not* function, slice, map *or*interface*types*
181+
182+
It is needed to use `==` and `!=` or to use a type-parameter in a map:
183+
184+
.code comparable.go ,/INTERFACE/
185+
186+
Importantly, interface-types do *not* implement `comparable` (see [[https://github.com/golang/go/issues/51338][#51338]]):
187+
188+
.code comparable.go /INTERFACE/,$
189+
190+
* Pointer methods
191+
192+
Back to the example. There is a problem with our `Message` interface.
193+
194+
.code pointer_methods.go /type Message/,/END DEFINITION/
195+
196+
* Pointer methods (cont)
197+
198+
If we try to use this, we get into trouble, though:
199+
200+
.code pointer_methods.go /type Request/,/^}/
201+
202+
* Pointer methods (cont)
203+
204+
`Call` needs to accept/return the plain types, but call the methods on their pointers:
205+
206+
.code pointer_methods_2.go
207+
208+
* Pointer methods (cont)
209+
210+
We thus have to pass *both* the base and the pointer types and constrain the
211+
pointer type to have the relevant methods:
212+
213+
.code pointer_methods_fix.go /type Message/,$
214+
215+
* Library changes
216+
217+
There are a couple of new packages, taking advantage of generics:
218+
219+
- `golang.org/x/exp/constraints`: A set of useful constraints to be used with
220+
type parameters.
221+
- `golang.org/x/exp/maps`: Various functions useful with maps of any type.
222+
- `golang.org/x/exp/slices`: Various functions useful with slices of any type.
223+
- `go/*` have been updated to be able to write tools for generic code.
224+
225+
* Limitations
226+
227+
* No higher abstractions
228+
229+
Every generic function/type must be fully instantiated before use:
230+
231+
.code higher_abstraction.go
232+
233+
One design goal was to allow [[https://research.swtch.com/generic][different implementation strategies]].
234+
235+
Allowing higher abstraction would require a boxing implementation.
236+
237+
* No extra type parameters on methods
238+
239+
It is not possible to add extra type parameters to methods:
240+
241+
.code method_parameters.go ,/SPLIT/
242+
243+
Use functions instead:
244+
245+
.code method_parameters.go /SPLIT/,$
246+
247+
This is because Go allows interface type-assertions, which would require
248+
runtime implementation strategies:
249+
250+
.code method_type_assertion.go
251+
252+
* Other limitations
253+
254+
- No embedding of type parameters.
255+
- A union element with more than one term may not contain an interface type with a non-empty method set.
256+
- A couple of minor limitations, to be addressed in Go 1.19
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
func Sort[T constraints.Ordered](s []T) {
2+
sort.Slice(s, func(i, j int) bool {
3+
return s[i] < s[j]
4+
})
5+
}
6+
7+
func Example() {
8+
f := Sort // Error: Sort must be fully instantiated
9+
f := Sort[int] // OK
10+
}

2022-03-08_generics/join_example.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package strings
2+
3+
// Join is like strings.Join, but also works on defined types based on string.
4+
// S is any type with underlying type string.
5+
func Join[S ~string](parts []S, sep S) S {
6+
p := []string(parts) // allowed conversion from []S to []string
7+
joined := strings.Join(p, string(sep) // allowed conversion from S to string
8+
return S(joined) // allowed conversion from string to S
9+
}
10+
// SPLIT OMIT
11+
type Path string
12+
13+
const Sep Path = "/"
14+
15+
func Join(parts ...Path) Path {
16+
// Infers strings.Join[Path], which has type
17+
// func([]Path, Path) Path
18+
return strings.Join(parts, Sep)
19+
}
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
type Set[A comparable] map[A]struct{}
2+
3+
// Error: Can't have extra type parameters on methods
4+
func (s Set[A]) Map[B comparable](f func(A) B) Set[B]
5+
6+
// SPLIT OMIT
7+
func Map[A, B comparable](s Set[A], f func(A) B) Set[B]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
type X struct{}
2+
func (X) F[T any](v T) {}
3+
4+
func FarAwayCode(x X) {
5+
// Compiler did not know it might need to generate S.F[int]
6+
fint := x.(interface{ F(int) })
7+
}
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package x
2+
3+
import (
4+
"encoding/json"
5+
)
6+
7+
type Message interface {
8+
json.Marshaler
9+
json.Unmarshaler
10+
}
11+
12+
// Illustrative implementation of the relevant parts of Call.
13+
func Call[Req, Resp Message](req Req) (resp Resp, error) {
14+
b, err := req.MarshalJSON()
15+
if err != nil { return resp, err }
16+
// Send bytes over network, get response back
17+
err := resp.UnmarshalJSON(b)
18+
if err != nil { return resp, err }
19+
return resp, nil
20+
}
21+
22+
// END DEFINITION OMIT
23+
24+
type Request struct { /* … */ }
25+
func (m *Request) MarshalJSON() ([]byte, error) { /* … */ }
26+
func (m *Request) UnmarshalJSON(b []byte) error { /* … */ }
27+
28+
type Response struct { /* … */ }
29+
func (m *Response) MarshalJSON() ([]byte, error) { /* … */ }
30+
func (m *Response) UnmarshalJSON(b []byte) error { /* … */ }
31+
32+
func instantiation() { // OMIT
33+
// Error: Request/Response do not implement Message, methods have pointer receivers
34+
resp, err := Call[Request, Response](req)
35+
// Panics: resp.UnmarshalJSON(b) tries to unmarshal into a nil-pointer
36+
resp, err := Call[*Request, *Response](req)
37+
} // OMIT
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package x
2+
3+
import (
4+
"encoding/json"
5+
)
6+
7+
func Call[TODO](req Req) (resp Resp, error) { // HL
8+
// Call MarshalJSON on the pointer // HL
9+
b, err := (&req).MarshalJSON() // HL
10+
if err != nil { return resp, err }
11+
// Send bytes over network, get response back
12+
// Call UnmarshalJSON on the pointer // HL
13+
err := (&resp).UnmarshalJSON(b) // HL
14+
if err != nil { return resp, err }
15+
return resp, nil
16+
}

0 commit comments

Comments
 (0)