Tables

Tables are Lua’s only data structure. They serve as arrays, dictionaries, objects, and modules.

Table Basics

Creating Tables

-- Empty table
local t = {}

-- Array-style (sequential integer keys starting at 1)
local hosts = {"ise-01", "ise-02", "ise-03"}

-- Dictionary-style (string keys)
local config = {
    hostname = "ise-01",
    port = 443,
    verify_ssl = true
}

-- Mixed (avoid when possible)
local mixed = {
    "first",           -- [1] = "first"
    "second",          -- [2] = "second"
    name = "mixed",    -- ["name"] = "mixed"
}

-- Explicit keys
local explicit = {
    [1] = "one",
    [2] = "two",
    ["key"] = "value",
    [{}] = "table as key"  -- Any value can be a key
}

Accessing Elements

local hosts = {"ise-01", "ise-02", "ise-03"}
local config = {hostname = "ise-01", port = 443}

-- Array access (1-indexed!)
print(hosts[1])  -- "ise-01"
print(hosts[2])  -- "ise-02"

-- Dictionary access
print(config["hostname"])  -- "ise-01"
print(config.hostname)     -- "ise-01" (syntactic sugar)

-- Non-existent keys return nil
print(hosts[10])      -- nil
print(config.missing) -- nil

Modifying Tables

local hosts = {"ise-01", "ise-02"}
local config = {hostname = "ise-01"}

-- Add/update element
hosts[3] = "ise-03"
config.port = 443
config["verify_ssl"] = true

-- Remove element
hosts[2] = nil        -- Creates hole in array!
config.port = nil     -- Removes key

-- Array manipulation (use table functions)
table.insert(hosts, "ise-04")        -- Append
table.insert(hosts, 1, "ise-00")     -- Insert at position 1
table.remove(hosts, 2)               -- Remove at position 2
table.remove(hosts)                  -- Remove last

-- Get length (array part only)
print(#hosts)  -- Counts sequential integer keys from 1

Arrays

Working with Arrays

local endpoints = {}

-- Build array
for i = 1, 5 do
    endpoints[i] = "endpoint-" .. i
end

-- Append
table.insert(endpoints, "endpoint-6")
endpoints[#endpoints + 1] = "endpoint-7"  -- Same effect

-- Iterate (ipairs for arrays)
for i, ep in ipairs(endpoints) do
    print(i, ep)
end

-- Length
print(#endpoints)

-- Check if empty
if #endpoints == 0 then
    print("No endpoints")
end

-- Last element
print(endpoints[#endpoints])

Array Operations

-- Sort
local hosts = {"ise-03", "ise-01", "ise-02"}
table.sort(hosts)
-- hosts = {"ise-01", "ise-02", "ise-03"}

-- Sort with custom comparator
table.sort(hosts, function(a, b)
    return a > b  -- Descending
end)

-- Reverse (no built-in, manual)
local function reverse(t)
    local n = #t
    for i = 1, n // 2 do
        t[i], t[n - i + 1] = t[n - i + 1], t[i]
    end
end

-- Concatenate to string
local str = table.concat(hosts, ", ")  -- "ise-01, ise-02, ise-03"
local str = table.concat(hosts, "\n")  -- Newline separated

-- Slice (no built-in, manual)
local function slice(t, start, stop)
    local result = {}
    for i = start, stop or #t do
        result[#result + 1] = t[i]
    end
    return result
end

Common Array Patterns

-- Filter
local function filter(t, predicate)
    local result = {}
    for _, v in ipairs(t) do
        if predicate(v) then
            result[#result + 1] = v
        end
    end
    return result
end

local active = filter(hosts, function(h)
    return string.match(h, "^ise")
end)

-- Map
local function map(t, fn)
    local result = {}
    for i, v in ipairs(t) do
        result[i] = fn(v)
    end
    return result
end

local upper_hosts = map(hosts, string.upper)

-- Reduce
local function reduce(t, fn, initial)
    local acc = initial
    for _, v in ipairs(t) do
        acc = fn(acc, v)
    end
    return acc
end

local total = reduce({1, 2, 3, 4}, function(acc, v)
    return acc + v
end, 0)  -- 10

-- Find
local function find(t, predicate)
    for i, v in ipairs(t) do
        if predicate(v) then
            return v, i
        end
    end
    return nil
end

local ise_host = find(hosts, function(h)
    return string.match(h, "ise")
end)

-- Contains
local function contains(t, value)
    for _, v in ipairs(t) do
        if v == value then return true end
    end
    return false
end

Dictionaries

Working with Dictionaries

local config = {
    hostname = "ise-01",
    port = 443,
    verify_ssl = true
}

-- Add/update
config.timeout = 30
config["max_retries"] = 3

-- Iterate (pairs for dictionaries)
for key, value in pairs(config) do
    print(key .. " = " .. tostring(value))
end

-- Check key exists
if config.timeout then
    print("Timeout is set")
end

if config.missing == nil then
    print("Key doesn't exist")
end

-- Get with default
local timeout = config.timeout or 30

-- Keys and values
local function keys(t)
    local result = {}
    for k in pairs(t) do
        result[#result + 1] = k
    end
    return result
end

local function values(t)
    local result = {}
    for _, v in pairs(t) do
        result[#result + 1] = v
    end
    return result
end

Dictionary Operations

-- Merge (shallow)
local function merge(t1, t2)
    local result = {}
    for k, v in pairs(t1) do result[k] = v end
    for k, v in pairs(t2) do result[k] = v end
    return result
end

local defaults = {timeout = 30, retries = 3}
local user_config = {timeout = 60}
local config = merge(defaults, user_config)
-- {timeout = 60, retries = 3}

-- Deep merge
local function deep_merge(t1, t2)
    local result = {}
    for k, v in pairs(t1) do
        if type(v) == "table" then
            result[k] = deep_merge(v, {})
        else
            result[k] = v
        end
    end
    for k, v in pairs(t2) do
        if type(v) == "table" and type(result[k]) == "table" then
            result[k] = deep_merge(result[k], v)
        else
            result[k] = v
        end
    end
    return result
end

-- Pick specific keys
local function pick(t, keys)
    local result = {}
    for _, k in ipairs(keys) do
        result[k] = t[k]
    end
    return result
end

local subset = pick(config, {"hostname", "port"})

Nested Tables

Creating Nested Structures

-- ISE deployment structure
local deployment = {
    nodes = {
        {
            hostname = "ise-01",
            ip = "10.50.1.20",
            roles = {"PAN", "MNT"},
            status = "running"
        },
        {
            hostname = "ise-02",
            ip = "10.50.1.21",
            roles = {"PSN"},
            status = "running"
        }
    },
    version = "3.2",
    patches = {"patch1", "patch2", "patch3"}
}

-- Access nested
print(deployment.nodes[1].hostname)     -- "ise-01"
print(deployment.nodes[1].roles[1])     -- "PAN"

-- Safe nested access
local function get_nested(t, ...)
    local current = t
    for _, key in ipairs({...}) do
        if type(current) ~= "table" then
            return nil
        end
        current = current[key]
    end
    return current
end

local ip = get_nested(deployment, "nodes", 1, "ip")  -- "10.50.1.20"
local missing = get_nested(deployment, "nodes", 99, "ip")  -- nil

Modifying Nested Tables

-- Add node
table.insert(deployment.nodes, {
    hostname = "ise-03",
    ip = "10.50.1.22",
    roles = {"PSN"},
    status = "pending"
})

-- Update nested value
deployment.nodes[1].status = "maintenance"

-- Safe set nested
local function set_nested(t, value, ...)
    local keys = {...}
    local current = t
    for i = 1, #keys - 1 do
        local key = keys[i]
        if current[key] == nil then
            current[key] = {}
        end
        current = current[key]
    end
    current[keys[#keys]] = value
end

set_nested(deployment, "patched", "nodes", 1, "patch_status")

Table as Object

-- Simple object pattern
local Endpoint = {}

function Endpoint.new(mac, ip, vlan)
    return {
        mac = mac,
        ip = ip,
        vlan = vlan
    }
end

function Endpoint.is_active(ep)
    return ep.status == "active"
end

local ep = Endpoint.new("00:11:22:33:44:55", "10.50.10.100", 10)
ep.status = "active"
print(Endpoint.is_active(ep))  -- true

-- Method syntax (with metatables, covered later)
-- ep:is_active()

Reference vs Copy

-- Tables are passed by reference
local original = {a = 1, b = 2}
local reference = original

reference.a = 99
print(original.a)  -- 99 (modified!)

-- Shallow copy
local function shallow_copy(t)
    local copy = {}
    for k, v in pairs(t) do
        copy[k] = v
    end
    return copy
end

local copy = shallow_copy(original)
copy.a = 1
print(original.a)  -- 99 (unchanged)

-- Deep copy
local function deep_copy(t)
    if type(t) ~= "table" then return t end
    local copy = {}
    for k, v in pairs(t) do
        copy[k] = deep_copy(v)
    end
    return copy
end

local nested = {config = {port = 443}}
local deep = deep_copy(nested)
deep.config.port = 9060
print(nested.config.port)  -- 443 (unchanged)

Neovim Table Utilities

-- vim.tbl_* functions

-- Deep extend (merge)
local result = vim.tbl_deep_extend("force",
    {a = 1, nested = {x = 1}},
    {b = 2, nested = {y = 2}}
)
-- {a = 1, b = 2, nested = {x = 1, y = 2}}

-- Check if table is list (array)
vim.tbl_islist({1, 2, 3})      -- true
vim.tbl_islist({a = 1})        -- false

-- Check if empty
vim.tbl_isempty({})            -- true

-- Count entries
vim.tbl_count({a = 1, b = 2})  -- 2

-- Filter
local filtered = vim.tbl_filter(function(v)
    return v > 2
end, {1, 2, 3, 4})  -- {3, 4}

-- Map
local mapped = vim.tbl_map(function(v)
    return v * 2
end, {1, 2, 3})  -- {2, 4, 6}

-- Keys
local keys = vim.tbl_keys({a = 1, b = 2})  -- {"a", "b"}

-- Values
local values = vim.tbl_values({a = 1, b = 2})  -- {1, 2}

-- Contains value
vim.tbl_contains({1, 2, 3}, 2)  -- true

Next Module

Functions - Definitions, closures, variadic functions.