Go 1.22 업데이트가 되면서 실험 기능으로 range-over-function iteratiors
라는 것이 생겼다. 간단히 말하자면 일종의 이터레이터를 구현할 수 있는 것이다. 자바스크립트 yield
나 파이썬 __getitem__
같은 걸 생각하면 될 것 같다.
Go 1.22에서의 API로는 따로 제공하지 않고, 빌드할 때 GOEXPERIMENT
환경변수를 rangefunc
로 설정해주어야 한다. 이렇게 하면 iter
패키지 이용이 가능하다는데, 따라서 현재로써는 코드 짜기가 굉장히 골치 아프다. 애초에 당장은 지원 안하기도 하고...
기본적으로 이터레이터 구현에 사용되는 타입은 Seq
, Seq2
가 있다.
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)
그리고 헬퍼 타입으로 Pull
, Pull2
가 있다.
func Pull[V any](seq Seq[V]) (next func() (V, bool), stop func())
func Pull2[K, V any](seq Seq2[K, V]) (next func() (K, V, bool), stop func())
리턴 타입이 bool
인 이유는 true
면 다음 루프로 진행, false
면 루프 종료를 나타내는 것이다.
저 타입을 구현하면 이터레이터를 만들고 for
문을 통해 돌릴 수 있는 것이다.
공식 가이드에 나와있는 코드를 통해 설명하자면
func Backward[E any](s []E) func(func(int, E) bool) {
return func(yield func(int, E) bool) {
for i := len(s)-1; i >= 0; i-- {
if !yield(i, s[i]) {
return
}
}
}
}
위 코드의 Backward
함수는 파라미터로 받은 s
슬라이스를 마지막 인덱스부터 첫번째 인덱스까지 루프를 돌리며 yield
함수를 실행시킨다.
이 코드를 아래처럼 실행시킬 수 있다.
s := []string{"hello", "world"}
for i, x := range slices.Backward(s) {
fmt.Println(i, x)
}
그럼 아마 실행 결과는 다음과 같을 것이다.
1 world
0 hello
Seq
타입의 함수를 for
문의 range
뒤에 배치함으로써 루프를 돌 수 있는 것이며, 이후 중괄호 안을 작성함으로써 yield
콜백을 구현할 수 있다.
위 실행 코드는 다음 코드와 같은 실행을 하게 되는 것이다.
Backward(s)(func(i int, x string) bool {
fmt.Println(i, x)
return true
})
그니까 정리하자면
// Seq[int] 타입을 반환하는 함수 Iterator()
func Iterator() Seq[int] {
numbers := []int{1, 2, 3}
return func(yield func(int, int) bool) {
for i, n := range numbers {
// numbers를 루프 돌리면서 yield 콜백 함수 실행
yield(i, n)
}
}
}
for i, n := range Iterator() {
// 이 부분이 yield 함수가 된다.
fmt.Println(i, n * n)
}
// 출력결과
// 0 1
// 1 4
// 2 9
헬퍼 타입의 사용은 파이썬의 zip
처럼 여러 루프를 엮어서 다룰 수 있게 해주는 것 같다.
// Zipped holds values from an iteration of a Seq returned by [Zip].
type Zipped[T1, T2 any] struct {
V1 T1
OK1 bool
V2 T2
OK2 bool
}
// Zip returns a new Seq that yields the values of seq1 and seq2 simultaneously.
func Zip[T1, T2 any](seq1 iter.Seq[T1], seq2 iter.Seq[T2]) iter.Seq[Zipped[T1, T2]] {
return func(yield func(Zipped[T1, T2]) bool) {
p1, stop := iter.Pull(seq1)
defer stop()
p2, stop := iter.Pull(seq2)
defer stop()
for {
var val Zipped[T1, T2]
val.V1, val.OK1 = p1()
val.V2, val.OK2 = p2()
if (!val.OK1 && !val.OK2) || !yield(val) {
return
}
}
}
}
이걸 Go스럽다고 봐도 되는 것일지..
그리고 Seq
에 대해 파라미터 개수를 그냥 늘린게 Seq2
인데 이러면 Seq3
Seq4
막 잔뜩 있어야 되는 거 아닌가?
아무튼 함수형을 슬슬 지원하려는 것 같기도 하다.