1 Introduction
This article describes and provides a few implementations of Lua iterators. These iterators are roughly similar in functionality to a few of the tools in the "Lua Functional" library. See -- https://luafun.github.io/
Motivation -- My goal is not just to make available a few usable tools, but also to provide sample code that can be copied and modified to produce custom iterators of your own.
2 Other approaches
You should note that the implementations given below are not the only way to implement Lua iterators. You may also want to consider the following approaches:
- Use the "Lua Functional" library itself. See -- https://luafun.github.io/
- Coroutines -- see below.
2.1 Coroutines as iterators
With this strategy, we implement producer/consumer pairs of functions as Lua coroutines. For a explanation, see -- https://www.lua.org/pil/9.2.html Imagine a chain of Lua coroutines. Each coroutine in the chain is a producer for the next coroutine in the chain, which "consumes" the items that the previous coroutine produces (with coroutine.yield), one by one, by "resuming" (coroutine.resume) that producer coroutine. And, then the consumer, in turn, is or can be a producer for the next coroutine in the chain.
Here is a simple example:
function file_producer(filename) local status = 'normal' local infile = io.open(filename, 'r') while true do if status == 'closed' then coroutine.yield(nil) else line = infile:read('l') if line == nil then infile:close() status = 'closed' end coroutine.yield(line) end end end function file_to_upper(filename) print('filename:', filename) local flag, line, line2 local co = coroutine.create(file_producer) while true do flag, line = coroutine.resume(co, filename) if line == nil then coroutine.yield(nil) else line2 = string.upper(line) coroutine.yield(line2) end end end
Notes:
- Function file_producer implements a Lua coroutine. Each time it is "resumed", it produces another line from the specified file.
- Function file_to_upper is also a Lua coroutine. Each time it is "resumed", it resumes coroutine file_producer to get another line, converts that line to uppercase, and yields (produces) it.
- Notice that there is a similar structure to these two coroutines. Each has (1) a bit of initialization at the start, followed by (2) a loop. Each time the loop is repeated, it produces ("yields") one item.
And, here is a example of its use:
> comod = require('coroutines') > co = coroutine.create(comod.file_to_upper) > coroutine.resume(co, 'test.txt') _[164] = true _[165] = "AAA" > coroutine.resume(co, 'test.txt') _[166] = true _[167] = "BBB" > coroutine.resume(co, 'test.txt') _[168] = true _[169] = "CCC" > coroutine.resume(co, 'test.txt') _[170] = true _[171] = "DDD" > coroutine.resume(co, 'test.txt') _[172] = true _[173] = nil
Here is the contents of the input data file:
aaa bbb ccc ddd
And, without the clutter and output, you can do this:
comod = require('coroutines') co = coroutine.create(comod.file_to_upper) coroutine.resume(co, 'test.txt') coroutine.resume(co, 'test.txt') coroutine.resume(co, 'test.txt') coroutine.resume(co, 'test.txt') coroutine.resume(co, 'test.txt')
The following is an example of using the above iterator/coroutines to accumulate a result, in this case, a table containing the items produced by the iterator/coroutine:
function accum_lengths(filename) local flag, line local accum_tbl = {} local co = coroutine.create(M.file_to_upper) while true do flag, line = coroutine.resume(co, filename) if line == nil then return accum_tbl end table.insert(accum_tbl, {line, string.len(line), utf8.len(line)}) end end function show_codepoints(str, indent) for idx, val in ipairs({utf8.codepoint(str, 1, #str)}) do print(string.format( '%s%d. char: "%s" codepoint: %04d', indent, idx, utf8.char(val), val )) end end function test01(filename) print('test 12') local indent = ' ' local accum = accum_lengths(filename) for k, v in ipairs(accum) do print(string.format('%d. line: "%s" length: %s utf8 length: %s', k, v[1], v[2], v[3])) M.show_codepoints(v[1], indent) end end
Notes:
- Function accum_lengths collects the items produced (yielded) by the iterator and inserts them in a table. When the items have been exhausted, it returns that table.
- Then it displays information about each of the items in that table.
3 Some sample implementations
You can find the source code for these sample implementations of Lua iterators here -- lua_iterator_examples.zip
3.1 Cycle
Repeat an iterator over and over. See -- https://luafun.github.io/compositions.html#fun.cycle
Consider using this with take (https://luafun.github.io/slicing.html#fun.take) or take_n (https://luafun.github.io/slicing.html#fun.take_n) or take_while or some other mechanism to stop iteration.
#!/usr/bin/env lua Usage = [[ synopsis: Run a cycle iterator. usage: lua test16.lua <limit> ]] local M = {} Data = {2, 5, 3, 4} local function make_cycle_iter(data) -- [1] -- carry mutable data in closure. local pos = 0 local count = 0 local sum = 0 local sumofsquares = 0 local function iter(data, pos) pos = pos + 1 if pos > #data then pos = 1 end int_item = tonumber(data[pos]) count = count + 1 sum = sum + int_item sumofsquares = sumofsquares + (int_item * int_item) return pos, int_item, count, sum, sumofsquares end return iter, data, pos end function M.test(limit) limit = limit or 10 local idx = 0 for pos, item, count, sum, sumofsquares in make_cycle_iter(Data) do print(idx, pos, item, count, sum, sumofsquares) idx = idx + 1 if idx >= limit then break end end end function test() if #arg ~= 1 then print(Usage) os.exit() end arg1 = tonumber(arg[1]) M.test(arg1) end --test() return M
3.2 Drop-while
After skipping a sequence of items that satisfy a predicate, iterate over the remaining elements. See -- https://luafun.github.io/slicing.html#fun.drop_while
#!/usr/bin/env lua Usage = [[ synopsis: Run a dropwhile iterator. Iterate over a table of integers. Drop (ignore) integers until a predicate fails, then process all the remaining integers. For each item that is processed print the iteger, the accumulated sum of integers, and the accumulated sum of squares of integers. The iterator function passes state to next iteration in an argument (a table), It does not use a closure. A simple test: > > iter = require 'Iterators.drop_while' > iter.run({3, 6, 9, 2, 4, 5, 7}) pos count item sum sos 4 1 2 2 4 5 2 4 6 20 6 3 5 11 45 7 4 7 18 94 > Funcion `make_dropwhile_iter` takes a predicate function and an integer array as arguments. It returns an iterator function and a state (table). The iterator function can be called as follows: iterator_fn(state_tbl) Example: > > iter = require 'Iterators.drop_while' > iter_fn, state_tbl = iter.make_dropwhile_iter(p1, {4, 8, 3, 6}) > iter_fn(state_tbl) 3 1 3 3 9 > iter_fn(state_tbl) 4 2 6 9 45 > iter_fn(state_tbl) nil > The state can be reset and reused by setting various attributes in it. E.g. > > state_tbl.pos = 0 > state_tbl.sum = 0 > state_tbl.searching = true > iter_fn(state_tbl) 3 7 3 3 179 > iter_fn(state_tbl) 4 8 6 9 215 > iter_fn(state_tbl) nil > command line usage: $ LUACLI=1 lua filter_false.lua <integer1> <integer1> ... example: $ LUACLI=1 ./filter_false.lua 44 66 77 99 88 22 ]] local M = {} Data = {2, 5, 3, 4} function M.make_dropwhile_iter(predicate, data) data = data or Data local state_tbl = { pos = 0, count = 0, sum = 0, sumofsquares = 0, predicate = predicate, data = data, searching = true } local function iter(state) local int_item state.pos = state.pos + 1 if state.searching then while true do if state.pos > #state.data then return nil end int_item = tonumber(state.data[state.pos]) if state.predicate(int_item) then --print('yes', int_item) state.pos = state.pos + 1 else --print('no', int_item) state.searching = false break end end end if state.pos > #state.data then return nil end int_item = tonumber(state.data[state.pos]) state.count = state.count + 1 state.sum = state.sum + int_item state.sumofsquares = state.sumofsquares + (int_item * int_item) return state.pos, state.count, int_item, state.sum, state.sumofsquares end return iter, state_tbl end function M.predicate01(item) return item % 3 == 0 end function M.test(predicate, data) print('pos', 'count', 'item', 'sum', 'sos') for pos, count, item, sum, sumofsquares in M.make_dropwhile_iter(predicate, data) do print(pos, count, item, sum, sumofsquares) end end function M.run(args) if #args == 1 and (args[1] == '-h' or args[1] == '--help') then print(Usage) os.exit() elseif #args < 1 then data = nil else data = {} for _key, value in ipairs(args) do table.insert(data, tonumber(value)) end end M.test(M.predicate01, data) end if os.getenv('LUACLI') ~= nil then M.run(arg) else return M end
3.3 Filter-false
Iterate over a sequence filtering out (skipping) those items that do not satisfy a precicate.
#!/usr/bin/env lua Usage = [[ synopsis: Run a filterfalse iterator. Iterate over a table of integers. For each item that satisfies the predicate, print the iteger, the accumulated sum of integers, and the accumulated sum of squares of integers. Passes state to each iteration in an argument (a table), Does not use a closure. A simple test: > > iter = require 'Iterators.filter_false' > iter.run({11, 22, 33, 44, 55}) no 11 yes 22 2 1 22 22 484 no 33 yes 44 4 2 44 66 2420 no 55 > Funcion `make_filterfalse_iter` takes a predicate function and an integer array as arguments. It returns an iterator function and a state (table). The iterator function can be called as follows: iterator_fn(state_tbl) Example: > > iter_fn, state_tbl = iter.make_filterfalse_iter(iter.predicate01, {144,133, 155, 144, 166, 155}) > iter_fn(state_tbl) yes 144 1 1 144 144 20736 > iter_fn(state_tbl) no 133 no 155 yes 144 4 2 144 288 41472 > The state can be reset and reused by setting various attributes in it. E.g. state_tbl['pos'] = 0 state_tbl['sum'] = 0 command line usage: $ lua filter_false.lua <int1> <int1> ... example: $ LUACLI=1 ./filter_false.lua 44 66 77 99 88 22 ]] local M = {} Data = {2, 5, 3, 4} function M.make_filterfalse_iter(predicate, data) data = data or Data local state_tbl = { pos = 0, count = 0, sum = 0, sumofsquares = 0, predicate = predicate, data = data } local function iter(state) local int_item = tonumber(state.data[state.pos]) while true do state.pos = state.pos + 1 if state.pos > #state.data then return nil end int_item = tonumber(state.data[state.pos]) if state.predicate(int_item) then print('yes', int_item) break else print('no', int_item) end end state.count = state.count + 1 state.sum = state.sum + int_item state.sumofsquares = state.sumofsquares + (int_item * int_item) return state.pos, state.count, int_item, state.sum, state.sumofsquares end return iter, state_tbl end function M.predicate01(item) return item % 2 == 0 end function M.test(predicate, data) limit = limit or 10 for pos, count, item, sum, sumofsquares in M.make_filterfalse_iter(predicate, data) do print(pos, count, item, sum, sumofsquares) end end function M.run(args) if #args == 1 and (args[1] == '-h' or args[1] == '--help') then print(Usage) os.exit() elseif #args < 1 then data = nil else data = {} for _key, value in ipairs(args) do table.insert(data, tonumber(value)) end end M.test(M.predicate01, data) end if os.getenv('LUACLI') ~= nil then M.run(arg) else return M end
3.4 Pair-wise
Iterate over a sequence of items. Return (iterate over) successive pairs of consecutive items. See the examples given in the usage/description comments in the code, below.
#!/usr/bin/env lua Usage = [[ synopsis: Run a pairwise iterator. Iterate over a table of integers. Create an array of arrays (2-tuples) of successive overlapping pairs taken from the input. The iterator function passes state to next iteration in an argument (a table), It does not use a closure. A simple test: > > iter = require 'Iterators.pairwise' > iter.run({3, 6, 9, 2, 4, 5, 7}) pair: 3 6 pair: 6 9 pair: 9 2 pair: 2 4 pair: 4 5 pair: 5 7 > Funcion `make_pairwise_iter` takes an integer array as its argument. It returns an iterator function and a state (table). The iterator function can be called as follows: iterator_fn(state_tbl) Example: > > iter = require 'Iterators.pairwise' > iter_fn, state_tbl = iter.make_pairwise_iter({6, 5, 4, 3, 2}) > iter_fn(state_tbl) table: 0x56484217c2f0 > iter_fn(state_tbl) table: 0x56484217c660 > iter_fn(state_tbl) table: 0x56484217c980 > iter_fn(state_tbl) table: 0x56484217ccb0 > iter_fn(state_tbl) nil > state_tbl.pairs table: 0x56484217c0f0 > utils.show_table(state_tbl.pairs) 1. -------------------- key: 1 value: 6 key: 2 value: 5 2. -------------------- key: 1 value: 5 key: 2 value: 4 3. -------------------- key: 1 value: 4 key: 2 value: 3 4. -------------------- key: 1 value: 3 key: 2 value: 2 > The state can be reset and reused by setting various attributes in it. E.g. > > state_tbl.pos = 0 > state_tbl.pairs = {} > > iter_fn(state_tbl) table: 0x56484217f4a0 > iter_fn(state_tbl) table: 0x56484217f8b0 > iter_fn(state_tbl) table: 0x56484217fc60 > iter_fn(state_tbl) table: 0x56484215eca0 > iter_fn(state_tbl) nil > utils.show_table(state_tbl.pairs) 1. -------------------- key: 1 value: 6 key: 2 value: 5 2. -------------------- key: 1 value: 5 key: 2 value: 4 3. -------------------- key: 1 value: 4 key: 2 value: 3 4. -------------------- key: 1 value: 3 key: 2 value: 2 > command line usage: $ LUACLI=1 lua pairs.lua <integer1> <integer1> ... example: $ LUACLI=1 ./pairs.lua 44 66 77 99 88 22 ]] local M = {} Data = {2, 5, 3, 4} function M.make_pairwise_iter(data) data = data or Data local state_tbl = { pos = 0, data = data, pairs = {}, } local function iter(state) local pair state.pos = state.pos + 1 if state.pos > #state.data then return nil end if state.pos < #state.data then pair = {state.data[state.pos], state.data[state.pos + 1]} table.insert(state.pairs, pair) return pair else return nil end end return iter, state_tbl end function M.test(data) local iter, state_tbl iter, state_tbl = M.make_pairwise_iter(data) for pair in iter, state_tbl do end local pairs = state_tbl.pairs for idx, value in ipairs(pairs) do print('pair:', value[1], value[2]) end end function M.run(args) local data if #args == 1 and (args[1] == '-h' or args[1] == '--help') then print(Usage) os.exit() elseif #args < 1 then data = nil else data = {} for _key, value in ipairs(args) do table.insert(data, tonumber(value)) end end M.test(data) end if os.getenv('LUACLI') ~= nil then M.run(arg) else return M end
3.5 Partition
Iterate of a sequence returning a sequences of a given size of non-overlapping consecutive items.
#!/usr/bin/env lua local Usage = [[ synopsis: Run a partition iterator. Iterate over a table of items. Create an array of arrays (n-tuples) of successive non-overlapping tuples taken from the input. The iterator function passes state to next iteration in an argument (a table), It does not use a closure. A simple test: > > iter = require 'Iterators.partition' > iter.run({3, 11, 22, 33, 44, 55, 66, 77, 88, 99}) tuple 1: 11 22 33 tuple 2: 44 55 66 tuple 3: 77 88 99 > Funcion `make_partition_iter` takes an integer array and a partition/tuple size as its arguments. It returns an iterator function and a state (table). The iterator function can be called as follows: iterator_fn(state_tbl) Example: > > iter_fn, state_tbl = iter.make_partition_iter({11, 22, 33, 44, 55, 66, 77, 88, 99}, 4) > iter_fn(state_tbl) table: 0x556973186e80 > iter_fn(state_tbl) table: 0x556973187260 > iter_fn(state_tbl) table: 0x556973199750 > iter_fn(state_tbl) table: 0x556973198da0 > iter_fn(state_tbl) nil > state_tbl.tuples table: 0x5569731993d0 > utils.show_table(state_tbl.tuples) 1. -------------------- key: 1 value: 11 key: 2 value: 22 key: 3 value: 33 key: 4 value: 44 2. -------------------- key: 1 value: 55 key: 2 value: 66 key: 3 value: 77 key: 4 value: 88 3. -------------------- key: 1 value: 99 4. -------------------- > The state can be reset and reused by setting various attributes in it. E.g. > > state_tbl.pos = 0 > state_tbl.tuples = {} > state_tbl.tuplesize = 4 > iter_fn(state_tbl) table: 0x55697319cb80 > iter_fn(state_tbl) table: 0x55697319d420 > iter_fn(state_tbl) table: 0x55697319d7c0 > iter_fn(state_tbl) table: 0x55697319db80 > iter_fn(state_tbl) nil > utils.show_table(state_tbl.tuples) 1. -------------------- key: 1 value: 11 key: 2 value: 22 key: 3 value: 33 key: 4 value: 44 2. -------------------- key: 1 value: 55 key: 2 value: 66 key: 3 value: 77 key: 4 value: 88 3. -------------------- key: 1 value: 99 4. -------------------- > command line usage: $ LUACLI=1 lua partition.lua <tupesize> <item> <item> ... example: $ LUACLI=1 ./partition.lua 4 11 22 33 44 55 66 77 88 99 ]] local M = {} function M.make_partition_iter(data, tuplesize) local state_tbl = { pos = 0, data = data, tuplesize = tuplesize or 2, tuples = {}, } local function iter_fn(state) if state.pos >= #state.data then return nil end local tuple = {} for idx = 1, state.tuplesize do table.insert(tuple, state.data[state.pos + idx]) end table.insert(state.tuples, tuple) state.pos = state.pos + state.tuplesize return tuple end return iter_fn, state_tbl end function M.test(data, tuplesize) local iter_fn, state_tbl iter_fn, state_tbl = M.make_partition_iter(data, tuplesize) for tuple in iter_fn, state_tbl do -- no-op end local tuples = state_tbl.tuples for idx, value in ipairs(tuples) do print(string.format('tuple %d:', idx)) for i, v in ipairs(value) do print('', v) end end end function M.run(args) local data if (#args == 1 and (args[1] == '-h' or args[1] == '--help')) or #args < 1 then print(Usage) os.exit() elseif #args < 2 then tuplesize = tonumber(args[1]) data = nil else tuplesize = tonumber(args[1]) data = {} for idx = 2, #args do table.insert(data, args[idx]) end end M.test(data, tuplesize) end if os.getenv('LUACLI') ~= nil then M.run(arg) else return M end