Развлекаемся с итераторами в Go
- четверг, 24 октября 2024 г. в 00:00:06
Релиз версии Go 1.23 добавил поддержку итераторов и пакет iter. Теперь можно перебирать константы, контейнеры (map, slice, array, string) и функции. Сначала создание итератора показалось мне неудобным, хотя в то же время его использование выглядело простым.
Моя проблема с подходом к итераторам в Go заключается в том, что их нельзя «связывать» так,как это можно делать в JavaScript:
[1,2,3,4]
.reverse()
.map(e => e*e)
.filter(e => e % 2 == 0)
.forEach(e => console.log(e)) Написание аналогичной конструкции на Go потребует цепочки из 5 вызовов функций:
slices.ForEach(
slices.Filter(
slices.Map(
slices.Reverse(slices.All([]int{1,2,3,4})),
func(i int) int { return i * i},
),
func(i int) bool { return i % 2 == 0 }
),
func(i int) { fmt.Println(i) }
)Это пример, в пакете slices нет функций Map, Filter или ForEach.
Поскольку я испытываю сильную неприязнь к написанию цепочек “функциональных” операций подобным образом (смотрю на тебя, Python, не набрасывайтесь на меня, хаскельщики), я хотел использовать новые итераторы и пакет iter, обернув их в структуру, которая позволяла бы писать чистую и аккуратную цепочку операций, как это реализовано в JavaScript.
Ниже приведены те же операции, но вместо использования пакетов iter и slices я использую свою абстракцию:
func TestIterator(t *testing.T) {
From([]int{1, 2, 3, 4}).
Reverse().
Map(func(i int) int { return i * i }).
Filter(func(i int) bool { return i%2 == 0 }).
Each(func(a int) { println(a) })
// 16
// 4
}Давайте взглянем на реализацию, представляю вам структуру Iterator. Она оборачивает итератор (*Iterator).iter, что позволяет вызывать функции этой структуры, вместо передачи каждой функции итератора в качестве параметра следующей.
type Iterator[V any] struct {
iter iter.Seq[V]
}Давайте взглянем на первые функции, которые приходят на ум, когда мы говорим об итераторах: создание итератора из слайса и сбор его обратно в слайс:
func (i Iterator[V]) Collect() []V {
collect := make([]V, 0)
for e := range i.iter {
collect = append(collect, e)
}
return collect
}
func From[V any](slice []V) *Iterator[V] {
return &Iterator[V]{
iter: func(yield func(V) bool) {
for _, v := range slice {
if !yield(v) {
return
}
}
},
}
}Первая функция максимально проста – создаем слайс, используем итератор, добавляем каждый элемент и возвращаем слайс. Вторая подчеркивает странный способ создания итераторов в Go. Давайте сначала посмотрим на сигнатуру: мы возвращаем указатель на структуру, чтобы вызывающий код мог вызывать все методы без необходимости использовать временную переменную для каждого вызова. В самой функции итератор создается путем возврата замыкания, которое выполняет цикл по параметру и возвращает результат, который останавливает итератор, когда функция yield возвращает false.
Следующий метод, который я хочу реализовать – ForEach / Each. Он просто вызывает переданную функцию для каждого элемента итератора.
func (i *Iterator[V]) Each(f func(V)) {
for i := range i.iter {
f(i)
}
}Пример использования:
From([]int{1, 2, 3, 4}).Each(func(a int) { println(a) })
// 1
// 2
// 3
// 4Способ получить обратный итератор: сначала нужно собрать все элементы, а затем создать новый итератор из собранного слайса. К счастью, у нас есть функции, которые делают именно это:
func (i *Iterator[V]) Reverse() *Iterator[V] {
collect := i.Collect()
counter := len(collect) - 1
for e := range i.iter {
collect[counter] = e
counter--
}
return From(collect)
}Пример использования:
From([]int{1, 2, 3, 4}).Reverse().Each(func(a int) { println(a) })
// 4
// 3
// 2
// 1Мутирование каждого элемента итератора также необходимо:
func (i *Iterator[V]) Map(f func(V) V) *Iterator[V] {
cpy := i.iter
i.iter = func(yield func(V) bool) {
for v := range cpy {
v = f(v)
if !yield(v) {
return
}
}
}
return i
}Сначала мы копируем предыдущий итератор. Делая это, мы избегаем переполнения стека, ссылаясь на итератор i.iter в самом итераторе. Метод Map работает, переписывая i.iter новым итератором, который обрабатывает каждое поле копии итератора и заменяет значение итератора результатом передачи v в f, таким образом осуществляя отображение по итератору.
После Map, возможно, самым часто используемым методом функционального API является Filter. Давайте взглянем на нашу последнюю операцию:
func (i *Iterator[V]) Filter(f func(V) bool) *Iterator[V] {
cpy := i.iter
i.iter = func(yield func(V) bool) {
for v := range cpy {
if f(v) {
if !yield(v) {
return
}
}
}
}
return i
}Аналогично Map, мы копируем итератор и вызываем f с v в качестве параметра для каждого элемента. Если f возвращает true, мы сохраняем элемент в новом итераторе.
slices и пакет iter отлично работают вместе с системой дженериков, введенной в Go 1.18.
Хотя этот вариант API прощен в использовании, я понимаю почему команда Go реализовала итераторы по другому. Ниже приведены тесты, которые служат примерами, и результаты их выполнения.
package iter1
import (
"fmt"
"testing"
"unicode"
)
func TestIteratorNumbers(t *testing.T) {
From([]int{1, 2, 3, 4}).
Reverse().
Map(func(i int) int { return i * i }).
Filter(func(i int) bool { return i%2 == 0 }).
Each(func(a int) { println(a) })
}
func TestIteratorRunes(t *testing.T) {
r := From([]rune("Hello World!")).
Reverse().
// remove all spaces
Filter(func(r rune) bool { return !unicode.IsSpace(r) }).
// convert every rune to uppercase
Map(func(r rune) rune { return unicode.ToUpper(r) }).
Collect()
fmt.Println(string(r))
}
func TestIteratorStructs(t *testing.T) {
type User struct {
Id int
Name string
Hash int
}
u := []User{
{0, "xnacly", 0},
{1, "hans", 0},
{2, "gedigedagedeio", 0},
}
From(u).
// computing the hash for each user
Map(func(u User) User {
h := 0
for i, r := range u.Name {
h += int(r)*31 ^ (len(u.Name) - i - 1)
}
u.Hash = h
return u
}).
Each(func(u User) { fmt.Printf("%#+v\n", u) })
}Результаты запуска:
$ go test ./... -v
=== RUN TestIteratorNumbers
16
4
--- PASS: TestIteratorNumbers (0.00s)
=== RUN TestIteratorRunes
!DLROWOLLEH
--- PASS: TestIteratorRunes (0.00s)
=== RUN TestIteratorStructs
&iter1.User{Id:0, Name:"xnacly", Hash:20314}
&iter1.User{Id:1, Name:"hans", Hash:13208}
&iter1.User{Id:2, Name:"gedigedagedeio", Hash:44336}
--- PASS: TestIteratorStructs (0.00s)
PASS
ok iter1 0.263sВот и все, обертка в стиле JavaScript над iter и slices, готова.