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.