generics Лекция 8 Арсений Балобанов * Generics * New language features - Type parameters for functions and types - Type sets - Type inference * Sorting in Go what we have func Sort(data Interface) type Interface interface { Len() int Less(i, j int) bool Swap(i, j int) } what we really want func Sort(list []Elem) // use Sort(myList) * Type parameters to the rescue func Sort[Elem ?](list []Elem) * Parameter lists An ordinary parameter list (x, y aType, z anotherType) A type parameter list [P, Q aConstraint, R anotherConstraint] - Convention: Type parameter names are capitalized * Constraints - A constraint specifies the requirements which a type argument must satisfy. - In generic Go, constraints are interfaces - A type argument is valid if it implements its constraint. * Generic Sort func Sort[Elem interface{ Less(y Elem) bool }](list []Elem) { ... } - The constraint is an interface, but the actual type argument can be any type that implements that interface. - The scope of a type parameter starts at the opening "[" and ends at the end of the generic type or function declaration * Using generic Sort Somewhere in library func Sort[Elem interface{ Less(y Elem) bool }](list []Elem) User code type book struct{...} func (x book) Less(y book) bool {...} var bookshelf []book ... Sort[book](bookshelf) // generic function call * Type-checking the Sort call: Instantiation What happens when we call Sort? Sort[book](bookshelf) - Substitution. Substitute book for elem Sort[Elem interface{ Less(y Elem) bool }] | (list []Elem) Sort[book interface{ Less(y book) bool }] | (list []book) - Verification. Verify that book satisfies the book parameter constraint - Instantiate book-specific function #Sort[book] | (list []book) * Type-checking a generic call Instantiation (new) - replace type parameters with type arguments in entire signature - verify that each type argument satisfies its constraint Invocation (as usual) - verify that each ordinary argument can be assigned to its parameter * Types can be generic, too type Lesser[T any] interface{ Less(y T) bool } any stands for "no constraint" (same as "interface{}") * Sort, decomposed type Lesser[T any] interface{ Less(y T) bool } func Sort[Elem Lesser[Elem]](list []Elem) * Problems what we want Sort([]int{1, 2, 3}) int does not implement Elem constraint (no Less method) what we could do type myInt int func (x myInt) Less(y myInt) bool { return x < y } * min .play -edit min/basic/min.go /^func min/,/^}/ * Generic min .play -edit min/basic/min.go /^func min/,/^}/ .play -edit min/generic/min.go /^func min/,/^}/ * Calling generic min m := min[int](1, 2) * Instantiation fmin := min[float64] // non generic m := fmin(2.71, 3.14) * Generic type type Tree[T interface{}] struct { left, right *Tree[T] data T } func (t *Tree[T]) Lookup(x T) *Tree[T] var stringTree Tree[string] - Methods cannot have it's own type parameters * Type sets func min(x, y float64) float64 - float64 defines a set of values x and y can have func min[T constraints.Ordered](x, y T) T { - constraints.Ordered defines a set of values T can have * constaints.Ordered // 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 Ordered interface { Integer | Float | ~string } - The < operator is supported by every type in this subset - ~T means with underlying type T * constaints type Signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } type Unsigned interface { ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr } type Integer interface { Signed | Unsigned } type Float interface { ~float32 | ~float64 } type Complex interface { ~complex64 | ~complex128 } * Constaints & type sets [T aConstraint] - aConstraint is an interface - interface has a type set - type set defines the types that are permissible * Constaint literals [S interface{~[]E}, E interface{}] - interface{E} could be rewritten as E in a constraint [S ~[]E, E interface{}] - any is a new predeclared identifier — an alias for interface{} in a constraint [S ~[]E, E any] * Type inference - Type inference is complicated but usage is simple - Programms that don't need type arguments today won't need them tomorrow * Scale type Point []uint32 func (p Point) String() string { return "" } We'd like to write function ScaleAndPrint func ScaleAndPrint(p Point) { r := Scale(p, 2) fmt.Println(r.String()) } * Scale, first attempt .play -edit scale/wrong/scale.go /^func Scale/,/^}/ * Scale, first attempt .play -edit scale/wrong/scale.go /^func Scale/,/^}/ func ScaleAndPrint(p Point) { r := Scale(p, 2) fmt.Println(r.String()) } - Compiler error: (type []uint32 has no field or method String) * Scale, fixed .play -edit scale/fixed/scale.go /^func Scale/,/^}/ * Scale, fixed func Scale[S ~[]E, E constraints.Integer](s S, c E) S vs func Scale[E constraints.Integer](s []E, c E) []E * Inference func ScaleAndPrint(p Point) { r := Scale(p, 2) fmt.Println(r.String()) } Why don't we need explicit type parameters? r := Scale[Point, int32](p, 2) * Inference - argument type inference - constraint type inference * Argument type inference func Scale[S ~[]E, E constraints.Integer](s S, c E) S type Point []int32 Scale(p, 2) - p is []Point => S is Point - 2 is untyped constant => no info * Constraint type inference func Scale[S ~[]E, E constraints.Integer](s S, c E) S type Point []int32 Scale(p, 2) - S is Point (argument type inference) - S is defined in terms of E => we can infer E - E is int32 * Different type parameters are different types func invalid[Tx, Ty Ordered](x Tx, y Ty) Tx { ... if x < y { ...// INVALID ... } - "<" requires that both operands have the same type * Relationships between type parameters type Pointer[T any] interface { *T } func f[T any, PT Pointer[T]](p PT) or with inlined constraint func foo[T any, PT interface{*T}](p PT) * When to use generics - Improved static type safety. - More efficient memory use. - (Significantly) better performance. * Summary Generics are type-checked macros. Declarations - Type parameter lists are like ordinary parameter lists with "[" "]". - Function and type declarations may have type parameter lists. - Type parameters are constrained by interfaces. Use - Generic functions and types must be instantiated when used. - Type inference (if applicable) makes function instantiation implicit. - Instantiation is valid if the type arguments satisfy their constraints. * Ссылки .link https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-contracts.md - generics design draft .link https://blog.golang.org/why-generics - The Go Blog - Why Generics? .link https://www.youtube.com/watch?v=TborQFPY2IM - GopherCon 2020, Robert Griesemer - Typing [Generic] Go