Golang Rangefunc

박재훈·2024년 3월 21일
3

GO

목록 보기
19/23
post-custom-banner

Go 1.22 업데이트가 되면서 실험 기능으로 range-over-function iteratiors라는 것이 생겼다. 간단히 말하자면 일종의 이터레이터를 구현할 수 있는 것이다. 자바스크립트 yield나 파이썬 __getitem__ 같은 걸 생각하면 될 것 같다.

실행하려면

Go 1.22에서의 API로는 따로 제공하지 않고, 빌드할 때 GOEXPERIMENT 환경변수를 rangefunc로 설정해주어야 한다. 이렇게 하면 iter 패키지 이용이 가능하다는데, 따라서 현재로써는 코드 짜기가 굉장히 골치 아프다. 애초에 당장은 지원 안하기도 하고...

Types

기본적으로 이터레이터 구현에 사용되는 타입은 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면 루프 종료를 나타내는 것이다.

Iterator

저 타입을 구현하면 이터레이터를 만들고 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

Helper

헬퍼 타입의 사용은 파이썬의 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 막 잔뜩 있어야 되는 거 아닌가?
아무튼 함수형을 슬슬 지원하려는 것 같기도 하다.

References

profile
생각대로 되지 않을 때, 비로소 코딩은 재미있는 법.
post-custom-banner

0개의 댓글