Go

What is iter.Pull, and when do you need it? Implement a generic Zip.

Question 141HardGo 1.22 to 1.25

Range-over-func iterators are push iterators: the sequence drives the loop. That makes it impossible to step through two sequences in lockstep with a single for. iter.Pull converts a push iterator into a pull pair, next() (V, bool) and stop(). Internally it uses a coroutine that switches cheaply between goroutines, so it costs more than a plain range loop but far less than a channel.

type Pair[A, B any] struct {
    First  A
    Second B
}

func Zip[A, B any](as iter.Seq[A], bs iter.Seq[B]) iter.Seq[Pair[A, B]] {
    return func(yield func(Pair[A, B]) bool) {
        nextA, stopA := iter.Pull(as)
        defer stopA()
        nextB, stopB := iter.Pull(bs)
        defer stopB()
        for {
            a, ok1 := nextA()
            b, ok2 := nextB()
            if !ok1 || !ok2 {
                return
            }
            if !yield(Pair[A, B]{a, b}) {
                return
            }
        }
    }
}

names := slices.Values([]string{"a", "b", "c"})
ages := slices.Values([]int{1, 2})
for p := range Zip(names, ages) {
    fmt.Println(p.First, p.Second) // a 1, b 2
}

Gotchas:

  • Always call stop, usually with defer. Otherwise the paused iterator never finishes, its deferred cleanup never runs, and the resources leak.
  • Do not call next concurrently from several goroutines.
  • Prefer plain range when a single sequence is enough.

More on Generics

All 36 Generics questions