Go 1.22: Rangefunc Experiment
- вторник, 20 февраля 2024 г. в 00:00:14
Данная статья - проба пера.
Данная статья - перевод/вольная интерпретация соответствующей страницы с Go Wiki. Если знаете английский язык, то, возможно, стоит зайти в первоисточник, а здесь посмотреть лишь примеры.
В данной статье будет речь только о простых одноуровневых циклах.
Range func - это функция-итератор, которую можно использовать в for-range цикле. Функция позволяет проходиться по какому-либо множеству данных, конечному или бесконечному.
Примером конечного набора данных является какой-либо абстрактный контейнер: массив, слайс, хэш-таблица, структура (если мы хотим пройтись по полям этой структуры), текстовый файл (хотим пройтись построчно) и т.д.
Примером бесконечного набора можно считать какой-либо генератор: генератор рандомных чисел, рандомных строк.
For-range циклы на данный момент позволяют следующее:
Последовательно пройтись по слайсу/массиву
for i, v := range []int{2, 4, 6} {...}
В случайном порядке пройтись по мапе
for k, v := range map[string]int{“two”: 2, “four”: 4, “six”: 6} {...}
Обновление Go 1.22 (не экспериментально): пройтись по последовательности целых чисел [0, n)
for i := range 10 {...}
На этом все. Если вам нужно итерироваться как-то иначе, то используйте обычный for цикл. Различные библиотеки решают этот вопрос по-разному:
package main
import (
"fmt"
"reflect"
)
func main() {
type S struct {
F0 string `alias:"field_0"`
F1 string `alias:""`
F2 string
}
s := S{}
st := reflect.TypeOf(s)
for i := 0; i < st.NumField(); i++ {
field := st.Field(i)
if alias, ok := field.Tag.Lookup("alias"); ok {
if alias == "" {
fmt.Println("(blank)")
} else {
fmt.Println(alias)
}
} else {
fmt.Println("(not specified)")
}
}
}
Итерируемся по полям структуры S.
package main
import (
"bufio"
"fmt"
"log"
"os"
)
func main() {
file, err := os.Open("README.md")
if err != nil {
log.Fatal(err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
}
Итерируемся по каждой строчке файла "README.md".
В целом, эти решения имеют право на жизнь. Они довольно просты и читаемы.
s := []int{2, 4, 6}
for i := len(s)-1; i >= 0; i-- {
// ...
}
Ничего сложного нет. Но может быть кому-то покажется не очевидно, или по крайней мере не сразу. Скорее всего программа содержит что-то еще помимо цикла, и бывает иногда сложно помнить все и схватывать все налету.
После Решения 1, Вы решили, что было круто вообще все слайсы проходить с конца. В итоге, нужно поменять все текущие циклы, и быть готовым в будущем копипастить этот длинный for с присваиваниями, получением длины слайса, какими-то проверками. Либо можно сделать так:
type Iter[E any] func(body func(index int, value E))
func Backward[S ~[]E, E any](s S) Iter[E] {
return func(body func(int, E)) {
for i := len(s) - 1; i >= 0; i-- {
body(i, s[i])
}
}
}
// Использование
backwardIter := Backward([]int{2, 4, 6})
backwardIter(func(index int, value int) {
fmt.Printf("[%d]=%d\n", index, value)
})
Прежде чем разбираться с функцией Backward
и типом Iter
, давайте посмотрим на использование. Мы создаем некую переменную backwardIter
, а затем вызываем ее, передавая в качестве аргумента анонимную функцию, которую позволяет нам воспользоваться переменными index
и value
.
Это очень похоже на for-range цикл для слайса. Цикл так же дает нам доступ к индексу и значению текущего элемента для использования в теле.
Возвращаемся к Iter
. Тип Iter
- это функция, в которую передается некая callback-функция body. Это значит, что объект типа Iter
возможно вызовет body с необходимыми аргументами (типа int
и E
соответственно). А может и не вызовет, зависит от содержимого объекта.
Backward
- это функция, которая создает объект типа Iter
. Создаваемый объект может обратиться к слайсу s
. Поэтому может по нему проитерироваться в обратном порядке, запоминая индекс i
, по индексу узнавая значение s[i]
, и вызывая с этими аргументами "тело цикла" body.
Но является ли это решением? Для некоторых задач - да. На самом деле зависит от аргумента, с которым вызывается объект типа Iter
. Мы не можем сказать, что это эквивалент для for-range цикла (с проходом с конца слайса, а не сначала), потому что мы забыли такую конструкцию как defer
. Если мы используем defer
внутри for-range цикла, то defer
выполнится после завершения внешней функции, содержащей цикл. Добавим defer
в "тело цикла" нашего аналога.
backwardIter(func(index int, value int) {
defer func() { fmt.Printf("deferred [%d]=%d\n", index, value)}()
fmt.Printf("[%d]=%d\n", index, value)
})
Когда выполнится defer
? defer
выполнится после того, как именно анонимная функция завершит свою работу. Не функция, которая содержит backwardIter
, а именно анонимная. Кажется, что это является основной причиной, по которой разработчики решили нам подарить итераторы.
Использование совсем простое, то, которое мы привыкли видеть, достаточно лишь создать написать функцию-итератор.
for range h {...} // h has type func(func() bool)
for v := range f { ... } // f has type Seq[V], v has type V
for k, v := range g { ... } // g has type Seq2[K,V], k and v have types K and V
Семейство типов Seq
(предполагаю от "sequence" ) ограничено 3 экземплярами:
//type Seq0 func(yield func() bool)
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)
Почему всего 3? Потому что в языке отсутствуют for-range циклы с 3+ параметрами.
Давайте рассмотрим типы Seq
поподробнее. Это generic функции, которые принимает в себя другую функцию yield.
yield
- это callback, в котором содержится тело цикла. Аргументами служат значения, которые "итератор выдает". Seq[int]
должен выдавать int
, Seq[string, int]
соответственно string
и int
. Внутри итератора происходит многократный (хотя можно и 1 раз, но зачем тогда это все?) вызов функции yield
. При этом можно заметить, что yield
возвращает bool
. Зачем? Все дело в том, что в теле цикла могут находиться управляющие конструкции по типу continue
и break
. А значит итератор должен будет каким-нибудь образом узнать, когда ему нужно перестать вызывать тело цикла. Соответственно, если yield(...) == false
(в теле цикла написали break
), то итератор прекращает работу.
Мы знаем, что итератор - это функция. Что будет, если мы вызовем эту функцию?
package main
import (
"fmt"
"iter"
)
func main() {
arr := []int{16, 21, 3, 4, 58, 0, 73, 8, 2, 10}
backwardIterator := iter.Seq2[int, int](func(yield func(int, int) bool) {
for i := len(arr) - 1; i >= 0; i-- {
v := arr[i]
if !yield(i, v) {
return
}
}
})
body := func(i int, v int) bool {
if i%2 == 0 {
return true
}
if i == 1 {
return false
}
defer func() { fmt.Printf("iterator(body): [%d]=%d\n", i, v) }()
return true
}
backwardIterator(body)
for i, v := range backwardIterator {
if i%2 == 0 {
continue
}
if i == 1 {
break
}
defer func() { fmt.Printf("range iterator: [%d]=%d\n", i, v) }()
}
}
Вывод:
iterator(body): [9]=10
iterator(body): [7]=8
iterator(body): [5]=0
iterator(body): [3]=4
range iterator: [3]=4
range iterator: [5]=0
range iterator: [7]=8
range iterator: [9]=10
Посмотрим на вывод с приставкой "range iterator". Что из него можно понять? Несмотря на то, что итератор - обертка над телом цикла, выражения с defer
выполняются именно после прекращения работы внешней функции, которая и содержит цикл. Это поведение к которому мы привыкли, и которое есть сейчас без функций-итераторов.
И посмотрим на вывод с приставкой "iterator(body)". Он отличается порядком следования элементов. На каждой итерации вызывается body
, и defer
выражение выполняется сразу после того, как body
завершает работу.
Поэтому, если есть желание использовать итераторы, их нужно использовать только в for-range цикле для корректной работы.
Можно посмотреть из примера 1. Работает как обычно с for-range циклами.
defer исполнится после того, как итератор прекратит работу. Итератор же завершит свою работу, когда тело цикла скажет break
или когда значений для “выдачи” не останется.
package main
import (
"fmt"
"iter"
)
func main() {
arr := []int{16, 21, 3, 4, 58, 0, 73, 8, 2, 10}
backwardIterator := iter.Seq2[int, int](func(yield func(int, int) bool) {
fmt.Println("iterator start")
defer func() { fmt.Println("iterator end") }()
for i := len(arr) - 1; i >= 0; i-- {
v := arr[i]
if !yield(i, v) {
return
}
}
})
fmt.Println("before for-range")
for i, v := range backwardIterator {
if i%2 == 0 {
continue
}
if i == 1 {
break
}
fmt.Printf("range iterator: [%d]=%d\n", i, v)
}
fmt.Println("after for-range")
}
Вывод:
before for-range
iterator start
range iterator: [9]=10
range iterator: [7]=8
range iterator: [5]=0
range iterator: [3]=4
iterator end
after for-range
Вначале происходит все, что до цикла. Итератор начинает работу. Итератор заканчивает работу и выполняются defer'ы
, отложенные итератором. Происходит все, что после цикла. Выполняются defer'ы
, отложенные телом цикла.
package main
import (
"fmt"
"iter"
)
func main() {
arr := []int{16, 21, 3, 4, 58, 0, 73, 8, 2, 10}
backwardIterator := iter.Seq2[int, int](func(yield func(int, int) bool) {
fmt.Printf("iterator start\n")
defer func() { fmt.Printf("iterator end\n") }()
for i := len(arr) - 1; i >= 0; i-- {
v := arr[i]
if !yield(i, v) {
return
}
}
})
fmt.Println("before range")
for i, v := range backwardIterator {
defer func() { fmt.Printf("[%d]=%d\n", i, v) }()
}
fmt.Println("after range")
}
Вывод
before range
iterator start
iterator end
after range
[0]=16
[1]=21
[2]=3
[3]=4
[4]=58
[5]=0
[6]=73
[7]=8
[8]=2
[9]=10
Пусть мы итерируемся как обычно, без итератора. Что произойдет, если в теле цикла произойдет паника? Тогда вся функция, содержащая этот цикл прекратит работу, а так же прекратит работу функция, вызвавшая эту, и т.д. по стеку вызовов, до тех пор, пока стек не кончится или кто-то не использует recover()
.
Поскольку итератор - это обертка для тела цикла, внутри него мы можем использовать recover()
, например, продолжив цикл после паники:
package main
import (
"fmt"
"iter"
)
func main() {
arr := []int{16, 21, 3, 4, 58, 0, 73, 8, 2, 10}
backwardIterator := iter.Seq2[int, int](func(yield func(int, int) bool) {
for i := len(arr) - 1; i >= 0; i-- {
v := arr[i]
if func() bool {
defer func() {
if err := recover(); err != nil {
fmt.Printf("recovered [%d] iteration: %s\n", i, err)
}
}()
return !yield(i, v)
}() {
return
}
}
})
for i, v := range backwardIterator {
if i == 1 {
panic("i == 1")
}
fmt.Printf("[%d]=%d\n", i, v)
}
}
Вывод:
[9]=10
[8]=2
[7]=8
[6]=73
[5]=0
[4]=58
[3]=4
[2]=3
recovered [1] iteration: i == 1
[0]=16
Как вы думаете, насколько это ожидаемое поведение от for-range цикла? Допустим, что Вы используется сторонний итератор и можете не догадываться о том, что итератор обрабатывает панику. Тогда, вероятнее всего, программа будет некорректной. Разработчики Go пишут, что такое поведение, возможно, - ошибка. А значит в будущих версиях языка поведение может измениться.
В примерах выше, мы наблюдаем “Push”-семантику: итератор насильно “запихивает” свои значения внутрь функции, представляющей собой тело цикла. Можно ли их “брать” самостоятельно? Ответ: можем, используя вспомогательные функции Pull
из пакет iter
.
package main
import (
"fmt"
"iter"
"math/rand"
)
func main() {
randIntGen := iter.Seq[int](func(yield func(int) bool) {
for {
if !yield(rand.Int()) {
break
}
}
})
nextRandInt, stop := iter.Pull(randIntGen)
defer stop()
for i := range 5 {
v, ok := nextRandInt()
if !ok {
fmt.Println("no more values in sequence")
break
}
fmt.Printf("[%d] %d\n", i, v)
}
}
Вывод:
[0] 2509440936367219129
[1] 8002226566324048486
[2] 9111356380098048175
[3] 69841294487450816
[4] 5523863652085536380
В данном случае мы получим 5 значений. При этом блока с break
мы не достигнем: наш генератор всегда готов выдавать новые значения, лишь бы цикл, в котором этот генератор используется не остановился.
Давайте изменим итератор так, чтобы он мог вернуть только 4 значения:
package main
import (
"fmt"
"iter"
"math/rand"
)
func main() {
randIntGen := iter.Seq[int](func(yield func(int) bool) {
for range 4 {
if !yield(rand.Int()) {
break
}
}
})
nextRandInt, stop := iter.Pull(randIntGen)
defer stop()
for i := range 5 {
v, ok := nextRandInt()
if !ok {
fmt.Println("no more values in sequence")
break
}
fmt.Printf("[%d] %d\n", i, v)
}
}
Вывод:
[0] 3076252664958833517
[1] 5245788927748476723
[2] 4408941311063185846
[3] 2059933908849133260
no more values in sequence
Для того, чтобы попробовать эту экспериментальную фичу и позапускать свои приложения, необходимо выставить следующую переменную среды окружения:
GOEXPERIMENT=rangefunc
Если мозолят глаза красные надписи и подчеркивания при работе с пакетом iter, то:
Ctrl+Alt+s
Go -> Build tags
В поле Custom tags добавить goexperiment.rangefunc
package main
import (
"fmt"
"iter"
"math/rand"
)
// RandomOrderIterator iterates through s in random order returning
// index and value of each element.
func RandomOrderIterator[S ~[]E, E any](s S) iter.Seq2[int, E] {
order := rand.Perm(len(s))
return func(yield func(int, E) bool) {
for _, v := range order {
if !yield(v, s[v]) {
return
}
}
}
}
func main() {
s1 := []int{5, 10, 4, 7, 3}
for i, v := range RandomOrderIterator(s1) {
fmt.Printf("s1[%d]=%v\n", i, v)
}
fmt.Println()
s2 := []string{"five", "ten", "four", "seven", "three"}
for i, v := range RandomOrderIterator(s2) {
fmt.Printf("s2[%d]=%v\n", i, v)
}
fmt.Println()
type someStruct struct {
i int
str string
}
s3 := []someStruct{
{5, "five"},
{10, "ten"},
{4, "four"},
{7, "seven"},
{3, "three"},
}
for i, v := range RandomOrderIterator(s3) {
fmt.Printf("s3[%d]=%v\n", i, v)
}
}
Вывод программы:
s1[4]=3
s1[3]=7
s1[2]=4
s1[1]=10
s1[0]=5
s2[0]=five
s2[1]=ten
s2[3]=seven
s2[4]=three
s2[2]=four
s3[3]={7 seven}
s3[2]={4 four}
s3[4]={3 three}
s3[0]={5 five}
s3[1]={10 ten}
Прелесть такого итератора заключается в том, что он построен на generic'е. А значит будет работать со слайсами любого типа.
Возможно, показанные в этой статье примеры не разбудили в Вас интерес. Поэтому вот примеры от создателей языка, в которых можно было бы использовать итераторы:
Когда вы создаете свой собственный контейнер (например, упорядоченную мапу)
"Потоковая" обработка. Вместо того, чтобы накопить какой-то результат в слайсе и пройтись по нему, с помощью итераторов, вы сможете сразу получать интересующие значения. Возможно, в стандартной библиотеки появятся аналогичные функции, возвращающие итератор, вместо слайса с результатом:
strings.Split
strings.Fields
regexp.Regexp.FindAll
Создатели языка говорят, что программы с использование итераторов могут быть читаемы. Я вхожу в число людей, которым бы больше понравилось видеть for range i, v := range Backward(s) {...}
вместо for i := len(s)-1; i >= 0; i-- {...}
. Возможно, кто-то так не считает.
Пишите свои примеры использования итераторов, или почему Вы бы не хотели их использовать.
P.S.: Жду критики.