Deep(er) dive on Godot custom iterators, and the mysterious arg

The custom iterator example here leaves a lot out. My expectation, which might have been wrong, is that if I do

var arr = [1, 4, 7]

for x in arr:
  for y in arr:
    print(x, y)

that I should get the same behavior from

var iter = ForwardIterator.new(1, 10, 3)

for x in iter:
  for y in iter:
    print(x, y)

It doesn’t work that way, but it can be made to. The example ignores the “arg” parameter, and understanding this parameter is necessary if you want to make custom iterators that are reentrant or that encapsulate other iterators, such as a concat iterator, a zip iterator, or a filter or map iterator.

When an iterator is used in a for loop, the _iter_init function is passed an array with one null element in it. arg[0] is where you should store your iterator state, so that if you are called in a nested for loop or other situation, your code will be reentrant.

Your _iter_next is given the same arg value, and you’ll find that arg[0] contains whatever you put into it in _iter_init. In _iter_next you should update arg[0] with the new state. Don’t try to resize this array. It has to be the same single-element array that was given to you.

The _iter_get works a little differently. The value of arg[0] is passed to it, not the whole arg array.

So a “corrected” ForwardIterator class would look like this:

class ForwardIteratorCorrected:
	var start
	var end
	var increment

	func _init(start, stop, increment):
		self.start = start
		self.end = stop
		self.increment = increment

	func should_continue(arg):
		return (arg[0] < end)

	func _iter_init(arg):
		arg[0] = start
		return should_continue(arg)

	func _iter_next(arg):
		arg[0] += increment
		return should_continue(arg)

	func _iter_get(current):
		return current

And this will behave the same as the array example when used in nested for loops.

If your iterator encapsules another iterator, you’ll want to allocate your own [null] array for each of the contained iterators that you’re using, and you’ll need to implementation the same value vs reference behavior when you call the nested _iter_get. Here’s an example that wraps an array so that we can call the internal iterator functions, and another that combines two iterators into pairs of elements from each iterator, like a zipper:

class ArrayIterator:
	var array
	
	func _init(array):
		self.array = array
		
	func _iter_init(args):
		# Store the next array index in the state slot
		args[0] = 0
		return array.size()
		
	func _iter_next(args):
		# Increment the array index
		args[0] += 1
		return args[0] < array.size()
		
	func _iter_get(index):
		# Get the indexed element
		return array[index]
		
class ZipIterator:
	var first
	var second
	
	func _init(first, second):
		self.first = first
		self.second = second
	
	func _iter_init(args):
		# Initialize state for each sub-iterator
		args[0] = [[null], [null]]
		return first._iter_init(args[0][0]) and second._iter_init(args[0][1])
		
	func _iter_next(args):
		return first._iter_next(args[0][0]) and second._iter_next(args[0][1])
		
	func _iter_get(states):
		# Note that "states" is a pair of sub-args, not the array that contains the pair
		return [first._iter_get(states[0][0]), second._iter_get(states[1][0])]
		
func _ready() -> void:
	
	var a = ArrayIterator.new(range(3))
	var b = ArrayIterator.new(["red", "green", "blue", "skipped"])
	
	for pair in ZipIterator.new(a, b):
		print(pair)
2 Likes