Web Analytics

Metatables in Lua

Intermediate ~35 min read

Metatables are one of Lua's most powerful features, allowing you to change the behavior of tables. With metatables, you can overload operators, customize table access, create object-oriented systems, and implement advanced metaprogramming patterns. Understanding metatables is key to mastering Lua. Let's explore this powerful feature!

What are Metatables?

A metatable is a table that defines how another table behaves in certain situations:

local t = {}  -- Regular table
local mt = {}  -- Metatable

setmetatable(t, mt)  -- Attach metatable to t

-- Check metatable
local meta = getmetatable(t)
print(meta == mt)  -- true
Key Functions:
  • setmetatable(table, metatable) - Set a table's metatable
  • getmetatable(table) - Get a table's metatable

The __index Metamethod

Called when accessing a non-existent key:

local defaults = {
    name = "Guest",
    age = 0
}

local person = {}

setmetatable(person, {
    __index = defaults
})

print(person.name)  -- "Guest" (from defaults)
print(person.age)   -- 0 (from defaults)

person.name = "Alice"
print(person.name)  -- "Alice" (from person)

__index as a Function

local t = {}

setmetatable(t, {
    __index = function(table, key)
        return "Key '" .. key .. "' not found"
    end
})

print(t.anything)  -- "Key 'anything' not found"
Output
Click Run to execute your code

The __newindex Metamethod

Called when setting a new key:

local t = {}
local storage = {}

setmetatable(t, {
    __newindex = function(table, key, value)
        print("Setting " .. key .. " = " .. tostring(value))
        storage[key] = value
    end,
    
    __index = storage
})

t.name = "Alice"  -- Prints: Setting name = Alice
print(t.name)     -- Alice (from storage)

Read-Only Tables

local function readOnly(t)
    local proxy = {}
    local mt = {
        __index = t,
        __newindex = function(table, key, value)
            error("Attempt to modify read-only table")
        end
    }
    setmetatable(proxy, mt)
    return proxy
end

local config = readOnly({debug = true, timeout = 30})
print(config.debug)  -- true
-- config.debug = false  -- Error!
Output
Click Run to execute your code

Arithmetic Metamethods

Overload arithmetic operators:

local Vector = {}

function Vector:new(x, y)
    local v = {x = x, y = y}
    setmetatable(v, {__index = self})
    return v
end

-- Addition
function Vector.__add(a, b)
    return Vector:new(a.x + b.x, a.y + b.y)
end

-- Subtraction
function Vector.__sub(a, b)
    return Vector:new(a.x - b.x, a.y - b.y)
end

-- Multiplication (scalar)
function Vector.__mul(a, scalar)
    return Vector:new(a.x * scalar, a.y * scalar)
end

local v1 = Vector:new(1, 2)
local v2 = Vector:new(3, 4)
local v3 = v1 + v2  -- {x = 4, y = 6}

Available Arithmetic Metamethods

Metamethod Operator Description
__add + Addition
__sub - Subtraction
__mul * Multiplication
__div / Division
__mod % Modulo
__pow ^ Exponentiation
__unm - Unary negation
Output
Click Run to execute your code

Comparison Metamethods

Overload comparison operators:

local Set = {}

function Set:new(items)
    local s = {items = items or {}}
    setmetatable(s, {
        __index = self,
        __eq = function(a, b)
            -- Check if sets are equal
            for k in pairs(a.items) do
                if not b.items[k] then return false end
            end
            for k in pairs(b.items) do
                if not a.items[k] then return false end
            end
            return true
        end,
        __lt = function(a, b)
            -- Check if a is subset of b
            for k in pairs(a.items) do
                if not b.items[k] then return false end
            end
            return true
        end
    })
    return s
end

local s1 = Set:new({a = true, b = true})
local s2 = Set:new({a = true, b = true, c = true})

print(s1 < s2)   -- true (s1 is subset of s2)
print(s1 == s2)  -- false

The __tostring Metamethod

Customize string representation:

local Person = {}

function Person:new(name, age)
    local p = {name = name, age = age}
    setmetatable(p, {
        __index = self,
        __tostring = function(self)
            return self.name .. " (" .. self.age .. " years old)"
        end
    })
    return p
end

local person = Person:new("Alice", 25)
print(person)  -- Alice (25 years old)
Output
Click Run to execute your code

The __call Metamethod

Make tables callable like functions:

local Counter = {}

function Counter:new(start)
    local c = {count = start or 0}
    setmetatable(c, {
        __index = self,
        __call = function(self)
            self.count = self.count + 1
            return self.count
        end
    })
    return c
end

local counter = Counter:new(0)
print(counter())  -- 1
print(counter())  -- 2
print(counter())  -- 3

The __len Metamethod

Customize the # operator:

local CustomArray = {}

function CustomArray:new()
    local arr = {items = {}, count = 0}
    setmetatable(arr, {
        __index = self,
        __len = function(self)
            return self.count
        end
    })
    return arr
end

function CustomArray:add(value)
    self.count = self.count + 1
    self.items[self.count] = value
end

local arr = CustomArray:new()
arr:add("a")
arr:add("b")
print(#arr)  -- 2
Output
Click Run to execute your code

Practical Examples

Class System

local Class = {}

function Class:new()
    local class = {}
    class.__index = class
    
    function class:create(...)
        local instance = setmetatable({}, self)
        if instance.init then
            instance:init(...)
        end
        return instance
    end
    
    return class
end

-- Usage
local Animal = Class:new()

function Animal:init(name)
    self.name = name
end

function Animal:speak()
    print(self.name .. " makes a sound")
end

local dog = Animal:create("Buddy")
dog:speak()  -- Buddy makes a sound

Property Tracking

local function tracked(t)
    local proxy = {}
    local mt = {
        __index = t,
        __newindex = function(table, key, value)
            print("Changed: " .. key .. " = " .. tostring(value))
            t[key] = value
        end
    }
    setmetatable(proxy, mt)
    return proxy
end

local person = tracked({name = "Alice"})
person.age = 25  -- Prints: Changed: age = 25
Output
Click Run to execute your code

Complete Metamethod Reference

Metamethod Purpose
__index Table access (read)
__newindex Table access (write)
__call Call table as function
__tostring String conversion
__len Length operator #
__add, __sub, __mul, __div Arithmetic operators
__eq, __lt, __le Comparison operators
__concat Concatenation ..
__pairs, __ipairs Custom iteration

Practice Exercise

Try these metatable challenges:

Output
Click Run to execute your code

Summary

In this lesson, you learned:

  • What metatables are and how to use them
  • __index for customizing table access
  • __newindex for controlling table modification
  • Arithmetic metamethods for operator overloading
  • Comparison metamethods for custom comparisons
  • __tostring for string representation
  • __call to make tables callable
  • Practical applications: classes, read-only tables, property tracking

What's Next?

You've mastered metatables! Next, we'll explore iterators—how to create custom iteration patterns for your tables. You'll learn about stateless and stateful iterators, and how to build powerful iteration abstractions. Let's continue! 🚀