Lua runner with rich builtin stdlib
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
a/docs/table.md

7.5 KiB

table

a keeps Lua's standard table library (insert, remove, concat, sort, pack, unpack) and adds the higher-order and collection helpers stock Lua leaves out: map/filter/reduce, merge and deep-copy, aggregates, lookups, and a read-only proxy. The additions live on the same global table.

local nums = {1, 2, 3, 4, 5}
local evens = table.ifilter(nums, function(n) return n % 2 == 0 end)
local doubled = table.map(nums, function(n) return n * 2 end)
print(table.sum(nums), table.max(nums))   --> 15   5

Sequences vs. general tables

Lua tables are both arrays and maps, so most helpers come in two flavours. The distinction runs through the whole module:

  • Plain name (map, filter, reduce) iterates with pairs — it visits every key, preserves keys in the result, and the order is unspecified.
  • i-prefixed name (imap-style: ifilter, ireduce, …) iterates with ipairs over the sequence part 1..#t in order, and produces a fresh gapless sequence.

Reach for the i variants when you have an array and want array semantics (order preserved, no gaps); use the plain ones for maps or when keys matter.

Transforming

table.map(tbl, fn)

Apply fn(value) to every value, preserving keys. Order unspecified.

table.map({a = 1, b = 2}, function(v) return v * 10 end)  --> {a = 10, b = 20}

table.filter(tbl, predicate)

Keep the entries where predicate(value) is truthy, preserving keys. On a sequence this may leave gaps — use ifilter to avoid them.

table.filter({a = 1, b = 2, c = 3}, function(v) return v > 1 end)  --> {b = 2, c = 3}

table.ifilter(tbl, predicate)

Keep the sequence elements matching predicate, producing a new gapless sequence in order.

table.ifilter({1, 2, 3, 4}, function(v) return v % 2 == 0 end)  --> {2, 4}

table.filterMap(tbl, fn)

Map and filter in one pass: apply fn(value), and drop entries where fn returns nil. Keys are preserved (may leave gaps on a sequence).

-- keep and square only the even numbers
table.filterMap({a = 1, b = 2, c = 4}, function(v)
    if v % 2 == 0 then return v * v end
end)  --> {b = 4, c = 16}

table.ifilterMap(tbl, fn)

The sequence version of filterMap: map over 1..#t in order, drop nil results, and return a gapless sequence.

table.ifilterMap({1, 2, 3, 4}, function(v)
    if v % 2 == 0 then return v * 10 end
end)  --> {20, 40}

Reducing

table.reduce(tbl, fn, init)

Fold every value into a single accumulator: acc = fn(acc, value), starting from init. Iterates with pairs, so order is unspecified — use it for order-independent folds (sum, product, building a set).

table.reduce({1, 2, 3}, function(acc, v) return acc + v end, 0)  --> 6

table.ireduce(tbl, fn, init)

Like reduce but folds the sequence part in order, for when order matters (e.g. concatenation, left-to-right composition).

table.ireduce({"a", "b", "c"}, function(acc, v) return acc .. v end, "")  --> "abc"

Aggregates

These walk all values with pairs, so they work on sequences and maps alike. Each returns nil for an empty table (except sum, which returns 0), and a NaN value anywhere propagates to the result.

Function Result
table.min(tbl) smallest value, or nil if empty
table.max(tbl) largest value, or nil if empty
table.mean(tbl) arithmetic mean, or nil if empty
table.sum(tbl) sum of values, 0 if empty
local t = {4, 8, 15, 16, 23, 42}
print(table.min(t), table.max(t))   --> 4   42
print(table.sum(t), table.mean(t))  --> 108   18.0

Querying

table.keys(tbl) / table.values(tbl)

Return an array of all keys, or all values. Order is unspecified.

table.keys({a = 1, b = 2})    --> {"a", "b"}  (some order)
table.values({a = 1, b = 2})  --> {1, 2}      (matching order)

table.contains(tbl, value)

Return true if any value equals value (compared with ==).

table.contains({"red", "green"}, "green")  --> true

table.find(tbl, predicate)

Return the first value matching predicate, together with its key: (value, key), or (nil, nil) if none match. Because a matching value could itself be false, test the returned key against nil to distinguish "found false" from "not found".

local v, k = table.find({10, 20, 30}, function(x) return x > 15 end)
print(v, k)  --> 20   2

table.isEmpty(tbl)

Return true if the table has no keys at all (faster and more correct than #tbl == 0, which only inspects the sequence part).

table.isEmpty({})          --> true
table.isEmpty({x = 1})     --> false

Combining and copying

table.merge(...)

Merge any number of tables into a new table (a shallow merge). Sequence parts are appended in argument order; other keys are copied, with later arguments overwriting earlier ones on a collision. Not recursive — for nested merges use deepMerge.

table.merge({1, 2}, {3, 4})              --> {1, 2, 3, 4}   (sequences concatenate)
table.merge({a = 1, b = 2}, {b = 9})     --> {a = 1, b = 9} (later wins)

table.deepMerge(...)

Like merge, but where the same key holds a table on both sides, those tables are merged recursively. The result shares no tables with the inputs — everything is deep-copied — so mutating the result never touches the originals.

table.deepMerge(
    { db = { host = "localhost", port = 5432 } },
    { db = { port = 5433 } }
)  --> { db = { host = "localhost", port = 5433 } }

table.deepCopy(value)

Return a deep copy of a table, recursively copying nested tables. Metatables are not copied. Non-table values pass through unchanged.

local original = { list = {1, 2, 3} }
local copy = table.deepCopy(original)
copy.list[1] = 99
print(original.list[1])  --> 1  (unaffected)

deepMerge and deepCopy cap recursion at 100 levels and error beyond that, so a cyclic table raises rather than looping forever.

table.reverse(tbl)

Return a new array with the sequence elements in reverse order.

table.reverse({1, 2, 3})  --> {3, 2, 1}

Protecting

table.readonly(tbl [, name])

Return a read-only proxy of tbl: reads pass through to the underlying table, writes raise an error. The optional name is included in that error message. Useful for exposing a constant namespace that callers must not mutate.

local config = table.readonly({ retries = 3 }, "config")
print(config.retries)   --> 3
config.retries = 5      --> error: Attempt to update a read-only table config

Caveat: the proxy is an empty shell that forwards reads through a metatable, so pairs, next, and # see no contents — it is meant for guarding API tables you read by known key, not for iterable data.

Notes

  • Additions, not replacements. Stock table.insert, remove, concat, sort, pack, unpack are all still present.
  • Map vs. sequence is the recurring choice. Plain functions preserve keys via pairs (unspecified order); i-prefixed ones produce gapless sequences via ipairs (in order). Pick by whether you have an array or a map.
  • Most helpers return new tables. map, filter*, merge*, deepCopy, reverse, keys, values never mutate their input.
  • Type-checked. Passing a non-table where a table is expected raises a clear error naming the offending function.