Since generators in Python, I have always liked lazy evaluation. The simplicity and elegance of yield combined with .next() elevate the iterator pattern to the next level. Since version 1.23, this pattern is available in Golang. In Go, these are called Range over function types. I have read many blog posts on them, but it won’t stop me from writing my own, if only to retain what I have learned.

Formal introductions and lengthy explanations can be found here, here, and here. I just want to jump right into the code and experiment with it.

Simplest version#

To range over a function, we need a function and a range. The signature, at a minimum, must be func(func() bool). More details can be found in the official documentation.

 func SimplestIterator() func(func() bool) {
	return func(yield func() bool) {
		yield()
		yield()
		yield()
	}
}

func TestSimplest(t *testing.T) {
	counter := 0
	for range SimplestIterator() {
		counter++

	}
	assert.Equal(t, counter, 3)
}

We “yield” three times, so the counter is set to 3. What is the big deal then? The fact that we could have used range and lazily evaluated the partial result. Albeit, here we have no results because we yield nothing.

Note: yield is a convention. You could return func(foo func() bool) and later call foo() instead of yield(). But don’t do that.

How to yield “something” and what is the story with the bool?#

To yield something, the yield function needs to take that “something”. In the previous example, the yield function was yield func() bool, so we couldn’t iterate over any values. With yield func(int) bool, we can range over some sweet int types.

func CountToThree() iter.Seq[int] {
	return func(yield func(int) bool) {
		if !yield(1) {
			return
		}
		if !yield(2) {
			return
		}
		if !yield(3) {
			return
		}
	}
}

func TestCountToThree(t *testing.T) {
	result := make([]int, 0)
	for i := range CountToThree() {
		result = append(result, i)
	}
	assert.Equal(t, result, []int{1, 2, 3})
}

Awesome! With for i := range CountToThree(), the i holds whatever we yielded in the iterator. But you should also notice the if !yield(1) { return } part that is new. We are obligated to return if yield returns false, which means that no more values are needed from the range. What if you simply ignore it as in the first example? You will get the following:

panic: runtime error: range function continued iteration after function for loop body returned false

So, to be stoic about your runtime and avoid unnecessary panics: always handle what yield returns.

How to control the iterator output?#

These range over functions can also take arguments like any other functions. This way, you can control what you iterate over. You can also build such iterators with methods. It is a really versatile feature.

func Countdown(v int) func(func(int) bool) {
	return func(yield func(int) bool) {
		for i := v; i >= 0; i-- {
			yield(i)
		}
	}
}

func TestCountdown(t *testing.T) {
	items := make([]int, 0)
	for i := range Countdown(3) {
		items = append(items, i)
	}

	assert.Equal(t, []int{3, 2, 1, 0}, items)
}

The Countdown takes the int and yield as many time as in the argument.

What’s next?#

Check: