Generics
Лекция 10
Максим Иванов
Максим Иванов
Сейчас:
func Sort(data Interface)
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}Что хотим:
func Sort(list []Elem) // use Sort(myList)
func Sort[Elem ?](list []Elem)
Параметризованный тип — это объявление типа с параметрами типа в квадратных скобках (например, `[T any]`),
из которого компилятор может получить множество конкретных типов, подставляя вместо параметров реальные типы (Sort[int], Sort[string] и т.п.).
Аргументы функции
(x, y aType, z anotherType)
Type параметры
[P, Q aConstraint, R anotherConstraint]
func Sort[Elem interface{ Less(y Elem) bool }](list []Elem) {
...
}Код в библиотеке:
func Sort[Elem interface{ Less(y Elem) bool }](list []Elem)Код пользователя:
type book struct{...}
func (x book) Less(y book) bool {...}
var bookshelf []book
...
Sort[book](bookshelf) // generic function callЧто происходит, когда мы вызываем Sort?
Sort[book](bookshelf)
Sort[Elem interface{ Less(y Elem) bool }] | (list []Elem)
Sort[book interface{ Less(y book) bool }] | (list []book)Sort[book] | (list []book)
Инстанцирование (новое)
Вызов (как обычно)
type Lesser[T any] interface{
Less(y T) bool
}type Lesser[T any] interface{
Less(y T) bool
}
func Sort[Elem Lesser[Elem]](list []Elem)Что хотим:
Sort([]int{1, 2, 3})int не реализует "ограничение" Elem (нет метода Less)
Что можно было бы сделать
type myInt int
func (x myInt) Less(y myInt) bool { return x < y }Но что если ...
14Есть одно аккуратное решение
// orderedSlice — это внутренний тип, который реализует sort.Interface.
// Метод Less использует оператор <. Ограничение типа Ordered
// гарантирует, что для T определён оператор <.
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))
}func min(x, y int) int {
if x < y {
return x
}
return y
}func min[T constraints.Ordered](x, y T) T {
if x < y {
return x
}
return y
}calling generic min
m := min[int](1, 2)
fmin := min[float64] // non generic m := fmin(2.71, 3.14)
компилятор создаёт специализированный вариант функции или типа.
18type Tree[T any] struct {
left, right *Tree[T]
data T
}
func (t *Tree[T]) Lookup(x T) *Tree[T]
var stringTree Tree[string]T объявлен в `type Tree[T any]`, поэтому он виден и в определении структуры, и во всех её методах (как в Lookup).func min(x, y float64) float64
func min[T constraints.Ordered](x, y T) T {// Ordered — это "ограничение", которое допускает любой упорядоченный тип:
// любой тип, который поддерживает операторы < <= >= >.
// Если в будущих версиях Go появятся новые упорядоченные типы,
// в это "ограничение" будут добавлены новые типы.
type Ordered interface {
Integer | Float | ~string
}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 - встроенный идентификатор для всего, что можно сравнить с помощью ==
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
}начиная с Go 1.20 comparable также разрешён для интерфейсов
24type OrderedStringer interface {
constraints.Ordered // набор типов
fmt.Stringer // и интерфейс Stringer
}
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()
}[S interface{~[]E}, E interface{}][S ~[]E, E interface{}][S ~[]E, E any]
type Point []uint32
func (p Point) String() string { return "" }Хотим написать функцию ScaleAndPrint
func ScaleAndPrint(p Point) {
r := Scale(p, 2)
fmt.Println(r.String())
}import "golang.org/x/exp/constraints"
type Point []uint32
func (p Point) String() string { return "" }
func Scale[E constraints.Integer](s []E, c E) []E {
r := make([]E, len(s))
for i, v := range s {
r[i] = v * c
}
return r
}
func ScaleAndPrint(p Point) {
r := Scale(p, 2)
fmt.Println(r.String())
}// Generic функция
func Scale[E constraints.Integer](s []E, c E) []E
// Подстановка E = uint32 (Point — это []uint32)
func Scale[uint32 constraints.Integer](s []uint32, c uint32) []uint32
// После проверки ограничений остаётся конкретная функция
func ScaleUint32(s []uint32, c uint32) []uint32 {
r := make([]uint32, len(s))
for i, v := range s {
r[i] = v * c
}
return r
}p имеет тип Point, но `Scale(p, 2)` возвращает []uint32, поэтому r имеет тип []uint32,
а у среза []uint32 нет метода String, из‑за этого r.String() в ScaleAndPrint не компилируется.func Scale[S ~[]E, E constraints.Integer](s S, c E) S {
r := make(S, len(s))
for i, v := range s {
r[i] = v * c
}
return r
}// Generic функция
func Scale[S ~[]E, E constraints.Integer](s S, c E) S
// Подстановка S = Point, E = uint32 (Point — это []uint32)
func Scale[Point ~[]uint32, uint32 constraints.Integer](s Point, c uint32) Point
// После проверки ограничений остаётся конкретная функция
func ScalePoint(s Point, c uint32) Point {
r := make(Point, len(s))
for i, v := range s {
r[i] = v * c
}
return r
}
// Теперь r имеет тип Point, и у Point есть метод String, поэтому r.String() компилируется.func Scale[S ~[]E, E constraints.Integer](s S, c E) S
vs
func Scale[E constraints.Integer](s []E, c E) []E
func ScaleAndPrint(p Point) {
r := Scale(p, 2)
fmt.Println(r.String())
}Почему нам не нужно явно указывать type параметры?
r := Scale[Point, int32](p, 2)
func Scale[S ~[]E, E constraints.Integer](s S, c E) S type Point []int32 Scale(p, 2)
func Scale[S ~[]E, E constraints.Integer](s S, c E) S type Point []int32 Scale(p, 2)
func invalid[Tx, Ty Ordered](x Tx, y Ty) Tx {
...
if x < y { ...// INVALID
...
}type Pointer[T any] interface {
*T
}
func f[T any, PT Pointer[T]](p PT)или
func f[T any, PT interface{*T}](p PT)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
}Теперь нам не нужно руками писать шаблонный код для unmarshal:
res, err := CallJSONRPC[BatchReadResponse]("batch_read")BatchReadResponse нужно указывать явно — type inference нет.Все это по сравнению с any
41мы можем написать
func Concat[T fmt.Stringer](a, b T) string {
return a.String() + b.String()
}но почему бы не написать просто
func Concat(a, b fmt.Stringer) string {
return a.String() + b.String()
}P.S. эквивалентны ли эти функции?
package main import "fmt" func F[T ~[]T](t T) T { return t[1][3][3][7][6][6][6] } type G []G func main() { g := make(G, 10) for i := range g { g[i] = g } fmt.Println(F(g)) }
Не делайте так...
43func 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
}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")
}Generic-и — это «макросы», которые проверяются и разворачиваются по типам на этапе компиляции.
- предложение по добавлению generic-ов
- блог Go: «Зачем нужны generic-и?»
- GopherCon 2020, Robert Griesemer — доклад "Typing [Generic] Go"
47Максим Иванов