golang

Делаем себе сложно

  • воскресенье, 2 августа 2026 г. в 00:00:17
https://habr.com/ru/articles/1065634/

В пакете стандартной библиотеки slices есть функция Backward — итератор по элементам среза в обратном порядке:

// Backward returns an iterator over index-value pairs in the slice,
// traversing it backward with descending indices.
func Backward[Slice ~[]E, E any](s Slice) iter.Seq2[int, E]

Если вы не сильно погружены в тему дженериков и итераторов, то естественная реакция при взгляде на сигнатуру этой функции (и других из пакета slices) — «а что, нельзя попроще как-то было?».

Чтобы ответить на этот вопрос, проведем воображаемый эксперимент. Представим себя в роли нашего далекого предка, живущего в доитераторную эпоху, который решает реализовать Backward с нуля.

Наш воображаемый предок не работает в гугле, так что не проецируйте его решения на команду разработки Go. У них были свои соображения, а вот Jira не было.

1. Срез наоборот

Приятный летний солнечный день, поют птички. Вы как всегда за клавиатурой, и вдруг решаете написать функцию для обхода среза в обратном порядке. Это всяко приятней, чем делать очередную задачу из Jira.

// Backward возвращает срез в обратном порядке.
func Backward[T any](s []T) []T {
    n := len(s)
    res := make([]T, n)
    for i := n - 1; i >= 0; i-- {
        res[n-1-i] = s[i]
    }
    return res
}

Пример вызова:

s := []int{11, 22, 33, 44, 55}
b := Backward(s)
fmt.Println(b)
// [55 44 33 22 11]

Реализация простая, работает уверенно. Один только недочет: Backward создает копию среза, что может быть расточительно при большом N.

К тому же солнце скрылось за тучкой, и как будто собирается дождь. Вы решаете еще поработать.

2. Дай-дай-дай

Чтобы не создавать копию среза, вы решаете возвращать функцию-замыкание, которая знает текущую позицию в исходном срезе и при каждом вызове возвращает нужный элемент:

// Backward возвращает функцию, которая при каждом вызове
// возвращает следующий элемент среза (в обратном порядке)
// и признак продолжения итерации (false - завершение).
func Backward[T any](s []T) func() (T, bool) {
    i := len(s)
    return func() (T, bool) {
        if i == 0 {
            var zero T
            return zero, false
        }
        i--
        return s[i], true
    }
}

Пример вызова:

s := []int{11, 22, 33, 44, 55}
next := Backward(s)
for {
    v, ok := next()
    if !ok {
        break
    }
    fmt.Print(v, " ")
}
fmt.Println()
// 55 44 33 22 11

Теперь выделяется O(1) памяти вместо O(n). Так-то лучше.

Прежде чем продолжить, вы бросаете взгляд за окно. Да, точно, начался дождь, а туч только прибавилось. Отличная погода для работы!

3. Итератор на колбэках

Что-то в примере вызова не дает вам покоя. Он получился уж очень… императивный. Хочется переложить организацию цикла на Backward, а клиенту оставить только логику приложения (то что делаем с элементами среза).

Вы решаете немного усложнить сигнатуру Backward. Теперь она будет возвращать функцию-итератор, которая принимает в качестве аргумента колбэк и применяет его к каждому элементу среза:

// Backward возвращает функцию, которая принимает функцию-колбэк yield.
// Колбэк вызывается для каждого элемента среза (в обратном порядке).
func Backward[T any](s []T) func(yield func(T) bool) {
    return func(yield func(T) bool) {
        for i := len(s) - 1; i >= 0; i-- {
            if !yield(s[i]) {
                return
            }
        }
    }
}

Функция yield возвращает bool — это чтобы колбэк мог сигнализировать, если хочет остановить обход досрочно.

Теперь тело for из примера вызова можно сделать такой функцией-колбэком и избавиться от цикла в принципе:

work := func(x int) bool {
    if x < 30 {
        return false // досрочный выход
    }
    fmt.Print(x, " ")
    return true
}

s := []int{11, 22, 33, 44, 55}
it := Backward(s)
it(work)
fmt.Println()
// 55 44 33

Ммм, очень функционально.

Небольшой нюанс: Backward стала выглядеть немного тяжеловесно. Вы добавляете отдельный тип для возвращаемого значения:

// Seq - итератор по значениям типа T.
// При вызове вида seq(yield) вызывает yield(v) для каждого значения v
// последовательности. Останавливает итерацию, если yield вернула false.
type Seq[T any] func(yield func(T) bool)

Функция от этого хорошеет необычайно:

func Backward[T any](s []T) Seq[T] {
    // тело не меняется
}

Хваля себя за изобретение итератора, вы подходите к окну. Жаль, но похоже, что погода окончательно испортилась. Дождь льет как из ведра, а небо затянуло так, что стало темно как вечером.

4. Итератор-2: возвращение итератора

Все здорово, но вы внезапно понимаете: обычный range по срезу возвращает и индекс, и значение элемента. А ваш итератор — только значение. Вы решаете исправить эту досадную недоработку:

func Backward[T any](s []T) func(yield func(int, T) bool) {
    return func(yield func(int, T) bool) {
        for i := len(s) - 1; i >= 0; i-- {
            if !yield(i, s[i]) {
                return
            }
        }
    }
}

Пример вызова:

work := func(i int, x int) bool {
    fmt.Print(i, ":", x, " ")
    return true
}

s := []int{11, 22, 33, 44, 55}
it := Backward(s)
it(work)
fmt.Println()
// 4:55 3:44 2:33 1:22 0:11

Поскольку сигнатура результата поменялась, он больше не подходит под тип Seq. Что ты будешь делать — придется добавлять новый тип. После десятиминутного обдумывания вы решаете назвать его Seq2:

// Seq2 - итератор пар значений типа K и V.
// При вызове вида seq(yield) вызывает yield(k, v)
// для каждой пары (k, v) последовательности.
// Останавливает итерацию, если yield вернула false.
type Seq2[K any, V any] func(yield func(K, V) bool)
func Backward[T any](s []T) Seq2[int, T] {
    // тело не меняется
}

Чтобы размяться, вы встаете и подходите к окну. Ливень такой, что ничего не разглядеть. Сверкают молнии. Сыпется град величиной чуть ли не с кулак — вы подобного в жизни не видели. Бывает же!

5. Срез, да не простой

Все ли вы предусмотрели? Вроде да. Но не возвращаться же к задачам в Jira. Освежив в памяти спецификацию Go, вы понимаете, что кроме обычных срезов бывают «пользовательские» — типы, созданные на основе среза:

// IDs - срез идентификаторов.
type IDs []int

Backward прекрасно работает с IDs — компилятор принимает значение типа IDs, поскольку он основан на []int:

ids := IDs{11, 22, 33, 44, 55}
it := Backward(ids)
it(work)
fmt.Println()
// 4:55 3:44 2:33 1:22 0:11

А что если так?

// backwardIDs строит итератор по срезу идентификаторов
// в обратном порядке.
var backwardIDs func(IDs) Seq2[int, int] = Backward[int]
// ОШИБКА: cannot use Backward[int]
// (value of type func(s []int) Seq2[int, int])
// as func(IDs) Seq2[int, int] value in variable declaration

Вылезла разница между IDs и []int.

Когда мы присваиваем саму функцию, сравниваются сигнатуры: func(IDs) Seq2[int, int] против func([]int) Seq2[int, int]. Сигнатуры совпадают, только если типы параметров идентичны. Но IDs и []int отличаются, хоть один и основан на другом. Сигнатуры отличаются → получаем ошибку.

Что же делать, как же быть. Вы снова обращаетесь к спецификации и находите в ней специальный синтаксис для дженериков: ~T. Он означает множество всех типов, у которых базовый тип — T. То что надо!

Теперь придется параметризовать не только тип элемента (E), но и тип среза (Slice). E нужен для возвращаемых значений, Slice — чтобы функция принимала не только []E, но и любые основанные на нем типы:

func Backward[Slice ~[]E, E any](s Slice) Seq2[int, E] {
    return func(yield func(int, E) bool) {
        for i := len(s) - 1; i >= 0; i-- {
            if !yield(i, s[i]) {
                return
            }
        }
    }
}

Теперь пример:

// backwardIDs - итератор по срезу идентификаторов в обратном порядке.
var backwardIDs func(IDs) Seq2[int, int] = Backward[IDs, int]

ids := IDs{11, 22, 33, 44, 55}
work := func(i int, x int) bool {
    fmt.Print(i, ":", x, " ")
    return true
}
it := backwardIDs(ids)
it(work)
fmt.Println()
// 4:55 3:44 2:33 1:22 0:11

Работает! Вот и получилось что-то похожее на Backward из пакета slices.

Устало выдохнув, вы подходите к окну. Ливень и град сменились ураганом. Мимо пролетают вырванные с корнем деревья и рекламные щиты. С неба почему-то падают жабы.

6. Итератор-3: судный день

Чтобы отвлечься от происходящих за окном странностей, вы продолжаете размышлять.

Обычный Backward — это уже здорово. Но еще лучше было бы его параметризовать, чтобы конкретная логика обхода настраивалась. С другой стороны, если параметров будет много, то лучше бы подошла стратегия. И, кстати, неплохо было бы добавить фабрику, производящую итераторы по заданным критериям…

Не успеваете вы додумать эту мысль, как земля за окном с оглушительным грохотом разрывается. Огромная черная рука — вся в потоках раскаленной лавы и всполохах огня — стремительно вырывается из трещины, хватает вас и утаскивает прямиком в ад.

P.S. Несмотря на ироничный тон статьи, «усложненная» версия Backward в стандартной библиотеке оправдана. Да и мотивация для нее была не совсем такая, как у нашего лирического героя (а какая — выходит за рамки заметки). Но если вы делаете что-то подобное в проекте, который решает конкретную задачу — возможно, имеет смысл остановиться на более простом варианте.