shad-go/lectures/08-generics/lecture.slide
2023-04-02 01:02:32 +04:00

501 lines
9.7 KiB
Text

Generics
Лекция 8
Арсений Балобанов
* Generics
* Generics 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.
* 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{}")
Moreover,
type any = interface{}
* Sort, decomposed
type Lesser[T any] interface{
Less(y T) bool
}
func Sort[Elem Lesser[Elem]](list []Elem)
* Problem
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 }
but what if ...
* Problem
there is one nice solution
// orderedSlice is an internal type that implements sort.Interface.
// The Less method uses the < operator. The Ordered type constraint
// ensures that T has a < operator.
type orderedSlice[T constraints.Ordered] []T
func (s orderedSlice[T]) Len() int { return len(s) }
func (s orderedSlice[T]) Less(i, j int) bool { return s[i] < s[j] }
func (s orderedSlice[T]) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func Sort[T constraints.Ordered](s []T) {
sort.Sort(orderedSlice[T](s))
}
* min
.play -edit min/basic/min.go /^func min/,/^}/
* Generic 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 any] 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 types T can be
* constraints.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
* constraints
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
}
* comparable
built-in identifier for anything that can be compared via ==
func SetFrom[T comparable](s []T) map[T]struct{} {
m := make(map[T]struct{}, len(s))
for _, v := range s {
m[v] = struct{}{}
}
return m
}
since go 1.20 comparable also allows interfaces
* Constraints & type sets
[T aConstraint]
- aConstraint is an interface
- interface has a type set
- type set defines the types that are permissible
* Constraints & type sets
type OrderedStringer interface {
constraints.Ordered // Type set
fmt.Stringer // And stringer as well
}
type Int int
func (i Int) String() string { return strconv.Itoa(int(i)) }
func MaxString[T OrderedStringer](a, b T) string {
if a > b {
return a.String()
}
return b.String()
}
* Constraint literals
[S interface{~[]E}, E interface{}]
- interface{E} could be rewritten as E in a constraint
[S ~[]E, E interface{}]
- any is a predeclared identifier — an alias for interface{} in a constraint
[S ~[]E, E any]
* Type inference
- Type inference is complicated but usage is simple
- Programs 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 => E is numeric
* 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 f[T any, PT interface{*T}](p PT)
* Output type instantiation
func CallJSONRPC[Output any](method string) (Output, error) {
var output Output
resBytes, err := doCall(method)
if err != nil {
return output, err
}
err = json.Unmarshal(resBytes, &output)
return output, err
}
now we don't need to write boilerplate for unmarshalling
res, err := CallJSONRPC[BatchReadResponse]("batch_read")
but no type inference in this case
* When to use generics
- Improved static type safety.
- More efficient memory use.
- (Significantly) better performance.
* When not to use generics
we can write
func Concat[T fmt.Stringer](a, b T) string {
return a.String() + b.String()
}
but why not just
func Concat(a, b fmt.Stringer) string {
return a.String() + b.String()
}
P.S. are these functions equivalent?
- Type parameter be replaced by simple interface
* When not to use generics
.play skip/skip.go
just skip this slide...
* Some problems
func Smallest[E ~[]T, T constraints.Ordered](e E) (T, error) {
if len(e) == 0 {
var zero T
return zero, errors.New("empty slice provided")
}
s := e[0]
for _, v := range e[1:] {
is v < s {
s = v
}
}
return s, nil
}
- No way to inline zero value
* Some problems
func Mul[T string | int](t T, cnt int) T {
switch v := any(t).(type) {
case string:
v = strings.Repeat(v, cnt)
return *(*T)(unsafe.Pointer(&v))
case int:
v *= cnt
return *(*T)(unsafe.Pointer(&v))
}
panic("impossible type")
}
- No way to determine the instantiated type statically
- No function overloading
- No way to express convertibility
* 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/43651-type-parameters.md - generics design proposal
.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