Развлекаемся с итераторами в Go

от автора

Релиз версии 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.

Each

Следующий метод, который я хочу реализовать – 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

Reverse

Способ получить обратный итератор: сначала нужно собрать все элементы, а затем создать новый итератор из собранного слайса. К счастью, у нас есть функции, которые делают именно это:

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

Map

Мутирование каждого элемента итератора также необходимо:

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, таким образом осуществляя отображение по итератору.

Filter

После 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, готова.


ссылка на оригинал статьи https://habr.com/ru/articles/852940/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *