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! ๐Ÿš€