|
| 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 |
0 commit comments