Lua - Metatables
Overview
Estimated time: 40–45 minutes
Metatables are one of Lua's most powerful features, allowing you to change the behavior of tables by defining custom operations. Through metamethods, you can overload operators, customize table access, and create sophisticated data structures and object-oriented patterns.
Learning Objectives
- Understand metatables and how they modify table behavior
- Master common metamethods for arithmetic and comparison operations
- Implement custom table access patterns with __index and __newindex
- Create object-oriented programming patterns using metatables
- Apply advanced metamethod techniques for complex data structures
Prerequisites
- Strong understanding of Lua tables and their operations
- Knowledge of Lua functions and closures
- Basic understanding of object-oriented concepts
What are Metatables?
A metatable is a regular table that defines how another table behaves in certain situations. You can think of it as a "table of behaviors" that gets consulted when specific operations are performed.
-- Basic metatable example
local my_table = {value = 10}
local meta = {
__tostring = function(t)
return "MyTable with value: " .. t.value
end
}
-- Set the metatable
setmetatable(my_table, meta)
-- Now print will use our custom __tostring
print(my_table) -- Output: MyTable with value: 10
-- Check if a table has a metatable
print(getmetatable(my_table)) -- Returns the metatable
-- Remove metatable
setmetatable(my_table, nil)
print(getmetatable(my_table)) -- Returns nil
Arithmetic Metamethods
Override arithmetic operators for tables:
-- Vector class using metatables
local function Vector(x, y)
return {x = x or 0, y = y or 0}
end
local vector_meta = {
-- Addition
__add = function(v1, v2)
return Vector(v1.x + v2.x, v1.y + v2.y)
end,
-- Subtraction
__sub = function(v1, v2)
return Vector(v1.x - v2.x, v1.y - v2.y)
end,
-- Multiplication (scalar and dot product)
__mul = function(v1, v2)
if type(v2) == "number" then
-- Scalar multiplication
return Vector(v1.x * v2, v1.y * v2)
elseif type(v1) == "number" then
-- Scalar multiplication (reverse)
return Vector(v2.x * v1, v2.y * v1)
else
-- Dot product
return v1.x * v2.x + v1.y * v2.y
end
end,
-- Division
__div = function(v1, v2)
if type(v2) == "number" then
return Vector(v1.x / v2, v1.y / v2)
else
error("Cannot divide vector by vector")
end
end,
-- Unary minus
__unm = function(v)
return Vector(-v.x, -v.y)
end,
-- String representation
__tostring = function(v)
return string.format("(%g, %g)", v.x, v.y)
end,
-- Equality
__eq = function(v1, v2)
return v1.x == v2.x and v1.y == v2.y
end
}
-- Create vectors with metatable
local function create_vector(x, y)
local v = Vector(x, y)
setmetatable(v, vector_meta)
return v
end
-- Test arithmetic operations
local v1 = create_vector(3, 4)
local v2 = create_vector(1, 2)
print("v1:", v1) -- (3, 4)
print("v2:", v2) -- (1, 2)
print("v1 + v2:", v1 + v2) -- (4, 6)
print("v1 - v2:", v1 - v2) -- (2, 2)
print("v1 * 2:", v1 * 2) -- (6, 8)
print("3 * v2:", 3 * v2) -- (3, 6)
print("v1 * v2:", v1 * v2) -- 11 (dot product)
print("v1 / 2:", v1 / 2) -- (1.5, 2)
print("-v1:", -v1) -- (-3, -4)
print("v1 == v2:", v1 == v2) -- false
The __index Metamethod
Control what happens when accessing non-existent table keys:
-- __index as a function
local default_values = {
name = "Unknown",
age = 0,
city = "Nowhere"
}
local person_meta = {
__index = function(table, key)
print("Key '" .. key .. "' not found, using default")
return default_values[key]
end
}
local person = {name = "Alice"}
setmetatable(person, person_meta)
print(person.name) -- "Alice" (exists in table)
print(person.age) -- 0 (from default_values via __index)
print(person.city) -- "Nowhere" (from default_values via __index)
-- __index as a table (simpler approach)
local defaults = {x = 0, y = 0, z = 0}
local point_meta = {__index = defaults}
local point = {x = 10}
setmetatable(point, point_meta)
print(point.x) -- 10 (exists in point)
print(point.y) -- 0 (from defaults)
print(point.z) -- 0 (from defaults)
The __newindex Metamethod
Control what happens when setting new table keys:
-- Read-only table using __newindex
local function readonly(table)
local proxy = {}
local meta = {
__index = table,
__newindex = function(t, key, value)
error("Table is read-only! Cannot set key: " .. tostring(key))
end
}
setmetatable(proxy, meta)
return proxy
end
local original = {name = "John", age = 30}
local protected = readonly(original)
print(protected.name) -- "John" (works fine)
-- protected.age = 31 -- Error: Table is read-only!
-- Property validation with __newindex
local function validated_person()
local data = {}
local meta = {
__index = data,
__newindex = function(t, key, value)
if key == "age" then
if type(value) ~= "number" or value < 0 or value > 150 then
error("Age must be a number between 0 and 150")
end
elseif key == "name" then
if type(value) ~= "string" or #value == 0 then
error("Name must be a non-empty string")
end
end
-- Validation passed, store the value
data[key] = value
end
}
local proxy = {}
setmetatable(proxy, meta)
return proxy
end
local person = validated_person()
person.name = "Alice"
person.age = 25
print(person.name) -- "Alice"
print(person.age) -- 25
-- person.age = -5 -- Error: Age must be a number between 0 and 150
-- person.name = "" -- Error: Name must be a non-empty string
Advanced Metamethods
The __call Metamethod
-- Make tables callable like functions
local function Counter(initial)
local count = initial or 0
local counter = {
value = function() return count end,
reset = function() count = 0 end
}
-- Make the counter callable to increment
local meta = {
__call = function(self, increment)
increment = increment or 1
count = count + increment
return count
end,
__tostring = function(self)
return "Counter: " .. count
end
}
setmetatable(counter, meta)
return counter
end
local my_counter = Counter(5)
print(my_counter()) -- 6 (increment by 1)
print(my_counter(3)) -- 9 (increment by 3)
print(my_counter.value()) -- 9
print(my_counter) -- Counter: 9
Object-Oriented Programming with Metatables
Create classes and inheritance using metatables:
-- Base class
local function Animal(name, species)
return {
name = name or "Unknown",
species = species or "Unknown",
energy = 100
}
end
-- Animal methods
local Animal_methods = {
speak = function(self)
return self.name .. " makes a sound"
end,
eat = function(self, food)
self.energy = math.min(100, self.energy + 20)
return self.name .. " eats " .. food
end,
sleep = function(self)
self.energy = 100
return self.name .. " is sleeping"
end
}
-- Animal metatable
local Animal_meta = {
__index = Animal_methods,
__tostring = function(self)
return self.name .. " the " .. self.species .. " (Energy: " .. self.energy .. ")"
end
}
-- Animal constructor
local function create_animal(name, species)
local animal = Animal(name, species)
setmetatable(animal, Animal_meta)
return animal
end
-- Dog class (inherits from Animal)
local function Dog(name, breed)
local dog = Animal(name, "Dog")
dog.breed = breed or "Mixed"
return dog
end
-- Dog methods (extends Animal methods)
local Dog_methods = {
speak = function(self)
return self.name .. " barks: Woof! Woof!"
end,
fetch = function(self, item)
self.energy = math.max(0, self.energy - 10)
return self.name .. " fetches the " .. item
end,
-- Inherit other methods from Animal
eat = Animal_methods.eat,
sleep = Animal_methods.sleep
}
local Dog_meta = {
__index = Dog_methods,
__tostring = function(self)
return self.name .. " the " .. self.breed .. " (Energy: " .. self.energy .. ")"
end
}
local function create_dog(name, breed)
local dog = Dog(name, breed)
setmetatable(dog, Dog_meta)
return dog
end
-- Usage
local generic_animal = create_animal("Fuzzy", "Cat")
local my_dog = create_dog("Buddy", "Golden Retriever")
print(generic_animal) -- Fuzzy the Cat (Energy: 100)
print(my_dog) -- Buddy the Golden Retriever (Energy: 100)
print(generic_animal:speak()) -- Fuzzy makes a sound
print(my_dog:speak()) -- Buddy barks: Woof! Woof!
print(my_dog:fetch("ball")) -- Buddy fetches the ball
print(my_dog) -- Buddy the Golden Retriever (Energy: 90)
Common Pitfalls
- Remember that metamethods are only called when the operation isn't defined normally
- Be careful with __index and __newindex - they can create infinite loops if not handled properly
- Arithmetic metamethods should always return a new value, not modify existing ones
- Some metamethods like __gc (finalizer) have specific requirements and limitations
- Performance consideration: metamethod calls have overhead compared to direct operations
- Not all Lua environments support all metamethods (some are Lua 5.1+ only)
Checks for Understanding
- What is a metatable and how do you attach it to a table?
- Which metamethod would you implement to make a table printable with a custom format?
- What's the difference between using __index as a function vs. as a table?
- How would you create a read-only table using metamethods?
- What metamethod makes a table callable like a function?
- How do you implement custom comparison operators for tables?
Show answers
- A metatable is a table that defines custom behaviors for another table. Use setmetatable(table, metatable) to attach it.
- The __tostring metamethod - it's called when the table is converted to a string (like with print()).
- As a function: called every time with arguments (table, key), more flexible. As a table: direct lookup, simpler and faster.
- Use __newindex to throw an error when trying to set values, and __index to provide read access to the original data.
- The __call metamethod allows a table to be called like a function with parentheses.
- Implement __eq (equality), __lt (less than), and __le (less than or equal). Lua derives other comparisons from these.
Next Steps
Metatables are a powerful feature that enables advanced Lua programming patterns. With this knowledge, you can create sophisticated data structures, implement object-oriented designs, and build domain-specific languages. Next, explore coroutines for cooperative multitasking.