Genny

Genny

Genny is a lua libraries for working with generators.

Lua defines iterators that can be used with for loops. Unfortunately, since they are defined as 3 separate values, it is very hard to manipulate these iterators. Genny defines so-called "generators", which nothing but lua iterators that don't take any arguments. Since this means a generator is a single (callable) value, it's much easier to pass them around, manipulate them, store them, etc.

Of course, Genny is more than this concept, and comes with a bunch of functions to create, manipulate and use generators. Note that where other libraries tend to create lots of intermediary tables, no functions create tables unless otherwise specified.

Generators

The following functions all produce generators. While the code examples will all use lua's generic for loops to iterate over generators, it is possible to iterate over them manually. Calling the generator (e.g. gen() for a generator gen) produces the next values, when the first return value is nil, the generator ends. Note that calling a generator after it has returned nil is undefined behaviour.

genny.generator

gen = genny.generator(it, state, init)

This function takes any lua iterator (including generators) and turns them into a generator. It's mostly useful when working with other libraries, or iterators that haven't already been wrapped by Genny.

Example:

g = genny.generator(ipairs{1, 2, 3})
for i, v in g do
    print(i, v) -- prints 1 and 1, then 2 and 2, then 3 and 3
end

genny.ipairs

gen = genny.ipairs(t)

Shorthand for genny.generator(ipairs(t)), iterates over a sequence and returns the index and its value. Stops on the first nil value.

genny.ripairs

gen = genny.ripairs(t)

Much like genny.ipairs, but iterates in reverse. Starts at index #t and ends at index 1. Mostly useful for when the table is to be modified in the loop, as the indices won't change when elements shift down.

genny.pairs

gen = genny.pairs(t)

Shorthand for genny.generator(pairs(t)), iterates over a table and returns the key and value.

genny.range

gen = genny.range([from,] to, [step])

A generator that iterates over all integers in the range from up to and including to. Generator version of for i = from, to. In case from is omitted, a value 1 is assumed. In case step is specified, its value is added each iteration.

Example:

for i in genny.range(3) do
    print(i) -- prints 1, 2, 3
end

for i in genny.range(2, 4) do
    print(i) -- prints 2, 3, 4
end

for i in genny.range(1, 5, 2) do
    print(i) -- prints 1, 3, 5
end

for i in genny.range(3, 1, -1) do
    print(i) -- prints 3, 2, 1
end

genny.gmatch

gen = genny.gmatch(str, pattern)

Shorthand for genny.generator(string.gmatch(str, pattern)), iterates over all matches of pattern in str.

genny.split

gen = genny.split(str, delim, [plain, [empty]])

Iterates over all substrings in str, delimited by pattern delim. In case plain is true, delim is treated as a literal string, instead of a pattern. If plain is absent, its value is assumed to be false. In case empty is true or absent, empty substrings are also returned, if it is false, empty substrings are skipped.

Example:

for column in genny.split("a,b,c,d", ",") do
    print(column) -- prints a, then b, then c, then d
end

genny.once

gen = genny.once(value)

Returns value the first time it is called, returns nil from then on. Usually only useful in combination with generator manipulation.

Combinator

Combinators act upon multiple generators to form a new generator.

genny.join

gen = genny.join(first, ...)

Iterates over all generators in turn. First all of first is iterated over, then all of second, then all of third, etc.

Example:

local t1 = {1, 2, 3}
local t2 = {"a", "b", "c"}
for i, v in genny.join(genny.ipairs(t1), genny.ipairs(t2)) do
    print(v) -- prints 1, 2, 3, a, b, c
end

genny.roundrobin

gen = genny.roundrobin(first, ...)

Iterates over all generators simultaneously. Returns the first value of first, then the first value of second, then the first value of third, etc, then the second value of first, the second value of second, the second value of third, etc.

Example:

local t1 = {1, 2, 3}
local t2 = {"a", "b", "c"}
for i, v in genny.roundrobin(genny.ipairs(t1), genny.ipairs(t2)) do
    print(v) -- prints 1, a, 2, b, 3, c
end

Operators

Operators take one generator, and return a new, modified generator.

genny.enumerate

gen = genny.enumerate(gen)

Adds a counter to each iteration, much like ipairs.

Example:

for c in genny.gmatch("abc", ".") do
    print(c) -- prints a, b, c
end
for i, c in genny.enumerate(genny.gmatch("abc", ".")) do
    print(i, c) -- prints 1 a, 2 b, 3 c

genny.map

gen = genny.map(gen, func)

Applies func to each iteration of the input generator, then returns its return values. Useful when an operation needs to be applied to each element. Note that map can change the number of values, their contents, and even stop iteration if it returns nil as first value.

Example:

for c in genny.gmatch("abc", ".") do
    print(c) -- prints a, b, c
end
for c in genny.map(genny.gmatch("abc", "."), string.upper) do
    print(c) -- prints A, B, C
end

genny.filter

gen = genny.filter(gen, func)

Applies func to each iteration of the input generator, and skips an iteration if func returns false or nil.

Example:

function even(x)
    return x % 2 == 0
end
for x in genny.filter(genny.range(5), even) do
    print(x) -- prints 2, 4
end

genny.discard

gen = genny.discard(gen, [elems])

Discards the first elems keys, mostly useful with iterators like ipairs. If elems is omitted, it defaults to 1 element. This means genny.discard(genny.enumerate(gen)) produces a generator that is equivalent to gen.

Example:

for v in genny.discard(genny.ipairs{4, 5, 6}) do
    print(v) -- prints 4, 5, 6
end

genny.tablify

gen = genny.tablify(gen)

Produces a new generator that no longer returns multiple values, but instead a single table containing those values.

Example:

for iv in genny.tablify(genny.ipairs{4, 5, 6}) do
    print(iv[1], iv[2]) -- prints 1 4, 2 5, 3 6
end

genny.take

gen = genny.take(gen, max)

Returns a new generator that produces at most max iterations. So genny.take(genny.ipairs{1, 2, 3}, 2) only returns the key/value pairs for the first and second elements. If the input generator produces less than max elements, the resulting generator does not add additional elements.

genny.when

gen = genny.when(gen, func)

Much like genny.filter, but when func returns false or nil iteration stops.

Example:

function f(i, v)
    return v == 5
end
for i, v in genny.when(genny.ipairs{4, 5, 6}, f) do
    print(v) -- prints 4
end

Collectors

Collectors consume a generator, and produce a value.

genny.sequence

tbl = genny.sequence(gen)

Returns a sequence, i.e. a table with only positive integer keys, built from the first return values of the generator. Note that if a generator returns multiple values, all but the first are ignored, so it's often useful to combine this with genny.discard.

Example:

t1 = genny.sequence(genny.range(5)) -- t1 is now {1, 2, 3, 4, 5}
t2 = genny.sequence(genny.ipairs{4, 5, 6}) -- t2 is now {1, 2, 3}
t3 = genny.sequence(genny.gmatch("abc", ".")) -- t3 is now {"a", "b", "c"}

genny.dictionary

tbl = genny.dictionary(gen)

Much like genny.sequence, this returns a table containing the returned values. Unlike genny.sequence, it expects two return values (and ignores the rest), using the first as key, and the second as value.

Example:

t = genny.dictionary(genny.gmatch("a=b;c=d;", "(.-)=(.-);")) -- t is now {a = "b", c = "d"}

genny.fold

state = genny.fold(gen, init, func)

Uses func to combine all return values in the generator into one value. func should be a function that takes the current state (initially init) and the return values returned by the generator and produces a new state. This function always calls func in iteration order, and when gen produces no value, it returns init.

Example:

function plus(state, value)
    return state + value
end

sum = genny.fold(genny.range(5), 0, plus) -- returns init+1+2+3+4+5=0+15=15

Utilities

Anything that does not fall in the above categories.

genny.chain

chain = genny.chain(gen)

Chain can be used to flatten the calls, to prevent deeply nested function calls. A chain is itself a valid generator, but also has a next method, which allows adding on an operator.

Example:

s = "a=b;c=d;"

function filter(k, v)
    return k == "c"
end

function mapper(k, v)
    return 4*k, v
end

chain = genny.chain(genny.gmatch(s, "(.-)=(.-);")) -- (a, b), (c, d)
    :next(genny.filter, filter) -- (c, d)
    :next(genny.discard) -- d
    :next(genny.map, string.upper) -- D
    :next(genny.enumerate) -- (1, D)
    :next(genny.map, mapper) -- (4, D)

for k, v in chain do
    print(k, v) -- prints 4 D
end