Go: дженерики

Вопросы написания собственного программного кода (на любых языках)

Модератор: Olej

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go: дженерики

Непрочитанное сообщение Olej » 07 мар 2023, 18:24

Дженерики в 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

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go: дженерики

Непрочитанное сообщение Olej » 07 мар 2023, 18:26

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

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

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

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go: дженерики

Непрочитанное сообщение Olej » 07 мар 2023, 19:01

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) 
Вложения
print_slice.go
(555 байт) 22 скачивания

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go: дженерики

Непрочитанное сообщение Olej » 11 мар 2023, 14:49

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
...
Механизм выведения типа сложен, но применять его просто: выведение типа либо происходит, либо нет. Если тип выводится, типы-аргументы можно опустить — тогда вызов параметризованных функций ничем не отличается от вызова обычных функций. Если выведение типа не происходит, в компиляторе выдаётся сообщение об ошибке — тогда мы можем просто указать необходимые типы-аргументы.
Вложения
print_slice.0.go
(590 байт) 22 скачивания

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go: дженерики

Непрочитанное сообщение Olej » 12 мар 2023, 01:00

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 — ограничивающий типы, для которых определены операторы сравнения на равенство и неравенство.
Вложения
exist.go
(508 байт) 22 скачивания

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go: дженерики

Непрочитанное сообщение Olej » 12 мар 2023, 01:12

Следующий пример: поиск большего из 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, содержащего достаточно много частных констрейнтов на разные случаи.
Но тут не всё так просто...
Вложения
max.go
(291 байт) 21 скачивание

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go: дженерики

Непрочитанное сообщение Olej » 12 мар 2023, 01:47

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
Мы получили готовое приложение.

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go: дженерики

Непрочитанное сообщение Olej » 12 мар 2023, 01:50

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
А эта ошибка напоминает нам о том, что для комплексных числовых значений не определены соотношения больше-меньше (эти операции имеют смысл, например, для модулей комплексных векторов, но не самих значений).
И это убедительно показывает как работает механизм ограничения типа в дженериках.

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go: дженерики

Непрочитанное сообщение Olej » 12 мар 2023, 01:54

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:

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Go: дженерики

Непрочитанное сообщение Olej » 12 мар 2023, 01:57

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

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

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

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

Ответить

Вернуться в «Программирование»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 8 гостей