Страница 1 из 2

Go: дженерики

Добавлено: 07 мар 2023, 18:24
Olej
Дженерики в Go появились совсем недавно, в версии 1.18, март 2022г. ... причём, в версии 1.18 утверждалось, что они ещё могут претерпеть заметные изменения в реализации.
Дженерики в языке Go
2 июн 2021
Как вы уже наверняка знаете, proposal по дженерикам в Golang принят (официально это называется type parameters) и будет имплементирован в go 1.18. Бета будет доступна уже в конце этого года. А это значит, что пора разобраться, на чём в итоге остановились разработчики языка — ведь черновик type parameters постоянно менялся в течение последних лет.
Нужно ли усложнять язык дженериками?

Вопрос дискуссионный. Мнения разделились.
Как известно, язык Go изначально был заточен под максимальную простоту, и обобщение типов может усложнить читабельность кода. Многие противопоставляют Go языку Java, традиционно наполненному обобщениями различного рода, и дженерики — это как первый шаг в эту сторону.
С другой стороны, если надо написать универсальную библиотеку для каких-то универсальных целей, то придётся использовать interface{} или кодогенерацию, а это тоже в общем-то читабельности и надёжности не добавляет. Также необходимо отметить, что разработчики языка сделали всё возможное, чтобы дженерики выглядели и использовались как можно проще. Намного проще, чем в других языках.
Введение в использование дженериков в Golang
понедельник, 2 мая 2022 г.
Дженерики — это самое большое изменение, которое было внесено в Go с момента первого релиза с открытым исходным кодом. В этом посте вы познакомитесь с новыми функциями языка.
...
Дженерики — это способ написания кода, который не зависит от используемых конкретных типов. Функции и типы теперь могут быть написаны для использования любого набора типов.
В документации GoLang: Tutorial: Getting started with generics

Go: дженерики

Добавлено: 07 мар 2023, 18:26
Olej
Olej писал(а):
07 мар 2023, 18:24
Дженерики в Go появились
С чего начинаем?
Конечно же - с проверки используемой версии Go :lol: :

Код: Выделить всё

olej@R420:~$ go version
go version go1.20rc2 linux/amd64

Go: дженерики

Добавлено: 07 мар 2023, 19:01
Olej
Olej писал(а):
07 мар 2023, 18:26
С чего начинаем?
Синтаксически это выглядит так - простейший пример:

Код: Выделить всё

olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ go fmt print_slice.go 
print_slice.go
Программа, распечатающая слайс (срез массива) переменных любого типа:

Код: Выделить всё

package main

import "fmt"

func PrintSlice[T any](s []T) {
	for _, v := range s {
		fmt.Printf("%v ", v)
		//fmt.Printf("площадь = %.2f\n", многоугольник.square())
	}
	fmt.Println()
}

func main() {
	срез_str := []string{"Hello", "world"}
	PrintSlice(срез_str)
	срез_int8 := []int8{9, 8, 7, 6, 5, 4}
	PrintSlice(срез_int8)
	срез_float32 := []float32{9.0, 8.1, 7.2, 6.3, 5.4, 4.5}
	PrintSlice(срез_float32)
	срез_complex128 := []complex128{9 + 0i, 8 + 1i, 7 + 2i, 6 + 3i}
	PrintSlice(срез_complex128)
}
Вообще то, правильно полностью вызов PrintSlice следовало бы записывать так: PrintSlice[string](срез_str) ... но во многих случаях компилятор может сам сделать вывод типа из переданных аргументов.

Код: Выделить всё

olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ go build print_slice.go 

Код: Выделить всё

olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ ./print_slice 
Hello world 
9 8 7 6 5 4 
9 8.1 7.2 6.3 5.4 4.5 
(9+0i) (8+1i) (7+2i) (6+3i) 

Go: дженерики

Добавлено: 11 мар 2023, 14:49
Olej
Olej писал(а):
07 мар 2023, 19:01
Вообще то, правильно полностью вызов PrintSlice следовало бы записывать так: PrintSlice[string](срез_str) ... но во многих случаях компилятор может сам сделать вывод типа из переданных аргументов.
Синтексически полный вызоов параметризированных (дженерик) функций должен бы записываться так:

Код: Выделить всё

package main

import "fmt"

func PrintSlice[T any](s []T) {
	for _, v := range s {
		fmt.Printf("%v ", v)
		//fmt.Printf("площадь = %.2f\n", многоугольник.square())
	}
	fmt.Println()
}

func main() {
	срез_str := []string{"Hello", "world"}
	PrintSlice[string](срез_str)
	срез_int8 := []int8{9, 8, 7, 6, 5, 4}
	PrintSlice[int8](срез_int8)
	срез_float32 := []float32{9.0, 8.1, 7.2, 6.3, 5.4, 4.5}
	PrintSlice[float32](срез_float32)
	срез_complex128 := []complex128{9 + 0i, 8 + 1i, 7 + 2i, 6 + 3i}
	PrintSlice[complex128](срез_complex128)
}

Код: Выделить всё

olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ go build -o print_slice print_slice.0.go

olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ ls -l print_slice
-rwxrwxr-x 1 olej olej 1858687 мар 11 13:05 print_slice

Код: Выделить всё

olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ ./print_slice
Hello world
9 8 7 6 5 4
9 8.1 7.2 6.3 5.4 4.5
(9+0i) (8+1i) (7+2i) (6+3i)
Но тут на помощь (упрощение) приходит выведение типов, как показано раньше. Это сильно напоминает то, как делается выведение типов в C++ в стандартах C++11/C++14 (что особенно хорошо видно на примерах касающихся контейнеров STL).

Но выведение типов возможно и работает не всегда. Тут очень интересно, что на этот счёт пишут сами авторы разработки GoLang - Дженерики в Go — подробности из блога разработчиков:
29.03.22 15:43
...
Механизм выведения типа сложен, но применять его просто: выведение типа либо происходит, либо нет. Если тип выводится, типы-аргументы можно опустить — тогда вызов параметризованных функций ничем не отличается от вызова обычных функций. Если выведение типа не происходит, в компиляторе выдаётся сообщение об ошибке — тогда мы можем просто указать необходимые типы-аргументы.

Go: дженерики

Добавлено: 12 мар 2023, 01:00
Olej
Olej писал(а):
11 мар 2023, 14:49

Код: Выделить всё

func PrintSlice[T any](s []T) { ... }
То, что во всех примерах выше указывалось в скобках […] за обозначением типа T — это ограничения типа (constraints, констрейнты), условия которым должен удовлетворять обобщённый тип T. Это ограничение типа может быть как любым привычным интерфейсом Go, а может быть интерфейсом, перечисляющим полный список типов, для которых интерфейс может быть использован:

Код: Выделить всё

type MyConstraint interface {
   int | int8 | int16 | int32 | int64
}
Следующий пример:

Код: Выделить всё

Смотрим следующий пример: поиск присутствия элемента в срезе (массиве)
exist.go :
package main

import "fmt"

func existsInSlice[T comparable](val T, values []T) bool {
	for _, v := range values {
		if val == v {
			return true
		}
	}
	return false
}

func showIn[T comparable](val T, values []T) {
	fmt.Printf("%v in %v => %v\n", val, values, existsInSlice(val, values))
}

func main() {
	showIn("worlds", []string{"Hello", "world"})
	showIn(7, []int8{9, 8, 7, 6, 5, 4})
	showIn(8.3, []float32{9.0, 8.1, 7.2, 6.3, 5.4, 4.5})
	showIn(7 + 2i, []complex128{9 + 0i, 8 + 1i, 7 + 2i, 6 + 3i})
}

Код: Выделить всё

$ go build exist.go

Код: Выделить всё

$ ./exist
worlds in [Hello world] => false
7 in [9 8 7 6 5 4] => true
8.3 in [9 8.1 7.2 6.3 5.4 4.5] => false
(7+2i) in [(9+0i) (8+1i) (7+2i) (6+3i)] => true
Здесь в качестве ограничения типа указан встроенный интерфейс Go comparable — ограничивающий типы, для которых определены операторы сравнения на равенство и неравенство.

Go: дженерики

Добавлено: 12 мар 2023, 01:12
Olej
Следующий пример: поиск большего из 2-х значений любого типа (файл max.go ).

Код: Выделить всё

package main

import "fmt"
import "golang.org/x/exp/constraints"

func Max[T constraints.Ordered](a T, b T) T {
	if a > b {
		return a
	}
	return b
}

func main() {
	fmt.Println(Max("Hello", "world"))
	fmt.Println(Max(9, 4))
	fmt.Println(Max(4.5, 5.4))
	// fmt.Println(Max(8 + 1i, 7 + 2i))
}
Здесь ограничение типа (констрейнт) импортируется из соответствующего пакета constraints, содержащего достаточно много частных констрейнтов на разные случаи.
Но тут не всё так просто...

Go: дженерики

Добавлено: 12 мар 2023, 01:47
Olej
Olej писал(а):
12 мар 2023, 01:12
Но тут не всё так просто...
Если мы не создадим описание модуля и не осуществим импорт (загрузку) пакета constraints из внешнего сетевого репозитория, как это пошагово описано здесь: Go: модули - мы будем упорно ловить терминальную ошибку компиляции.

А вот когда мы проделаем всё там описанное, то:

Код: Выделить всё

olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ go fmt max.go
max.go

Код: Выделить всё

olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ go build max.go

Код: Выделить всё

olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ ls -l max
-rwxrwxr-x 1 olej olej 1839159 мар 11 16:17 max

Код: Выделить всё

olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ ./max 
world
9
5.4
Мы получили готовое приложение.

Go: дженерики

Добавлено: 12 мар 2023, 01:50
Olej
Olej писал(а):
12 мар 2023, 01:47
Мы получили готовое приложение.
А вот если мы раскомментируем 4-ю строку вызовов, то получим:

Код: Выделить всё

olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ go build max.go 
# command-line-arguments
./max.go:17:17: complex128 does not implement constraints.Ordered (complex128 missing in ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string)

Неожиданно? :-o
А эта ошибка напоминает нам о том, что для комплексных числовых значений не определены соотношения больше-меньше (эти операции имеют смысл, например, для модулей комплексных векторов, но не самих значений).
И это убедительно показывает как работает механизм ограничения типа в дженериках.

Go: дженерики

Добавлено: 12 мар 2023, 01:54
Olej
Olej писал(а):
12 мар 2023, 01:12
Здесь ограничение типа (констрейнт) импортируется из соответствующего пакета constraints, содержащего достаточно много частных констрейнтов на разные случаи.
В завершение хорошо бы посмотреть те конкретные конкрейты (категории типов), которые определены в пакете constraints:

Код: Выделить всё

olej@R420:~/2023/own.BOOKs/BHV.Go.2/examples/generic$ tree `go env GOPATH`/pkg/mod/golang.org/x/exp@v0.0.0-20230310171629-522b1b587ee0/constraints 
/home/olej/go/pkg/mod/golang.org/x/exp@v0.0.0-20230310171629-522b1b587ee0/constraints
├── constraints.go
└── constraints_test.go

0 directories, 2 files
Вот они:

Код: Выделить всё

olej@R420:~/go/pkg/mod/golang.org/x/exp@v0.0.0-20230310171629-522b1b587ee0/constraints$ grep ^type `go env GOPATH`/pkg/mod/golang.org/x/exp@v0.0.0-20230310171629-522b1b587ee0/constraints/constraints.go
type Signed interface {
type Unsigned interface {
type Integer interface {
type Float interface {
type Complex interface {
type Ordered interface {
Дополнительных объяснений тут не надо. :lol:

Go: дженерики

Добавлено: 12 мар 2023, 01:57
Olej
Ну и наконец...
Параметризация типов может использоваться не только с функциями (и, возможно, методами), но также и с ново образуемыми типами:

Код: Выделить всё

// новый тип
type Tree[T interface{}] struct {
    left, right *Tree[T]
    value       T
}

// метод этого же типа
func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }