ELI5: How do for loop iterators work? (specifically gmatch)

I’m familiar with numeric for loops and generic for loops, but the one thing that I can never understand is how exactly the iterators function. Chiefly, I don’t understand how string.gmatch’s for loops work.

for a in string.gmatch() do

How is this able to work without pairs, ipairs or next? Doesn’t it return a key-value table?

As for the aforementioned point about iterators, I don’t really understand them. I know that pairs and ipairs call next, but what’s the difference between using them and just calling in next? I know that pairs is for key-values and ipairs is for numeric indices-values, but can I not just skip those and use next?

And finally, for for loops in general, are there any conventions that can be stuck in here? Can you create your own iterator function and attach it to a for loop? The general convention seems to be:

for A in B do

So, from that, what can be placed in A and B respectively?

6 Likes

With regards to your final question, you certainly can create your own iterator function. Take this one example from lua-users wiki: Iterators Tutorial which allows you to iterate over the squares of a given number nbvals

The following iterator will return a sequence of squared values. When we return no value (i.e. n >= state), Lua returns nil which terminates iteration. Notice the iterator is returning the next value in the sequence for a given iteration. We use the state to hold the number of iterations we wish to perform.

-- state: The number of iterations to perform
-- n: a count of performed iterations
local function square(state, n)
    if n < state then
        n = n + 1
        return n, n*n
    end
end

-- Wrap the iterator in a function similar to pairs()
local function squares(nbvals)
    return square, nbvals, 0
end

-- Calling the iterator function
-- calling square() directly would be:
-- for s, n in square,5,0 do ... end
for s, n in squares(5) do
    print(s, n)
end

--[[
1   1
2   4
3   9
4   16
5   25
]]--

The above link goes into a lot more detail on custom iterators, including an ipairs implementation.

This link here also discusses iterators: lua-users wiki: For Tutorial

With regards to your main question, I’m not too sure because I haven’t used string.gmatch() often, but I’ll try and have a look.

1 Like

Answered via New Member Discord.

Thanks @school_month and @ScriptedForum for guiding me through this (happening now as this post is being made as well). I’m still open for responses though, since the concept isn’t fully down.

1 Like

(If anything I’m about to say is incorrect please correct me I’d love to learn more about this)

So I thought I’d try to go a little more in-depth here since my discord explanation was all over the place.

The for generator wants a function to call whether it’s directly or from calling a function and that function returns another function (like pairs which returns next, ipairs, and string.gmatch). If there are any arguments next to the function it’ll pass those to it for the first call after that it passes the returns from the previous call. It’ll keep calling the function you passed until it returns nil.
** Also in addition I believe the generator only allows a maximum of 2 arguments to be added next to the function **

Between the for and the in is where your variables go, they’re the returns from the function call and they’re local to the do block.

A quick example function (functionally the same as ipairs):

local function myGenerator(tbl, index)
    index = (index or 0) + 1
    if tbl[index] == nil then return end
    
    return index, tbl[index]
end

for index, value in myGenerator, {"a", "b", "c", 10, {}, false} do
    print(index, value)
end
1 Like

PiL - Programming in Lua : 7.3

In the wiki, there is an example for these kinds of loops, which includes what string.gmatch probably does.
Loops

local function gfind(stringToSearch, pattern, start)
	local start = start or 1 -- Default value is 1
	return function()
		local beginning, ending = stringToSearch:find(pattern, start) -- Start searching at the specified location
		if beginning and ending then -- Check to make sure that the match is there
			start = ending + 1 -- Add one to the ending so the pattern will start to look after the last match
			return beginning, ending, stringToSearch:sub(beginning, ending) -- return the 3 values
		end
	end
end

Something minor, but for in pairs loops only take into account the first two arguments passed after the function, not all.

for _ in print,1,2,3,4,5,6,7,8,9,10 do end
--> 1  2

If the first thing returned by the function is nil, then the loop breaks.

Otherwise, everything returned is passed.

for a,b,c,d,e,f in function()return 1,2,3,4,5,6 end do
    print(a,b,c,d,e,f)
    break
end
--> 1  2  3  4  5  6

The first thing returned by the function becomes the second argument next time the function is called.
As in, the first paramater will be static, while the second can change.

local h = 0
local function iter(x,y)
	if h == 10 then return end
	h = h + 1
	return y+1,x+1
end
for a,b in iter,0,0 do print(a,b) end
--> 1  1
--> 2  1
--> 3  1
--> 4  1
--> 5  1
--> 6  1
--> 7  1
--> 8  1
--> 9  1
--> 10  1

Example useage:

local function iter(End,pos)
	pos = (pos and pos + 1) or 1
	if End >= pos then
		return pos
	end
end
for position in iter,5 do
	print(position)
end
--> 1
--> 2
--> 3
--> 4
--> 5

A will be what is returned by B, as for B, the first thing should be a function, and the next two can be whatever you desire.

Explanation with how next works with the iterator-
next loops are done like this,

for i,v in next,t do

end

The first call will be next(t,nil) which gets the first thing to be iterated though with its key and value.

print(next{"first","second","third"})
--> 1  first

The next call takes into account the last key (next’s first return is the key, and as mentioned before, the first return becomes the second argument) and as will be next(t,lastkey)

print(next({"first","second","third"},1))
--> 2  second

This continues on until next returns nil, breaking the loop.

print(next({"first","second","third"},3))
--> nil

So it ends up like this

for i,v in next,{"first","second","third"} do
    print(i,v)
end
--> 1  first
--> 2  second
--> 3  third
5 Likes

So, using this as a template,

for x, y, z in Iterator, t, b, c do
	Mapper(x, y, z)
end

I think I found a concise way to reproduce the generic for loop without using it:

do
	local x, y, z = Iterator(t, b)

	while x ~= nil do
		Mapper(x, y, z)
		x, y, z = Iterator(t, x)
	end
end

Though not very useful, it plays off of more common coding structures.

1 Like

Oooohhh, so that’s what it’s called. “Stateless iterator”. Looks like this one’s on me for not being the one to search. :confounded:

Thanks for the link and the explanation. This is fairly satisfying.

1 Like

Expanding on this, it’s because Lua doesn’t think of these as “arguments”; the code generated marks them as “generator”, “state” and “control”.

for rets... in generator, state, control do
end

These are internally passed around in an optimized fashion (at least in vanilla Lua) which is why no more than 3 values are allowed, and any not provided are assumed nil.

It’s probably not very helpful to know, but gmatch basically creates a closure which holds its state index and match info within its upvalues with some magic. The function that it uses is always the same, though.

3 Likes