Итераторы…
TL;DR: в
iter.Seq итератор сам передаёт следующие элементы в код внутри range, а через
yield получает сигнал, продолжать обход. Ошибку можно передавать вторым значением через
Seq2[T, error], но тогда, если она должна дойти до конца цепочки, все промежуточные итераторы тоже должны уметь её принять и передать дальше, даже если она им не нужна (!). Стоит ли оно такой запары?
Вспомним обычный обход:
for i := 0; i < source.Len(); i++ {
process(source.Get(i))
}
Это типичная pull-модель: внешний код сам сначала вытаскивает значение из источника
Get(i), а потом передаёт его в бизнес-логику
process.
Обратный вариант - обход с callback функцией:
source.Scan(func(v Value) bool {
process(v) // принимает то, что будет попадать из цикла ниже
return true
})
func (s *Source) Scan(process func(Value) bool) {
for i := 0; i < s.Len(); i++ {
if !process(s.Get(i)) {
return
}
}
}
Внешний код вызывает
Scan один раз, а дальше уже
Scan достает данные и сам вызывает коллбек
process - в ответ получает
bool и принимает решение об остановке. С точки зрения вызывающего кода это push-модель.
Многострадальные итераторы работают по тому же принципу, только такой callback-обход стандартизирован общим типом
iter.Seq
go
func Numbers(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; i < n; i++ {
if !yield(i) {
return
}
}
}
}
Коллбек-механика спрятана внутри range:
go
for v := range Numbers(10) {
// callback логика тут, а вызывает ее код из Numbers
if v == 5 {
break
}
}
Numbers сам вызывает
yield с очередным значением. Код внутри
range получает значение, обрабатывает (в нашем случае сравнивает с 5), и отдает
bool итератору, где последний решает, продолжать ли обход.
С остановкой по инициативе коллбека внутри
range всё ок. Но что, если продолжить обход не может уже сам итератор - например, если чтение следующего значения закончилось ошибкой.
В
Seq[T] передать её некуда. Один из вариантов - вынести ошибку во второе значение:
func Values() iter.Seq2[int, error] {
return func(yield func(int, error) bool) {
for i := 0; i < 10; i++ {
v, err := readValue(i)
if err != nil {
yield(0, err)
return
}
if !yield(v, nil) {
return
}
}
}
}
Тогда ошибка приходит в тот же
range, что и данные:
for v, err := range Values() {
if err != nil {
return err
}
process(v)
}
Для одного итератора это выглядит естественно. Но в композиции с другими итераторами, например c
func Double(seq iter.Seq[int]) iter.Seq[int] {
return func(yield func(int) bool) {
for v := range seq {
if !yield(v * 2) {
return
}
}
}
}
eсли ошибка из
Values должна пройти через
Double до конечного потребителя, то`Double` (который сам упасть не может и которому не нужна ошибка), вынужден перейти на
Seq2[int, error] (иначе у нас просто не скомпилится тк
Double принимает `Seq[int]`) и явно передавать чужую ошибку
Чтобы не разводить зоопарк предложили даже собирать такие ошибки через отдельный хелпер`slices.CollectError` в
#70631
func CollectError[T any](seq iter.Seq2[T, error]) ([]T, error) {
var err error
result := slices.Collect(func(yield func(T) bool) {
for v, e := range seq {
if e != nil {
// записываем первую ошибку
err = e
// завершаем обход
return
}
if !yield(v) {
return
}
}
})
return result, err
}
Просто как кейс стандартизации обработки ошибки через
Seq2[T, error]. Или очередной способ задать себе правила 🙂
Еще нашла
#71901 где решают, какой контракт для итераторов с ошибками стоит считать стандартным. Но мне страшно туда лезть..
#go