How to make an iterator

Iterators

An iterator is a way you can iterate though a list. Well…not really theforloopisthethingthatiteratesbutthatsbesidesthepoint. They are quite hard to understand and code but easy to use. I learned how to use iterators only a couple of days ago and I have never seen any one make an iterator before so I might as well make one myself and share my little learning xp.

In lua there are several different iterators. Each are used differently and have different types: stateless and stateful(or multistate). Each have their own way of doing things.

For loops

There are 2 different types of for loops the numeric:

for i = 1, 10, 2 do
    print(i)
end
-- Output: 1, 3, 5, 7, 9

very simple.

The other is a generic for loop:

local t = {10,20,30}
for i, v in ipairs(t) do
    print(v) 
end
-- Output: 10, 20, 30

Generic for loops can be used with iterators. In this case we are using the standard iterator ipairs

Closures

Anothing thing to know is a closure. A closure is a way we can create put a value into a function so that it can only be accessed within the function. You may have seen it used for OOP using function based objects. Side note: Inheritance is not easy with closures however functions cost less memory than tables and you get privacy.

Example of function based OOP

Module
local RNG = Random.new()
return function()
    local numberWang = RNG:NextNumber(0,10000000000000000000)
    return function(guess: number) -- Notice how the only way to access the object is through the function
        if guess == numberWang then
            print("THATS NUMBER WANG!")
        else
            warn("NOT NUMBER WANG")
        end
    end
end
Script
local newNumberWang = require(script.NumberWang)

local NumberWang = newNumberWang()

NumberWang(932.51/230.29)
-- Output: THATS NUMBER WANG!

How does an iterator even work?: Stateless Iterators

Well, I’ll save you the explanation. This is what is going on behind the scenes.

local iter, t, control = ipairs(t)    -- creates the iterator
local element
while true do
	control, element = iter(t,control)   -- calls the iterator (IM IN YR LOOP UPPIN YR ITERATOR)
	if element == nil then break end -- breaks when function returns nil
	print(element)
end

This is what’s going on behind the scenes when you do this

for control, element in ipairs(t) do
    print(element)
end

This uses the ipairs iterator. It iterates everything in index-value. and it returns them. Notice what’s going on. The initial ipairs() call returns three values, First the iterator function. Then the Table and finally the control.

The control tells us what the last element view is. So, the control number always starts of at 0.
So, we can do this!

local iter, t, control = ipairs(t)

for index, value in iter, t, control do
	print(index,value)
end

This type of iterator is what we call a stateless as this type of iterator does not use closures to keep track of what the last field was. pairs is also stateless. As such if we do this.

local answers = {	
	A = "Yes",
	B = "No",
	C = "Maybe"
}
local actions = {
	First = "Run",
	Second = "Walk",
	Third = "Jump"
}

local iter1, t1, control1 = pairs(answers)
local iter2, t2, control2 = pairs(actions)

print(iter1 == iter2)
-- Output: true

we can see that a new iterator function is not created the same one is being reused over and over again. The downside is that we have to store the state or control variable ourselves in a value. (The for loop does this automatically)

If you are interested here would be how ipairs and pairs are coded:

SecretCode

Remeber to use breakpoints and watches when learning about code like this as you can see step by step what is happening.

local function pairs(t)
  -- next is the iterator function
  -- t is the invariant state
  -- control variable is nil
  return next, t, nil -- Notice how pairs loop uses next as iterator function
end
-- function which performs the actual iteration
local function ipairs_iter(t, i)-- Notice we have to pass the state into the function(the function doesn't store the state)
  local i = i + 1  -- next index in the sequence (i is the control variable)
  local v = t[i]   -- next value (t is the invariant state)
  if v ~= nil then
    return i, v    -- index, value
  end
  return nil       -- no more values (termination)
end

-- generator function which initializes the generic for loop
local function ipairs(t)
  -- ipairs_iter is the iterator function
  -- t is the invariant state (table to be iterated)
  -- 0 is the control variable (first index)
  return ipairs_iter, t, 0
end

I went ahead and coded up an iterator function which produces the squares up till a certain number

function square(max: number,control: number): (any?, any?)
	if control < max then
		control +=  1
		return control, control*control
	else
		return nil
	end
end

function getSquares(number)
	return square, number, 0
end

for i,n in getSquares(3) do
	print(n)
end

Pairs is similar however instead of returning three values(iterator function. invariant state and control) it only returns two. We also notice that the iteratof function for pairs is next. So some people skip calling pairs and just it’s returns pass this. Pairs is basically unnecesary.

for i, v in next, t do
   -- Code 
end

One thing to note: If you are hardcode and use --!strict like me then you will get a linting error. This is because for our loop to terminate our function needs to return a variant. So I put : (any?, any?) after the iterator function (square) and add a return nil to silence it. It’s really the only way i found.

FUN FACT: utf8.codepoint is an iterator used to iterate strings (How do I loop a string?)

How does an iterator even work?: Statefull Iterators

Look at this graph iterator.

function list_iter (t)
	local i = 0
	local n = table.getn(t)
	return function (): (any?)
		i = i + 1
		if i <= n then 
			return t[i] 
		else
			return nil
		end
	end
end

local t = {10,20,3}
for value in list_iter(t) do
	print(t)
end
-- Output: 10, 20, 30

The difference is that when you call the iterator it creates a function and then returns it. What can we do with this function that we couldn’t do with the others? Coroutines functions,(iterating in parallel) Closures allow complete secrecy of your code.What’s inside the functions stays inside the function. After all we only have a finite amount of namespace. Stateless iterators are better in general than Stateful iterators because they use much less RAM

Complex State Iterators

What are these? Basically if you don’t have enough to store your data in a single control variable and an invariant then you could use a closure to hold the state or you can create a complex state. The complex state would likely be an object e.g. table or function of which you would also be able to get the invariant.

I don’t really want to make one of these as I find them unnecessary. I can’t think of a reason why you would want to use one of these.

Use cases

Why should you use iterators? Simple: Abstraction. When it comes to code maintainability hiding away swaths of code in Objects or Functions just make things look much better. It’s much easier to debug once you get the initial stuff setup.

Take for example: looping a tensor

local Tensor = {
    {1,6},
	{2,8}
}

Usually you would loop a tensor like this:

for i = 1, #Tensor do
	for j = 1, #Tensor[1] do
		local v = Tensor[i][j]
	end
end

But we can create an iterator to hide all of that code for us:

for i, j, v in tensorLoop(Tensor) do
	-- Code
end

And this is just the beginning. There is so much more you can do when you start to create your own iterators.

True Iterators

The iterators we have looked at today: next(), ipairs() and pairs() are not “real” iterators. As technically they don’t iterate anything (They only provide an easy way to iterate something.)

Unfortunately I do not exactly know why or how to use them properly yet. So instead I will just link the docs

https://www.lua.org/pil/7.5.html

Closing notes

I am pretty new to this stuff so I might have made some mistakes with what I have said. Feel free to leave feedback.

I have found iterators really cool and wondered why no one is talking about them so…here they are.

Hopefull you learned something. Happy coding.

13 Likes