Polyglot
Languages Lua data structures
Lua § data-structures

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-basedt[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 ipairs for sequences.
  • Use pairs for all keys.
  • Use # on sequence-style tables only.
  • Use next(t) to check empty (substantial reliability over #t == 0).
  • Avoid nil holes in array-style tables.
  • Use table.concat for substantial array-to-string.
  • Use table.sort with comparator for custom sorting.
  • Use or for 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.