Metatables
Metatables are the principal Lua metaprogramming mechanism — admit overloading operators, customising indexing, and substantial OOP. A metatable is an ordinary table (associated with another table via setmetatable) that contains metamethods — special-named functions that the runtime calls for specific operations. The principal metamethods: __index (called on missing-key access), __newindex (called on missing-key assignment), __add/__sub/__mul/__div/etc. (operator overloading), __eq/__lt/__le (comparison), __tostring (string conversion), __call (callable tables), __len (custom length), __metatable (lock/protect the metatable). The combination — setmetatable for association, the substantial metamethod surface, the __index for OOP and defaults, the operator overloading — is the substance of Lua’s metaprogramming.
Setting and getting metatables
local mt = {} -- the metatable
local t = {} -- the table
setmetatable(t, mt) -- associate
print(getmetatable(t) == mt) -- true
-- Or in one call:
local t = setmetatable({}, mt)
The setmetatable returns the table — admits substantial fluent setup.
__index — missing key access
The most important metamethod. When a key is not present in the table, Lua calls __index:
local defaults = {color = "red", size = "medium"}
local config = setmetatable({}, {__index = defaults})
print(config.color) -- "red" (from defaults)
print(config.size) -- "medium"
config.color = "blue"
print(config.color) -- "blue" (own value)
print(config.size) -- "medium" (still default)
The __index may be a table (look up there) or a function (called with the table and key):
local lazy = setmetatable({}, {
__index = function(t, key)
return "lazy: " .. key
end
})
print(lazy.foo) -- "lazy: foo"
print(lazy.anything) -- "lazy: anything"
The form admits substantial DSLs — every access produces a value.
__newindex — missing key assignment
Called when assigning to a missing key:
local readonly = setmetatable({a = 1, b = 2}, {
__newindex = function(t, key, value)
error("attempt to modify readonly table: " .. key)
end
})
readonly.a = 10 -- OK (existing key)
readonly.c = 3 -- ERROR: attempt to modify readonly table: c
The mechanism admits substantial readonly tables, validation, lazy-init, etc.:
-- Auto-initialise sub-tables:
local groups = setmetatable({}, {
__newindex = function(t, key, value)
rawset(t, key, value) -- avoid recursion
print("added group: " .. key)
end
})
groups.users = {} -- "added group: users"
The rawset(t, k, v) admits assignment bypassing __newindex — substantial for avoiding infinite recursion in the metamethod.
__index for OOP
The conventional Lua OOP pattern:
local Point = {}
Point.__index = Point -- methods accessed via __index
function Point.new(x, y)
local self = setmetatable({}, Point)
self.x = x
self.y = y
return self
end
function Point:greet() -- colon syntax: implicit self
return "I am at (" .. self.x .. ", " .. self.y .. ")"
end
local p = Point.new(1, 2)
print(p:greet()) -- "I am at (1, 2)"
The mechanism:
- The instance is
{x = 1, y = 2}with metatablePoint. - Method access (
p.greet) is missing on the instance — falls through to__index(which isPoint). - The method
Point.greetis found and called withself = p.
Treated more substantially in OOP idioms.
rawget and rawset
To bypass __index and __newindex:
local t = setmetatable({}, {
__index = function(_, k) return "from metatable" end
})
print(t.foo) -- "from metatable"
print(rawget(t, "foo")) -- nil (raw access)
rawset(t, "bar", 42) -- bypasses __newindex
The conventional uses are avoiding recursion in metamethods and checking actual presence of keys.
Arithmetic metamethods
Operator overloading via metatables:
local Vec = {}
Vec.__index = Vec
function Vec.new(x, y)
return setmetatable({x = x, y = y}, Vec)
end
Vec.__add = function(a, b)
return Vec.new(a.x + b.x, a.y + b.y)
end
Vec.__sub = function(a, b)
return Vec.new(a.x - b.x, a.y - b.y)
end
Vec.__mul = function(v, scalar)
return Vec.new(v.x * scalar, v.y * scalar)
end
Vec.__unm = function(v) -- unary minus
return Vec.new(-v.x, -v.y)
end
Vec.__eq = function(a, b)
return a.x == b.x and a.y == b.y
end
Vec.__tostring = function(v)
return "(" .. v.x .. ", " .. v.y .. ")"
end
local a = Vec.new(1, 2)
local b = Vec.new(3, 4)
print(a + b) -- "(4, 6)"
print(b - a) -- "(2, 2)"
print(a * 3) -- "(3, 6)"
print(-a) -- "(-1, -2)"
print(a == Vec.new(1, 2)) -- true
The principal arithmetic metamethods:
| Metamethod | Operator |
|---|---|
__add | + |
__sub | - (binary) |
__mul | * |
__div | / |
__idiv | // (5.3+) |
__mod | % |
__pow | ^ |
__unm | - (unary) |
__concat | .. |
__len | # |
__band, __bor, __bxor, __shl, __shr, __bnot (5.3+) | &, |, ~, <<, >>, ~ |
Comparison metamethods
local Point = setmetatable({}, {})
Point.__index = Point
function Point.new(x, y)
return setmetatable({x = x, y = y}, Point)
end
Point.__eq = function(a, b)
return a.x == b.x and a.y == b.y
end
Point.__lt = function(a, b)
return (a.x ^ 2 + a.y ^ 2) < (b.x ^ 2 + b.y ^ 2) -- by distance from origin
end
Point.__le = function(a, b)
return (a.x ^ 2 + a.y ^ 2) <= (b.x ^ 2 + b.y ^ 2)
end
local a = Point.new(1, 1)
local b = Point.new(2, 2)
print(a < b) -- true
print(a == Point.new(1, 1)) -- true
The __eq is admitted only between values of the same type with the same metatable; __lt and __le admit substantial comparison.
__tostring
Called by tostring() and print():
local Animal = {}
Animal.__index = Animal
function Animal.new(name, kind)
return setmetatable({name = name, kind = kind}, Animal)
end
Animal.__tostring = function(a)
return a.kind .. " named " .. a.name
end
local cat = Animal.new("Whiskers", "Cat")
print(cat) -- "Cat named Whiskers" (uses __tostring)
print(tostring(cat)) -- same
__call
Make a table callable like a function:
local Counter = setmetatable({}, {
__call = function(self, x)
self.count = (self.count or 0) + 1
return self.count
end
})
print(Counter()) -- 1
print(Counter()) -- 2
print(Counter()) -- 3
The mechanism admits substantial functor-style patterns.
For callable instances:
local function make_adder(n)
return setmetatable({n = n}, {
__call = function(self, x)
return self.n + x
end
})
end
local add5 = make_adder(5)
print(add5(3)) -- 8
print(add5(10)) -- 15
__len
Custom length operator:
local t = setmetatable({}, {
__len = function() return 42 end
})
print(#t) -- 42
__metatable
Protect or lock the metatable:
local t = setmetatable({}, {__metatable = "locked"})
print(getmetatable(t)) -- "locked"
setmetatable(t, {}) -- error: cannot change protected metatable
The __metatable field admits substantial protection — getmetatable returns the field value rather than the metatable; setmetatable raises.
Common patterns
Default values
local function with_defaults(defaults)
return setmetatable({}, {__index = defaults})
end
local config = with_defaults({
host = "localhost",
port = 8080,
timeout = 30
})
config.host = "example.com"
print(config.host) -- "example.com"
print(config.port) -- 8080 (default)
print(config.timeout) -- 30 (default)
Read-only proxy
local function readonly(t)
return setmetatable({}, {
__index = t,
__newindex = function(_, k)
error("readonly: cannot set " .. k)
end,
__metatable = false -- hide the metatable
})
end
local CONFIG = readonly({version = "1.0", debug = false})
print(CONFIG.version) -- "1.0"
CONFIG.debug = true -- ERROR
Lazy table
local function make_lazy(compute)
return setmetatable({}, {
__index = function(t, key)
local value = compute(key)
rawset(t, key, value) -- cache the value
return value
end
})
end
local cache = make_lazy(function(key)
print("computing " .. key)
return key:upper()
end)
print(cache.hello) -- "computing hello"; "HELLO"
print(cache.hello) -- "HELLO" (cached)
print(cache.world) -- "computing world"; "WORLD"
Auto-vivification
local function autovivify()
return setmetatable({}, {
__index = function(t, key)
local new = autovivify()
rawset(t, key, new)
return new
end
})
end
local nested = autovivify()
nested.a.b.c.d = "deep" -- creates intermediate tables automatically
Counter (callable)
local function make_counter()
return setmetatable({count = 0}, {
__call = function(self)
self.count = self.count + 1
return self.count
end
})
end
local c = make_counter()
print(c()) -- 1
print(c()) -- 2
print(c.count) -- 2
Validation on assignment
local function validated(rules)
return setmetatable({}, {
__newindex = function(t, key, value)
local validate = rules[key]
if validate and not validate(value) then
error("invalid value for " .. key)
end
rawset(t, key, value)
end
})
end
local user = validated({
age = function(v) return type(v) == "number" and v >= 0 end,
email = function(v) return type(v) == "string" and v:match("@") end
})
user.age = 30 -- OK
user.email = "alice@b.c" -- OK
user.age = -1 -- ERROR
Operator overloading for matrices
local Matrix = {}
Matrix.__index = Matrix
function Matrix.new(rows)
return setmetatable({rows = rows}, Matrix)
end
function Matrix.__add(a, b)
local result = {}
for i = 1, #a.rows do
result[i] = {}
for j = 1, #a.rows[i] do
result[i][j] = a.rows[i][j] + b.rows[i][j]
end
end
return Matrix.new(result)
end
function Matrix.__tostring(m)
local lines = {}
for _, row in ipairs(m.rows) do
lines[#lines + 1] = table.concat(row, " ")
end
return table.concat(lines, "\n")
end
local a = Matrix.new({{1, 2}, {3, 4}})
local b = Matrix.new({{5, 6}, {7, 8}})
print(a + b)
-- 6 8
-- 10 12
Class-like with __index
local Animal = {}
Animal.__index = Animal
function Animal.new(name)
return setmetatable({name = name}, Animal)
end
function Animal:greet()
return "Hello, I am " .. self.name
end
local a = Animal.new("Rex")
print(a:greet()) -- "Hello, I am Rex"
Inheritance via __index chain
local Animal = {}
Animal.__index = Animal
function Animal.new(name)
return setmetatable({name = name}, Animal)
end
function Animal:speak()
return "..."
end
-- Dog inherits from Animal:
local Dog = setmetatable({}, {__index = Animal})
Dog.__index = Dog
function Dog.new(name, breed)
local self = Animal.new(name) -- call parent constructor
self.breed = breed
return setmetatable(self, Dog)
end
function Dog:speak() -- override
return "Woof!"
end
local d = Dog.new("Rex", "Labrador")
print(d.name) -- "Rex" (from Animal)
print(d:speak()) -- "Woof!"
print(d.breed) -- "Labrador"
Treated in OOP idioms.
Memoization
local function memoize(f)
return setmetatable({}, {
__index = function(t, k)
local v = f(k)
rawset(t, k, v)
return v
end,
__call = function(t, k)
return t[k] -- triggers __index
end
})
end
local fib = memoize(function(n)
if n < 2 then return n end
return fib(n - 1) + fib(n - 2)
end)
Method missing (dynamic dispatch)
local Proxy = {}
Proxy.__index = function(t, key)
return function(self, ...)
print("calling " .. key .. " with", ...)
return t.target[key](t.target, ...)
end
end
local function wrap(target)
return setmetatable({target = target}, Proxy)
end
local p = wrap({greet = function(self, name) return "Hello, " .. name end})
print(p:greet("Alice"))
-- "calling greet with Alice"
-- "Hello, Alice"
Self-modifying via __index
local cache = setmetatable({}, {
__index = function(t, k)
print("cache miss: " .. k)
local v = expensive_compute(k)
rawset(t, k, v) -- self-cache
return v
end
})
cache.hello -- compute on first
cache.hello -- cached
A note on metatable inheritance
A table’s metatable is not inherited by sub-tables — only the fields of the metatable that the runtime consults:
local mt1 = {__add = function(a, b) return "sum" end}
local mt2 = {} -- empty
setmetatable(mt2, {__index = mt1})
local t = setmetatable({}, mt2)
print(t + t) -- ERROR: __add not on mt2 directly
-- Metamethods are looked up directly on the metatable, not through __index of the metatable
The mechanism distinguishes metatables (consulted by the runtime for specific operations) from regular tables (consulted via __index chain).
A note on the conventional discipline
The contemporary Lua metatables advice:
- Use
__index = tablefor OOP and defaults. - Use
__index = functionfor substantial computed access. - Use
__newindexfor read-only or validating tables. - Use
rawget/rawsetin metamethods to avoid recursion. - Use operator metamethods sparingly — for substantial mathematical types.
- Use
__tostringfor substantial debugging output. - Use
__callfor callable objects. - Use
__metatableto protect from external manipulation. - Cache metatables as locals — substantial performance.
- Document metamethod behaviour — admit substantial non-obvious effects.
The combination — setmetatable for association, the substantial metamethod surface, the __index for OOP and defaults, the operator overloading, the rawget/rawset for direct access — is the substance of Lua’s metaprogramming. The discipline admits substantial customisation through a small core mechanism — the conventional Lua approach to metaprogramming.