Tables
The table is Lua’s single composite type — admit array, hash, record, object, module, namespace, set. Tables admit integer keys (the conventional array form) and arbitrary keys (string, number, boolean, function, table — but not nil). Indexing is 1-based — t[1] is the first element. The # length operator returns the border of a sequence-style table; iteration via pairs (all keys) or ipairs (sequence). The combination — universal table type, 1-based indexing, the pairs/ipairs distinction, the array/hash dual nature, the metatable extensibility — is the substance of Lua’s data-structure surface.
Table literals
local empty = {} -- empty table
-- Array-style (1-indexed sequences):
local arr = {10, 20, 30} -- arr[1]=10, arr[2]=20, arr[3]=30
-- Hash-style (string keys):
local map = {name = "Alice", age = 30} -- map.name, map.age
local map = {["name"] = "Alice", ["age"] = 30} -- equivalent verbose form
-- Mixed:
local mixed = {10, 20, 30, name = "list", count = 3}
-- mixed[1]=10, mixed[2]=20, mixed[3]=30, mixed.name="list", mixed.count=3
-- Computed keys:
local key = "name"
local t = {[key] = "Alice"} -- t.name = "Alice"
-- Nested:
local config = {
server = {host = "localhost", port = 8080},
features = {"auth", "logging", "cache"}
}
The , (comma) is the standard separator; ; (semicolon) is admitted but rare.
Indexing
Two equivalent forms for string keys:
local t = {name = "Alice"}
print(t.name) -- "Alice" (dot notation)
print(t["name"]) -- "Alice" (bracket notation)
The dot notation requires the key be a valid identifier; brackets admit any expression:
local key = "name"
print(t[key]) -- "Alice"
print(t["with spaces"]) -- requires brackets
local nums = {10, 20, 30}
print(nums[1]) -- 10 (1-based!)
print(nums[#nums]) -- 30 (last)
For missing keys, nil is returned:
print(t.unknown) -- nil
print(t[42]) -- nil
1-based indexing
A distinctive Lua feature: tables use 1-based indexing for arrays:
local arr = {"a", "b", "c"}
print(arr[1]) -- "a"
print(arr[2]) -- "b"
print(arr[3]) -- "c"
print(arr[0]) -- nil
print(arr[#arr]) -- "c" (last)
Standard library functions (table.insert, table.remove, string.sub, etc.) use 1-based indexing.
Length operator #
The unary # returns the border of a sequence-style table:
print(#{10, 20, 30}) -- 3
print(#{}) -- 0
print(#{10, 20, 30, name = "x"}) -- 3 (only sequence)
-- Pitfall: # is unreliable with nil holes:
print(#{1, 2, nil, 4}) -- 2 OR 4 (implementation-defined)
The # is well-defined for sequences — tables with consecutive integer keys 1 through n, no nil holes. For map-style tables, # returns 0 (no integer keys):
local map = {name = "Alice", age = 30}
print(#map) -- 0
-- For map size:
local count = 0
for _ in pairs(map) do count = count + 1 end
print(count) -- 2
Adding and removing
For arrays:
local arr = {10, 20, 30}
table.insert(arr, 40) -- append: {10, 20, 30, 40}
table.insert(arr, 1, 0) -- insert at index 1: {0, 10, 20, 30, 40}
table.remove(arr) -- remove last: {0, 10, 20, 30}
table.remove(arr, 1) -- remove at index 1: {10, 20, 30}
-- Or directly:
arr[#arr + 1] = 40 -- append (shorthand)
For maps:
local map = {name = "Alice", age = 30}
map.email = "alice@b.c" -- add
map["status"] = "active" -- add via brackets
map.age = nil -- remove
Iteration
ipairs — sequence iteration
local arr = {10, 20, 30}
for i, v in ipairs(arr) do
print(i, v)
end
-- 1, 10
-- 2, 20
-- 3, 30
The ipairs iterates from 1 until the first nil — admit substantial sequence iteration. Stops at first nil.
pairs — all keys
local mixed = {10, 20, name = "list", count = 3}
for k, v in pairs(mixed) do
print(k, v)
end
-- order is undefined; one possible output:
-- 1, 10
-- 2, 20
-- name, list
-- count, 3
The pairs iterates all keys (integer and non-integer) — order is undefined.
Numeric for
For sequences, the numeric for is conventional:
for i = 1, #arr do
print(arr[i])
end
Common patterns
Array operations
local arr = {1, 2, 3, 4, 5}
-- Map (transform):
local doubled = {}
for i, v in ipairs(arr) do
doubled[i] = v * 2
end
-- Filter:
local evens = {}
for _, v in ipairs(arr) do
if v % 2 == 0 then
evens[#evens + 1] = v
end
end
-- Reduce:
local sum = 0
for _, v in ipairs(arr) do
sum = sum + v
end
print(sum) -- 15
-- Find:
local function find(t, target)
for i, v in ipairs(t) do
if v == target then return i, v end
end
return nil
end
print(find({10, 20, 30}, 20)) -- 2, 20
-- Concatenate:
local s = table.concat(arr, ", ") -- "1, 2, 3, 4, 5"
-- Sort:
table.sort(arr)
table.sort(arr, function(a, b) return a > b end) -- descending
-- Reverse:
local function reverse(t)
local result = {}
for i = #t, 1, -1 do
result[#result + 1] = t[i]
end
return result
end
Hash operations
local map = {a = 1, b = 2, c = 3}
-- Keys:
local keys = {}
for k in pairs(map) do
keys[#keys + 1] = k
end
-- Values:
local values = {}
for _, v in pairs(map) do
values[#values + 1] = v
end
-- Filter:
local positive = {}
for k, v in pairs(map) do
if v > 0 then
positive[k] = v
end
end
-- Map values:
local doubled = {}
for k, v in pairs(map) do
doubled[k] = v * 2
end
-- Has key:
local function has_key(t, key)
return t[key] ~= nil
end
Set via table
Lua does not include a Set type; the conventional substitute uses true values:
local set = {}
set["apple"] = true
set["banana"] = true
if set["apple"] then print("yes") end
-- Add many:
local function set_from(arr)
local s = {}
for _, v in ipairs(arr) do
s[v] = true
end
return s
end
local fruits = set_from({"apple", "banana", "cherry"})
-- Set operations:
local function union(a, b)
local result = {}
for k in pairs(a) do result[k] = true end
for k in pairs(b) do result[k] = true end
return result
end
Stack
local stack = {}
-- Push:
stack[#stack + 1] = item
-- Pop:
local item = stack[#stack]
stack[#stack] = nil
-- Peek:
local item = stack[#stack]
Queue
For substantial queues, a simple table-based queue admits efficient operations using two pointers:
local Queue = {}
Queue.__index = Queue
function Queue.new()
return setmetatable({first = 1, last = 0}, Queue)
end
function Queue:push(item)
self.last = self.last + 1
self[self.last] = item
end
function Queue:pop()
if self.first > self.last then return nil end
local item = self[self.first]
self[self.first] = nil
self.first = self.first + 1
return item
end
function Queue:empty()
return self.first > self.last
end
local q = Queue.new()
q:push("a")
q:push("b")
print(q:pop()) -- "a"
Deep copy
Lua does not include a deep-copy function; the conventional implementation:
local function deep_copy(t)
if type(t) ~= "table" then return t end
local result = {}
for k, v in pairs(t) do
result[deep_copy(k)] = deep_copy(v)
end
return result
end
For substantial cycle handling, the conventional defence is a seen table:
local function deep_copy(t, seen)
seen = seen or {}
if type(t) ~= "table" then return t end
if seen[t] then return seen[t] end -- handle cycle
local result = {}
seen[t] = result
for k, v in pairs(t) do
result[deep_copy(k, seen)] = deep_copy(v, seen)
end
return result
end
Counting
local function tally(items)
local counts = {}
for _, item in ipairs(items) do
counts[item] = (counts[item] or 0) + 1
end
return counts
end
local words = {"a", "b", "a", "c", "b", "a"}
local counts = tally(words)
-- counts.a = 3, counts.b = 2, counts.c = 1
Group-by
local function group_by(items, key_fn)
local groups = {}
for _, item in ipairs(items) do
local key = key_fn(item)
groups[key] = groups[key] or {}
table.insert(groups[key], item)
end
return groups
end
local people = {
{name = "Alice", age = 30},
{name = "Bob", age = 25},
{name = "Charlie", age = 30}
}
local by_age = group_by(people, function(p) return p.age end)
-- by_age[30] = {Alice, Charlie}
-- by_age[25] = {Bob}
Default values via or
local options = options or {}
local timeout = options.timeout or 30
local retries = options.retries or 3
-- Beware false:
local debug = options.debug
if debug == nil then debug = true end -- explicit, since false is valid
Sparse arrays
For arrays with nil holes, pairs is conventionally safer than ipairs:
local sparse = {}
sparse[1] = "a"
sparse[3] = "c"
sparse[10] = "j"
for i, v in pairs(sparse) do
print(i, v) -- visits {1, 3, 10}
end
-- ipairs would stop at the first nil (after 1):
for i, v in ipairs(sparse) do
print(i, v) -- only 1, "a"
end
The conventional discipline is to avoid sparse arrays — use pairs if substantial.
Building from key-value pairs
local function from_pairs(pairs_array)
local result = {}
for _, pair in ipairs(pairs_array) do
result[pair[1]] = pair[2]
end
return result
end
local map = from_pairs({{"a", 1}, {"b", 2}})
-- map = {a = 1, b = 2}
Slicing an array
local function slice(t, start, stop)
start = start or 1
stop = stop or #t
local result = {}
for i = start, stop do
result[#result + 1] = t[i]
end
return result
end
local arr = {10, 20, 30, 40, 50}
slice(arr, 2, 4) -- {20, 30, 40}
slice(arr, 3) -- {30, 40, 50}
Concatenating arrays
local function concat_arrays(...)
local result = {}
for _, arr in ipairs({...}) do
for _, v in ipairs(arr) do
result[#result + 1] = v
end
end
return result
end
concat_arrays({1, 2}, {3, 4}, {5}) -- {1, 2, 3, 4, 5}
Removing duplicates
local function unique(arr)
local seen = {}
local result = {}
for _, v in ipairs(arr) do
if not seen[v] then
seen[v] = true
result[#result + 1] = v
end
end
return result
end
unique({1, 2, 2, 3, 3, 3, 4}) -- {1, 2, 3, 4}
next for iteration step
The next(t, k) returns the key/value following k (or the first if k is nil):
local k, v = next(t) -- first entry
local k, v = next(t, k) -- next entry
-- Check empty:
if next(t) == nil then
print("empty table")
end
The next is the foundation of pairs — admit substantial low-level table iteration.
Composite keys
-- String composite keys:
local cache = {}
cache["key1:key2"] = value
-- Or via separate tables:
local cache = {}
cache[a] = cache[a] or {}
cache[a][b] = value
-- Or via tostring of table-as-key:
local cache = {}
local key = {} -- table identity is unique
cache[key] = value
The mechanism admits substantial flexible keying.
A note on iteration order
The order of pairs iteration is undefined — varies between runs and Lua versions:
local t = {a = 1, b = 2, c = 3}
for k, v in pairs(t) do
print(k, v) -- order is unspecified
end
For deterministic iteration, sort the keys first:
local keys = {}
for k in pairs(t) do
keys[#keys + 1] = k
end
table.sort(keys)
for _, k in ipairs(keys) do
print(k, t[k])
end
A note on the conventional discipline
The contemporary Lua tables advice:
- Use tables for everything composite — there is no other option.
- Use array-style (1-indexed) for sequences.
- Use hash-style (string keys) for records.
- Avoid mixing — array-style and hash-style in the same table is admitted but conventionally separated.
- Use
ipairsfor sequences. - Use
pairsfor all keys. - Use
#on sequence-style tables only. - Use
next(t)to check empty (substantial reliability over#t == 0). - Avoid
nilholes in array-style tables. - Use
table.concatfor substantial array-to-string. - Use
table.sortwith comparator for custom sorting. - Use
orfor default values. - Sort keys for deterministic iteration on map-style.
The combination — universal table type, 1-based array indexing, the pairs/ipairs distinction, the # border operator, the substantial table.* library, the metatable extensibility — is the substance of Lua’s data-structure surface. The discipline produces concise, expressive code with substantial flexibility — a single composite type covers all the conventional cases.