Lua - Tables

Overview

Estimated time: 30–40 minutes

Tables are Lua's only data structure, but they're incredibly versatile. They can serve as arrays, dictionaries, objects, sets, and more. This tutorial covers all aspects of table usage in Lua.

Learning Objectives

  • Understand table creation and initialization
  • Master array-style and dictionary-style table usage
  • Learn table traversal with pairs and ipairs
  • Explore advanced table operations and the table library
  • Apply tables for object-oriented programming patterns

Prerequisites

  • Understanding of Lua variables and data types
  • Basic knowledge of functions

Table Creation

Empty Tables

-- Creating empty tables
local empty_table = {}
local another_empty = {}

print("Type of empty_table:", type(empty_table))
print("Length of empty table:", #empty_table)

-- Tables are reference types
local ref1 = empty_table
local ref2 = empty_table

print("ref1 == ref2:", ref1 == ref2)      -- true, same reference
print("empty_table == {}:", empty_table == {})  -- false, different tables

Expected Output:

Type of empty_table:	table
Length of empty table:	0
ref1 == ref2:	true
empty_table == {}:	false

Table Literals

-- Array-style initialization
local fruits = {"apple", "banana", "orange", "grape"}
local numbers = {10, 20, 30, 40, 50}
local mixed = {1, "hello", true, 3.14}

print("First fruit:", fruits[1])  -- Lua arrays are 1-indexed!
print("Second number:", numbers[2])
print("Mixed table:", mixed[1], mixed[2], mixed[3], mixed[4])

-- Dictionary-style initialization
local person = {
    name = "Alice",
    age = 30,
    city = "New York",
    is_student = false
}

print("Person name:", person.name)
print("Person age:", person["age"])  -- Alternative syntax

Expected Output:

First fruit:	apple
Second number:	20
Mixed table:	1	hello	true	3.14
Person name:	Alice
Person age:	30

Array-Style Tables

Creating and Accessing Arrays

local colors = {"red", "green", "blue", "yellow"}

-- Accessing elements (1-indexed)
print("First color:", colors[1])
print("Last color:", colors[#colors])  -- # gives length

-- Adding elements
colors[5] = "purple"
colors[#colors + 1] = "orange"  -- Append to end

print("Array length:", #colors)
for i = 1, #colors do
    print("Color " .. i .. ":", colors[i])
end

Expected Output:

First color:	red
Last color:	yellow
Array length:	6
Color 1:	red
Color 2:	green
Color 3:	blue
Color 4:	yellow
Color 5:	purple
Color 6:	orange

Multi-dimensional Arrays

-- 2D array (matrix)
local matrix = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
}

-- Accessing 2D array elements
print("Element at [2][3]:", matrix[2][3])  -- 6

-- Creating a 3x3 matrix dynamically
local grid = {}
for i = 1, 3 do
    grid[i] = {}
    for j = 1, 3 do
        grid[i][j] = i * 10 + j
    end
end

-- Print the grid
for i = 1, 3 do
    local row = ""
    for j = 1, 3 do
        row = row .. grid[i][j] .. " "
    end
    print("Row " .. i .. ":", row)
end

Expected Output:

Element at [2][3]:	6
Row 1:	11 12 13 
Row 2:	21 22 23 
Row 3:	31 32 33 

Dictionary-Style Tables

Key-Value Pairs

local student = {
    name = "John Doe",
    age = 20,
    grade = "A",
    subjects = {"Math", "Physics", "Chemistry"}
}

-- Different ways to access values
print("Name (dot notation):", student.name)
print("Age (bracket notation):", student["age"])

-- Adding new key-value pairs
student.gpa = 3.8
student["graduation_year"] = 2024

-- Keys can be variables
local key = "grade"
print("Grade using variable key:", student[key])

-- Keys can be any type (except nil)
local complex_table = {
    [1] = "numeric key",
    ["string_key"] = "string value",
    [true] = "boolean key",
    [{nested = "table"}] = "table key"  -- Not recommended
}

print("Numeric key:", complex_table[1])
print("String key:", complex_table.string_key)
print("Boolean key:", complex_table[true])

Expected Output:

Name (dot notation):	John Doe
Age (bracket notation):	20
Grade using variable key:	A
Numeric key:	numeric key
String key:	string value
Boolean key:	boolean key

Table Traversal

Using pairs() for All Elements

local inventory = {
    sword = 1,
    shield = 1,
    potion = 5,
    gold = 100,
    [1] = "first item",
    [2] = "second item"
}

print("=== Using pairs() ===")
for key, value in pairs(inventory) do
    print(key .. ":", value)
end

Expected Output:

=== Using pairs() ===
1:	first item
2:	second item
sword:	1
shield:	1
potion:	5
gold:	100

Using ipairs() for Arrays

local animals = {"cat", "dog", "bird", "fish"}

print("=== Using ipairs() ===")
for index, animal in ipairs(animals) do
    print("Animal " .. index .. ":", animal)
end

-- ipairs stops at the first nil
local sparse_array = {"a", "b", nil, "d"}
print("\n=== Sparse array with ipairs() ===")
for i, v in ipairs(sparse_array) do
    print(i, v)  -- Only prints 1: a, 2: b
end

print("\n=== Sparse array with numeric for loop ===")
for i = 1, 4 do
    print(i, sparse_array[i])  -- Shows all including nil
end

Expected Output:

=== Using ipairs() ===
Animal 1:	cat
Animal 2:	dog
Animal 3:	bird
Animal 4:	fish

=== Sparse array with ipairs() ===
1	a
2	b

=== Sparse array with numeric for loop ===
1	a
2	b
3	nil
4	d

Table Operations

Table Library Functions

local fruits = {"apple", "banana"}

-- table.insert - add elements
table.insert(fruits, "orange")           -- Append to end
table.insert(fruits, 2, "grape")         -- Insert at position 2

print("After insertions:")
for i, fruit in ipairs(fruits) do
    print(i, fruit)
end

-- table.remove - remove elements
local removed = table.remove(fruits, 2)  -- Remove at position 2
print("Removed fruit:", removed)

local last = table.remove(fruits)        -- Remove last element
print("Last fruit:", last)

print("After removals:")
for i, fruit in ipairs(fruits) do
    print(i, fruit)
end

-- table.concat - join array elements
local numbers = {1, 2, 3, 4, 5}
print("Joined numbers:", table.concat(numbers, "-"))
print("Partial join:", table.concat(numbers, ", ", 2, 4))  -- Elements 2-4

Expected Output:

After insertions:
1	apple
2	grape
3	banana
4	orange
Removed fruit:	grape
Last fruit:	orange
After removals:
1	apple
2	banana
Joined numbers:	1-2-3-4-5
Partial join:	2, 3, 4

Table Sorting

-- Sorting numeric arrays
local numbers = {5, 2, 8, 1, 9, 3}
table.sort(numbers)
print("Sorted numbers:", table.concat(numbers, ", "))

-- Sorting strings
local names = {"Charlie", "Alice", "Bob", "David"}
table.sort(names)
print("Sorted names:", table.concat(names, ", "))

-- Custom sort function
local students = {
    {name = "Alice", grade = 85},
    {name = "Bob", grade = 92},
    {name = "Charlie", grade = 78}
}

-- Sort by grade (descending)
table.sort(students, function(a, b)
    return a.grade > b.grade
end)

print("Students sorted by grade:")
for i, student in ipairs(students) do
    print(student.name .. ":", student.grade)
end

Expected Output:

Sorted numbers:	1, 2, 3, 5, 8, 9
Sorted names:	Alice, Bob, Charlie, David
Students sorted by grade:
Bob:	92
Alice:	85
Charlie:	78

Tables as Objects

Simple Object Pattern

-- Creating objects with tables
local function create_person(name, age)
    local person = {
        name = name,
        age = age,
        
        -- Methods
        greet = function(self)
            return "Hello, I'm " .. self.name
        end,
        
        get_age = function(self)
            return self.age
        end,
        
        have_birthday = function(self)
            self.age = self.age + 1
            print(self.name .. " is now " .. self.age .. " years old")
        end
    }
    return person
end

-- Using the object
local alice = create_person("Alice", 25)
print(alice:greet())              -- Using colon notation
print("Age:", alice:get_age())
alice:have_birthday()

local bob = create_person("Bob", 30)
print(bob:greet())

Expected Output:

Hello, I'm Alice
Age:	25
Alice is now 26 years old
Hello, I'm Bob

Class-like Pattern

-- Class-like pattern with shared methods
local Rectangle = {}
Rectangle.__index = Rectangle

function Rectangle.new(width, height)
    local rect = {
        width = width,
        height = height
    }
    setmetatable(rect, Rectangle)
    return rect
end

function Rectangle:area()
    return self.width * self.height
end

function Rectangle:perimeter()
    return 2 * (self.width + self.height)
end

function Rectangle:scale(factor)
    self.width = self.width * factor
    self.height = self.height * factor
end

-- Using the Rectangle class
local rect1 = Rectangle.new(5, 3)
print("Rectangle area:", rect1:area())
print("Rectangle perimeter:", rect1:perimeter())

rect1:scale(2)
print("After scaling - area:", rect1:area())

Expected Output:

Rectangle area:	15
Rectangle perimeter:	16
After scaling - area:	60

Advanced Table Techniques

Table Copying

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

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

local original = {
    name = "Alice",
    scores = {math = 95, english = 87}
}

local shallow = shallow_copy(original)
local deep = deep_copy(original)

-- Modify nested table
original.scores.math = 100

print("Original math score:", original.scores.math)
print("Shallow copy math score:", shallow.scores.math)  -- Also changed!
print("Deep copy math score:", deep.scores.math)       -- Unchanged

Expected Output:

Original math score:	100
Shallow copy math score:	100
Deep copy math score:	95

Table as Set

-- Using tables as sets
local function create_set(list)
    local set = {}
    for _, item in ipairs(list) do
        set[item] = true
    end
    return set
end

local function has_item(set, item)
    return set[item] == true
end

local function set_union(set1, set2)
    local result = {}
    for k in pairs(set1) do result[k] = true end
    for k in pairs(set2) do result[k] = true end
    return result
end

local fruits_set = create_set({"apple", "banana", "orange"})
local colors_set = create_set({"red", "green", "blue", "orange"})

print("Has apple:", has_item(fruits_set, "apple"))
print("Has red:", has_item(fruits_set, "red"))

local combined = set_union(fruits_set, colors_set)
print("Combined set items:")
for item in pairs(combined) do
    print("-", item)
end

Expected Output:

Has apple:	true
Has red:	false
Combined set items:
- orange
- apple
- banana
- red
- green
- blue

Common Pitfalls

  • 1-indexed arrays: Lua arrays start at index 1, not 0
  • Length operator: # only works correctly with array-like tables
  • Reference vs value: Tables are passed by reference, not copied
  • nil values: Setting a table element to nil removes it from the table
  • Sparse arrays: Arrays with nil values can cause unexpected behavior

Checks for Understanding

  1. What index does the first element of a Lua array have?
  2. What's the difference between pairs() and ipairs()?
  3. How do you get the length of an array-style table?
  4. What happens when you assign nil to a table element?
  5. How do you add an element to the end of an array?
Show answers
  1. Index 1 - Lua arrays are 1-indexed, not 0-indexed
  2. pairs() iterates over all key-value pairs; ipairs() only iterates over consecutive integer keys starting from 1
  3. Use the length operator #table_name
  4. The element is removed from the table completely
  5. Use table.insert(array, value) or array[#array + 1] = value

Next Steps

Tables are fundamental to Lua programming. Now that you understand how they work, you're ready to explore arrays in more detail and learn about iterators for advanced table processing.