-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package set // OMIT | ||
|
||
type Set[T any] map[T]struct{} // Error: T is not comparable | ||
type Set[T comparable] map[T]struct{} // OK | ||
// INTERFACE OMIT | ||
var s Set[any] // Error: any does not implement comparable |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package constraints // OMIT | ||
|
||
// Signed is a constraint that permits any signed integer type. | ||
type Signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } | ||
|
||
// Unsigned is a constraint that permits any unsigned integer type. | ||
type Unsigned interface { ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr } | ||
|
||
// Integer is a constraint that permits any integer type. | ||
type Integer interface { Signed | Unsigned } | ||
|
||
// Float is a constraint that permits any floating-point type. | ||
type Float interface { ~float32 | ~float64 } | ||
|
||
// Complex is a constraint that permits any complex numeric type. | ||
type Complex interface { ~complex64 | ~complex128 } | ||
|
||
// Ordered is a constraint that permits any type that supports the operators < <= >= >. | ||
type Ordered interface { Integer | Float | ~string } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,256 @@ | ||
Generics in Go 1.18 | ||
Introduced using a practical example | ||
|
||
Axel Wagner | ||
Software Engineer, Infront Quant | ||
https://blog.merovius.de/ | ||
@TheMerovius | ||
|
||
* The example | ||
|
||
* Providers | ||
|
||
A service has its business logic split into _providers_ | ||
|
||
.code provider.go | ||
|
||
: ID is used to route messages to the right provider | ||
: RequestType/ResponseType are used for allocating messages and dynamic type checking | ||
: Message is an interface embedding json.Marshaler etc. | ||
: Publisher allows to send multiple responses | ||
: ResponseIterator allows receiving multiple responses | ||
|
||
* Implementing providers | ||
|
||
An implementation of a provider is roughly | ||
|
||
.code usage.go | ||
|
||
: New might take dependencies etc. | ||
: dep is a different provider, this one depends on | ||
: Note the type-assertions for Request and subresp | ||
|
||
* Problems | ||
|
||
- Possible to pass wrong request type to Call/Stream/Publish | ||
- Request/Responses need type-assertions and extra variables | ||
- Slice request/responses use pointers | ||
- `RequestType()`/`ResponseType()` is boilerplate | ||
|
||
: For simplicity, the reflect logic just assumes request/responses are pointers | ||
|
||
* Generics | ||
|
||
* Type parameters | ||
|
||
.code provider_generic_1.go | ||
|
||
: Package-scoped declarations can have an extra argument list using square brackets | ||
: These can be used as types in the declaration | ||
: The declaration has to be instantiated, substituting the specific types used | ||
: Publish now checks that ts argument matches the type declared by the provider | ||
: We don't need type-assertions anymore | ||
: No more need for Request/ResponseType methods | ||
|
||
* Type parameters (cont) | ||
|
||
Now an implementation is | ||
|
||
.code usage_generic_1.go ,/INFER/ | ||
|
||
: Request/ResponseType methods are gone | ||
: We don't need a temporary variable for the sub response anymore | ||
: We can instantiate using non-pointers, if we want, for slices | ||
: Publish can infer its type argument | ||
: We could still accidentally instantiate Call with the wrong arguments | ||
|
||
* Type-inference | ||
|
||
It is a bit unwieldy having to add the instantiation to every type. Luckily, | ||
the compiler can sometimes _infer_ these types, allowing us to omit them: | ||
|
||
.code usage_generic_1.go /INFER/,/NOINFER/ | ||
|
||
*Limitations* | ||
|
||
- Type-inference only works based on the *arguments* of a function call: | ||
|
||
.code usage_generic_1.go /NOINFER/,$ | ||
|
||
- Thus it only works on *calls*, not for generic types or storing in a variable | ||
|
||
* Making Call safe | ||
|
||
We can make `Call` even more type-safe, by using a little trick: | ||
|
||
.code provider_generic_2.go | ||
|
||
: Even though ID is just a string, we can add parameters to it | ||
: Now the ID also carries information what Request/Response is needed | ||
: That information can then be used by Call/Stream to type-check their instantiation and arguments (next slide) | ||
|
||
* Making Call safe (cont) | ||
|
||
And on the implementation side: | ||
|
||
.code usage_generic_2.go ,/SPLIT/ | ||
|
||
.code usage_generic_2.go /SPLIT/,/INFER/ | ||
|
||
Bonus: `dep.ID` is an argument and "carries" request/response types. So we can | ||
now infer type arguments: | ||
|
||
.code usage_generic_2.go /INFER/,$ | ||
|
||
: Now, if we instantiate Call with the wrong arguments, it can tell based on dep.ID and the compiler complains | ||
: The fact that type-inference only considers arguments is another benefit of the ID trick. | ||
: Otherwise, the response type would not appear in the call and could not get infered. | ||
|
||
* Constraints | ||
|
||
Remember the `Message` interface? In our new version, requests and responses no | ||
longer need to comply with it, we can use `any` type. We can fix that by adding | ||
it as a _constraint_: | ||
|
||
.code provider_generic_3.go /type ID/,/func usage/ | ||
|
||
* Constraints (cont) | ||
|
||
Constraints can be any interface type. At instantiation, the compiler checks | ||
that the type-arguments implement that interface: | ||
|
||
.code provider_generic_3.go /func usage/,/ID/ | ||
|
||
The compiler allows a function to call exactly the methods defined by the constraints: | ||
|
||
.code provider_generic_3.go /CALL IMPL/,$ | ||
|
||
`any` is just a new, predeclared alias for `interface{}`. | ||
|
||
* Type sets | ||
|
||
So far, constraints only allow calling _methods_. For using _operators_, we | ||
introduce _type_sets_: | ||
|
||
- `T` is the set containing only `T` | ||
- `~T` is the set containing all types with _underlying_type_ `T` | ||
- `S|T` is the set of all types which are in the set `S` or the set `T` | ||
|
||
An interface can now contain a type set: | ||
|
||
.code type_sets.go | ||
|
||
Interfaces containing such type-sets can _only_ be used as constraints. | ||
|
||
* Type sets (cont) | ||
|
||
The compiler allows *using*an*operation* in a generic function, if it is | ||
supported by all types in the type set of the constraint: | ||
|
||
.code type_sets_use.go /func Concat/,/^}/ | ||
|
||
* Type sets (cont) | ||
|
||
The compiler allows an *instantiation*, if the type argument is in the type set of the constraint: | ||
|
||
.code type_sets_use.go /func usage/,/^}/ | ||
|
||
* Type sets (cont) | ||
|
||
It is also possible to use type-set elements _directly_ in a constraint: | ||
|
||
.code join_example.go ,/SPLIT/ | ||
|
||
.code join_example.go /SPLIT/,$ | ||
|
||
* The constraints package | ||
|
||
There is a new package `golang.org/x/exp/constraints`, for commonly used type sets: | ||
|
||
.code constraints_pkg.go | ||
|
||
* comparable | ||
|
||
There is one special predeclared interface `comparable`, implemented by | ||
anything that is (safely) comparable using `==` and `!=`: | ||
|
||
- Any string/numeric/boolean/pointer/channel type | ||
- Any struct-type with only comparable fields | ||
- Any array-type with a comparable element type | ||
- *Not* function, slice, map *or*interface*types* | ||
|
||
It is needed to use `==` and `!=` or to use a type-parameter in a map: | ||
|
||
.code comparable.go ,/INTERFACE/ | ||
|
||
Importantly, interface-types do *not* implement `comparable` (see [[https://github.com/golang/go/issues/51338][#51338]]): | ||
|
||
.code comparable.go /INTERFACE/,$ | ||
|
||
* Pointer methods | ||
|
||
Back to the example. There is a problem with our `Message` interface. | ||
|
||
.code pointer_methods.go /type Message/,/END DEFINITION/ | ||
|
||
* Pointer methods (cont) | ||
|
||
If we try to use this, we get into trouble, though: | ||
|
||
.code pointer_methods.go /type Request/,/^}/ | ||
|
||
* Pointer methods (cont) | ||
|
||
`Call` needs to accept/return the plain types, but call the methods on their pointers: | ||
|
||
.code pointer_methods_2.go | ||
|
||
* Pointer methods (cont) | ||
|
||
We thus have to pass *both* the base and the pointer types and constrain the | ||
pointer type to have the relevant methods: | ||
|
||
.code pointer_methods_fix.go /type Message/,$ | ||
|
||
* Library changes | ||
|
||
There are a couple of new packages, taking advantage of generics: | ||
|
||
- `golang.org/x/exp/constraints`: A set of useful constraints to be used with | ||
type parameters. | ||
- `golang.org/x/exp/maps`: Various functions useful with maps of any type. | ||
- `golang.org/x/exp/slices`: Various functions useful with slices of any type. | ||
- `go/*` have been updated to be able to write tools for generic code. | ||
|
||
* Limitations | ||
|
||
* No higher abstractions | ||
|
||
Every generic function/type must be fully instantiated before use: | ||
|
||
.code higher_abstraction.go | ||
|
||
One design goal was to allow [[https://research.swtch.com/generic][different implementation strategies]]. | ||
|
||
Allowing higher abstraction would require a boxing implementation. | ||
|
||
* No extra type parameters on methods | ||
|
||
It is not possible to add extra type parameters to methods: | ||
|
||
.code method_parameters.go ,/SPLIT/ | ||
|
||
Use functions instead: | ||
|
||
.code method_parameters.go /SPLIT/,$ | ||
|
||
This is because Go allows interface type-assertions, which would require | ||
runtime implementation strategies: | ||
|
||
.code method_type_assertion.go | ||
|
||
* Other limitations | ||
|
||
- No embedding of type parameters. | ||
- A union element with more than one term may not contain an interface type with a non-empty method set. | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
Merovius
Author
Owner
|
||
- A couple of minor limitations, to be addressed in Go 1.19 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
func Sort[T constraints.Ordered](s []T) { | ||
sort.Slice(s, func(i, j int) bool { | ||
return s[i] < s[j] | ||
}) | ||
} | ||
|
||
func Example() { | ||
f := Sort // Error: Sort must be fully instantiated | ||
f := Sort[int] // OK | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package strings | ||
|
||
// Join is like strings.Join, but also works on defined types based on string. | ||
// S is any type with underlying type string. | ||
func Join[S ~string](parts []S, sep S) S { | ||
p := []string(parts) // allowed conversion from []S to []string | ||
joined := strings.Join(p, string(sep) // allowed conversion from S to string | ||
return S(joined) // allowed conversion from string to S | ||
} | ||
// SPLIT OMIT | ||
type Path string | ||
|
||
const Sep Path = "/" | ||
|
||
func Join(parts ...Path) Path { | ||
// Infers strings.Join[Path], which has type | ||
// func([]Path, Path) Path | ||
return strings.Join(parts, Sep) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
type Set[A comparable] map[A]struct{} | ||
|
||
// Error: Can't have extra type parameters on methods | ||
func (s Set[A]) Map[B comparable](f func(A) B) Set[B] | ||
|
||
// SPLIT OMIT | ||
func Map[A, B comparable](s Set[A], f func(A) B) Set[B] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
type X struct{} | ||
func (X) F[T any](v T) {} | ||
|
||
func FarAwayCode(x X) { | ||
// Compiler did not know it might need to generate S.F[int] | ||
fint := x.(interface{ F(int) }) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package x | ||
|
||
import ( | ||
"encoding/json" | ||
) | ||
|
||
type Message interface { | ||
json.Marshaler | ||
json.Unmarshaler | ||
} | ||
|
||
// Illustrative implementation of the relevant parts of Call. | ||
func Call[Req, Resp Message](req Req) (resp Resp, error) { | ||
b, err := req.MarshalJSON() | ||
if err != nil { return resp, err } | ||
// Send bytes over network, get response back | ||
err := resp.UnmarshalJSON(b) | ||
if err != nil { return resp, err } | ||
return resp, nil | ||
} | ||
|
||
// END DEFINITION OMIT | ||
|
||
type Request struct { /* … */ } | ||
func (m *Request) MarshalJSON() ([]byte, error) { /* … */ } | ||
func (m *Request) UnmarshalJSON(b []byte) error { /* … */ } | ||
|
||
type Response struct { /* … */ } | ||
func (m *Response) MarshalJSON() ([]byte, error) { /* … */ } | ||
func (m *Response) UnmarshalJSON(b []byte) error { /* … */ } | ||
|
||
func instantiation() { // OMIT | ||
// Error: Request/Response do not implement Message, methods have pointer receivers | ||
resp, err := Call[Request, Response](req) | ||
// Panics: resp.UnmarshalJSON(b) tries to unmarshal into a nil-pointer | ||
resp, err := Call[*Request, *Response](req) | ||
} // OMIT |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package x | ||
|
||
import ( | ||
"encoding/json" | ||
) | ||
|
||
func Call[TODO](req Req) (resp Resp, error) { // HL | ||
// Call MarshalJSON on the pointer // HL | ||
b, err := (&req).MarshalJSON() // HL | ||
if err != nil { return resp, err } | ||
// Send bytes over network, get response back | ||
// Call UnmarshalJSON on the pointer // HL | ||
err := (&resp).UnmarshalJSON(b) // HL | ||
if err != nil { return resp, err } | ||
return resp, nil | ||
} |
Thanks for the great talk! I wondered where I could find the discussion for this decision since the release note didn't provide an issue number?