golang

Go 1.22: Rangefunc Experiment

  • вторник, 20 февраля 2024 г. в 00:00:14
https://habr.com/ru/articles/794564/

Дисклеймер

Данная статья - проба пера.

Данная статья - перевод/вольная интерпретация соответствующей страницы с Go Wiki. Если знаете английский язык, то, возможно, стоит зайти в первоисточник, а здесь посмотреть лишь примеры.

В данной статье будет речь только о простых одноуровневых циклах.

План

  1. Что такое range func?

  2. Нюансы

  3. Push/Pull - семантика

  4. Как попробовать

  5. Пример: перебор слайса в случайном порядке

  6. Послесловие

Что такое range func?

Range func - это функция-итератор, которую можно использовать в for-range цикле. Функция позволяет проходиться по какому-либо множеству данных, конечному или бесконечному.

Примером конечного набора данных является какой-либо абстрактный контейнер: массив, слайс, хэш-таблица, структура (если мы хотим пройтись по полям этой структуры), текстовый файл (хотим пройтись построчно) и т.д.

Примером бесконечного набора можно считать какой-либо генератор: генератор рандомных чисел, рандомных строк.

Как с этим дела сейчас?

For-range циклы на данный момент позволяют следующее:

  1. Последовательно пройтись по слайсу/массиву

    for i, v := range []int{2, 4, 6} {...}

  2. В случайном порядке пройтись по мапе

    for k, v := range map[string]int{“two”: 2, “four”: 4, “six”: 6} {...}

  3. Обновление Go 1.22 (не экспериментально): пройтись по последовательности целых чисел [0, n)

    for i := range 10 {...}

На этом все. Если вам нужно итерироваться как-то иначе, то используйте обычный for цикл. Различные библиотеки решают этот вопрос по-разному:

reflect
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.

bufio
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".

В целом, эти решения имеют право на жизнь. Они довольно просты и читаемы.

Задача: проитерироваться по слайсу с конца

Решение 1
s := []int{2, 4, 6}
for i := len(s)-1; i >= 0; i-- {
  // ...  
}

Ничего сложного нет. Но может быть кому-то покажется не очевидно, или по крайней мере не сразу. Скорее всего программа содержит что-то еще помимо цикла, и бывает иногда сложно помнить все и схватывать все налету.

После Решения 1, Вы решили, что было круто вообще все слайсы проходить с конца. В итоге, нужно поменять все текущие циклы, и быть готовым в будущем копипастить этот длинный for с присваиваниями, получением длины слайса, какими-то проверками. Либо можно сделать так:

Решение 2
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, а именно анонимная. Кажется, что это является основной причиной, по которой разработчики решили нам подарить итераторы.

Используем range func, пакет iter

Использование совсем простое, то, которое мы привыкли видеть, достаточно лишь создать написать функцию-итератор.

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), то итератор прекращает работу.

Неправильный использование итератора

Мы знаем, что итератор - это функция. Что будет, если мы вызовем эту функцию?

Пример 1. Вызов итератора как функции, 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) {
      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 цикле для корректной работы.

Нюансы

defer внутри тела цикла

Можно посмотреть из примера 1. Работает как обычно с for-range циклами.

defer внутри итератора

defer исполнится после того, как итератор прекратит работу. Итератор же завершит свою работу, когда тело цикла скажет break или когда значений для “выдачи” не останется.

Пример 2. 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.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'ы, отложенные итератором. Происходит все, что после цикла. Выполняются defer'ы, отложенные телом цикла.

Пример 3. Порядок 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().

Поскольку итератор - это обертка для тела цикла, внутри него мы можем использовать recover(), например, продолжив цикл после паники:

Пример 4. 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 - семантика

В примерах выше, мы наблюдаем “Push”-семантику: итератор насильно “запихивает” свои значения внутрь функции, представляющей собой тело цикла. Можно ли их “брать” самостоятельно? Ответ: можем, используя вспомогательные функции Pull из пакет iter

Пример 5. Pull - семантика. Генератор рандомных чисел
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 значения:

Пример 6. Pull-семантика. Генератор 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

GoLand
Заходим в конфигурации запусков
Заходим в конфигурации запусков
В поле Environment добавляем переменную
В поле Environment добавляем переменную

Если мозолят глаза красные надписи и подчеркивания при работе с пакетом iter, то:

  1. Ctrl+Alt+s

  2. Go -> Build tags

  3. В поле Custom tags добавить goexperiment.rangefunc

Пример: перебор слайса в случайном порядке

Пример 7: перебор слайса в случайном порядке
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'е. А значит будет работать со слайсами любого типа.

Послесловие

Возможно, показанные в этой статье примеры не разбудили в Вас интерес. Поэтому вот примеры от создателей языка, в которых можно было бы использовать итераторы:

  1. Когда вы создаете свой собственный контейнер (например, упорядоченную мапу)

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

    1. strings.Split

    2. strings.Fields

    3. regexp.Regexp.FindAll

Создатели языка говорят, что программы с использование итераторов могут быть читаемы. Я вхожу в число людей, которым бы больше понравилось видеть for range i, v := range Backward(s) {...} вместо for i := len(s)-1; i >= 0; i-- {...} . Возможно, кто-то так не считает.

Пишите свои примеры использования итераторов, или почему Вы бы не хотели их использовать.

P.S.: Жду критики.