Ітератори¶
Стаття про керування потоком виконання
показала range над функцією та пообіцяла розглянути продукуючу сторону
пізніше. Ось вона. Ітератор у Go — це просто функція, по якій можна
робити range, і написати таку функцію — менше механіки, ніж здається.
Форма¶
Два псевдоніми типів у пакеті iter називають ці сигнатури:
Ітератор — це функція, яка приймає функцію. range надає yield, ваш
код викликає її по одному разу на елемент, а тіло циклу — це те, що
виконується всередині yield:
func Countdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
if !yield(i) {
return
}
}
}
}
Напрямок тут перевернутий порівняно з більшістю мов: цикл не витягує значення з вас, а ви заштовхуєте значення в цикл.
yield повертає false, коли цикл зупиняється¶
Оце if !yield(i) { return } — увесь контракт. break, return чи
panic у тілі циклу змушує yield повернути false, і ви мусите
зупинитися й повернутися:
Ігнорування цього результату — єдиний справжній баг, який тут можна написати, і час виконання його ловить:
func Bad(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
yield(i) // результат ігнорується
}
}
}
// panic: runtime error: range function continued iteration after
// function for loop body returned false
Наслідок: тіло циклу може завершитись у будь-який момент, тож ресурс, що
відкритий до виклику yield, все одно потребує defer, щоб звільнитися,
коли викликач вийде достроково.
Дві пари значень: Seq2¶
Seq2 — та сама ідея з парою, яку range вже дає вам над мапою чи
зрізом:
func Enumerate[T any](s []T) iter.Seq2[int, T] {
return func(yield func(int, T) bool) {
for i, v := range s {
if !yield(i, v) {
return
}
}
}
}
for i, v := range Enumerate([]string{"a", "b"}) {
fmt.Printf("%d=%s ", i, v)
}
// output: 0=a 1=b
Ітератори компонуються¶
Оскільки ітератор — це звичайне значення, функція може приймати один ітератор і повертати інший. Нічого не буферизується — значення й далі течуть по одному:
func Filter[T any](seq iter.Seq[T], keep func(T) bool) iter.Seq[T] {
return func(yield func(T) bool) {
for v := range seq {
if keep(v) && !yield(v) {
return
}
}
}
}
even := Filter(Countdown(6), func(n int) bool { return n%2 == 0 })
fmt.Println(slices.Collect(even)) // output: [6 4 2]
Де вони окупаються: рекурсивні структури¶
Раніше, щоб показати вміст дерева назовні, потрібно було будувати зріз
або приймати колбек. Ітератор дає викликачам звичайний цикл for над
структурою, яку незручно обходити:
type Tree struct {
Val int
Left, Right *Tree
}
func (t *Tree) All() iter.Seq[int] {
return func(yield func(int) bool) {
t.walk(yield)
}
}
func (t *Tree) walk(yield func(int) bool) bool {
if t == nil {
return true
}
return t.Left.walk(yield) && yield(t.Val) && t.Right.walk(yield)
}
tr := &Tree{Val: 2, Left: &Tree{Val: 1}, Right: &Tree{Val: 3}}
fmt.Println(slices.Collect(tr.All())) // output: [1 2 3]
Ланцюжок && виконує подвійну роботу: він задає послідовність
ліворуч-себе-праворуч і одразу ж зупиняється, щойно yield повертає
false. Зверніть увагу на виклики методів на nil *Tree — це
безпечно, як пояснює стаття про методи.
Стандартна бібліотека і продукує, і споживає їх¶
Ви вже користувалися цим. maps.Keys повертає ітератор, тому вона й
поєднується зі slices.Sorted:
| Продюсер | Дає |
|---|---|
slices.Values(s) |
кожен елемент |
slices.All(s) |
індекс і елемент |
slices.Backward(s) |
індекс і елемент, від останнього до першого |
maps.Keys(m) / maps.Values(m) |
ключі / значення |
maps.All(m) |
ключ і значення |
strings.SplitSeq(s, sep) |
шматки, без виділення зрізу |
| Споживач | Дає |
|---|---|
slices.Collect(seq) |
[]T |
slices.Sorted(seq) |
відсортований []T |
maps.Collect(seq2) |
map[K]V |
fmt.Println(slices.Collect(slices.Values([]int{1, 2, 3}))) // output: [1 2 3]
fmt.Println(slices.Collect(strings.SplitSeq("a,b,c", ","))) // output: [a b c]
for i, v := range slices.Backward([]int{1, 2, 3}) {
fmt.Printf("%d:%d ", i, v)
}
// output: 2:3 1:2 0:1
SplitSeq — це вся суть цієї можливості в мініатюрі: strings.Split
виділяє зріз, який потім викидають, тоді як SplitSeq віддає шматки
одразу, як їх знаходить.
iter.Pull, коли потрібно керувати самому¶
Іноді циклом for не обійтись — треба просувати дві послідовності
синхронно. iter.Pull перетворює push-ітератор на функцію next:
next, stop := iter.Pull(slices.Values([]int{1, 2, 3}))
defer stop()
for {
v, ok := next()
if !ok {
break
}
fmt.Print(v, " ")
}
// output: 1 2 3
stop треба викликати завжди — звідси й defer — тому що Pull запускає
ітератор в окремій горутині, а stop саме і звільняє її. З двома такими
можна робити зіп:
a, stopA := iter.Pull(slices.Values([]string{"x", "y"}))
defer stopA()
b, stopB := iter.Pull(slices.Values([]int{10, 20}))
defer stopB()
for {
s, ok1 := a()
n, ok2 := b()
if !ok1 || !ok2 {
break
}
fmt.Printf("%s=%d ", s, n)
}
// output: x=10 y=20
Pull коштує дорожче за пряме перебирання через range, тож
використовуйте його лише тоді, коли керування потоком дійсно цього
вимагає.
Коли не варто писати ітератор¶
Якщо у вас уже є зріз, поверніть зріз. Ітератор виправдовує себе, коли послідовність коштовна, необмежена або незручна для матеріалізації — обхід дерева, посторінковий API, рядки великого файлу. Для жменьки значень у пам'яті це непотрібна непряма робота.
З досвіду Python: це генератор, але побудований навпаки.
yieldу Python призупиняє вашу функцію;yieldу Go — це колбек, який дає вам цикл, і поверненняfalse— те саме, що робитьGeneratorExit.iter.Pull— найближчий аналог утримування об'єкта-генератора й самостійного викликуnext().
Швидка довідка¶
| Форма | Значення |
|---|---|
iter.Seq[V] |
func(yield func(V) bool) |
iter.Seq2[K, V] |
func(yield func(K, V) bool) |
if !yield(v) { return } |
зупинитись, коли тіло циклу перериває — обов'язково |
slices.Collect(seq) |
вилити в зріз |
slices.Sorted(maps.Keys(m)) |
відсортовані ключі мапи |
iter.Pull(seq) |
next, stop — завжди defer stop() |