Релиз версии 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, готова.

Комментарии (3)


  1. manyakRus
    24.10.2024 08:16

    это всё для тех кто любит программировать в стиле "взрыв мозга" или "вырви глаз"


  1. quibex
    24.10.2024 08:16

    так го с каждой версией теряет свою главную фишку - простоту


    1. Deissh
      24.10.2024 08:16

      Теряет, но в тоже время остается верен некоторым концепциям, благодаря которым он все еще прост и понятен.