Home Courses Lua Day 3
Day 03 Day 3

Day 3

Day 3

~1 hour Intermediate Hands-on Precision AI Academy

Today's Objective

Metatables let you customize how tables behave — arithmetic, comparison, indexing, and more. Build a full class system in 20 lines of Lua.

What Is a Metatable?

Every Lua table can have a metatable — another table that holds metamethods. When Lua encounters an operation on a table that it doesn't know how to handle (like addition or indexing a missing key), it looks in the metatable for a corresponding metamethod and calls it.

Key metamethods: __index (read missing keys), __newindex (write missing keys), __tostring (string conversion), __add/__sub/__mul/__div (arithmetic), __eq/__lt/__le (comparison), __len (# operator), __call (call as function).

setmetatable and __tostring

lua_-_basic_metatable.txt
LUA — BASIC METATABLE
-- Attach a metatable to a table
local vec = {x = 3, y = 4}
local mt  = {}

mt.__tostring = function(v)
  return string.format("Vec(%g, %g)", v.x, v.y)
end

setmetatable(vec, mt)

print(tostring(vec))   --> Vec(3, 4)
print(vec)             --> Vec(3, 4)  (print calls tostring)

-- getmetatable retrieves the metatable
print(getmetatable(vec) == mt)  --> true

-- You can protect a metatable from outside access
mt.__metatable = "protected"
print(getmetatable(vec))   --> protected
-- setmetatable(vec, {})   -- would now error

Arithmetic Metamethods

lua_-_operator_overloading.txt
LUA — OPERATOR OVERLOADING
local Vec2 = {}
Vec2.__index = Vec2

function Vec2.new(x, y)
  return setmetatable({x=x, y=y}, Vec2)
end

Vec2.__tostring = function(v)
  return string.format("(%g, %g)", v.x, v.y)
end

Vec2.__add = function(a, b)
  return Vec2.new(a.x + b.x, a.y + b.y)
end

Vec2.__sub = function(a, b)
  return Vec2.new(a.x - b.x, a.y - b.y)
end

Vec2.__mul = function(a, b)
  if type(a) == "number" then return Vec2.new(a*b.x, a*b.y) end
  if type(b) == "number" then return Vec2.new(a.x*b, a.y*b) end
  return a.x*b.x + a.y*b.y  -- dot product
end

Vec2.__eq = function(a, b)
  return a.x == b.x and a.y == b.y
end

Vec2.__len = function(v)
  return math.sqrt(v.x^2 + v.y^2)
end

-- Methods
function Vec2:normalize()
  local len = #self
  return Vec2.new(self.x/len, self.y/len)
end

-- Usage
local a = Vec2.new(1, 0)
local b = Vec2.new(0, 1)
local c = a + b * 3

print(tostring(c))     --> (1, 3)
print(#c)              --> 3.1623...
print(a == Vec2.new(1,0))  --> true

__index and Prototype-Based OOP

The __index metamethod is the key to OOP in Lua. When you read a key that doesn't exist in a table, Lua checks the metatable's __index. If it's a table, Lua looks there. If it's a function, Lua calls it.

lua_-_class_system.txt
LUA — CLASS SYSTEM
-- Minimal class factory (20 lines)
local function class(base)
  local cls = {}
  cls.__index = cls
  if base then
    setmetatable(cls, {__index = base})
  end
  cls.new = function(...)
    local instance = setmetatable({}, cls)
    if instance.init then instance:init(...) end
    return instance
  end
  cls.is_a = function(self, klass)
    local mt = getmetatable(self)
    while mt do
      if mt == klass then return true end
      local parent_mt = getmetatable(mt)
      mt = parent_mt and parent_mt.__index
    end
    return false
  end
  return cls
end

-- ── Define classes ────────────────────────────
local Animal = class()

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

function Animal:speak()
  return self.name .. " says " .. self.sound
end

function Animal:__tostring()
  return "Animal(" .. self.name .. ")"
end

-- ── Inheritance ───────────────────────────────
local Dog = class(Animal)

function Dog:init(name)
  Animal.init(self, name, "Woof")
  self.tricks = {}
end

function Dog:learn(trick)
  table.insert(self.tricks, trick)
end

function Dog:show_tricks()
  if #self.tricks == 0 then
    return self.name .. " knows no tricks yet"
  end
  return self.name .. " knows: " .. table.concat(self.tricks, ", ")
end

-- Override parent method
function Dog:speak()
  return Animal.speak(self) .. "!"  -- call super
end

-- ── Usage ─────────────────────────────────────
local cat = Animal.new("Whiskers", "Meow")
local dog = Dog.new("Rex")
dog:learn("sit")
dog:learn("shake")

print(cat:speak())         --> Whiskers says Meow
print(dog:speak())         --> Rex says Woof!
print(dog:show_tricks())   --> Rex knows: sit, shake
print(dog:is_a(Dog))       --> true
print(dog:is_a(Animal))    --> true
print(cat:is_a(Dog))       --> false

__newindex and Read-Only Tables

lua_-___newindex.txt
LUA — __NEWINDEX
-- Make a table read-only using __newindex + proxy
local function readonly(t)
  local proxy = {}
  local mt = {
    __index = t,
    __newindex = function(_, k, v)
      error("attempt to update read-only table key: " .. tostring(k), 2)
    end,
    __len = function() return #t end,
    __ipairs = function() return ipairs(t) end,
    __pairs  = function() return pairs(t) end,
  }
  setmetatable(proxy, mt)
  return proxy
end

local config = readonly({
  host    = "localhost",
  port    = 5432,
  timeout = 30,
})

print(config.host)     --> localhost
-- config.host = "prod"  -- error: attempt to update read-only table

-- Observe writes with __newindex (logging proxy)
local function observable(t, on_set)
  local shadow = {}
  for k, v in pairs(t) do shadow[k] = v end
  return setmetatable({}, {
    __index = shadow,
    __newindex = function(_, k, v)
      on_set(k, shadow[k], v)
      shadow[k] = v
    end,
  })
end

local state = observable({count=0}, function(k, old, new)
  print(string.format("  %s: %s -> %s", k, tostring(old), tostring(new)))
end)

state.count = 1   -->   count: 0 -> 1
state.count = 5   -->   count: 1 -> 5
Exercise
Build a Vector3 Class with Full Operator Support
  1. Create a Vec3 class with x, y, z components. Support __add, __sub, __mul (scalar and dot product), __unm (negation), and __tostring.
  2. Add a :cross(other) method that returns the cross product as a new Vec3.
  3. Add a :normalize() method and a :length() method (use __len for the latter).
  4. Create a Matrix4 class backed by a flat array of 16 numbers. Add identity, multiply (matrix × matrix and matrix × Vec3), and transpose.
  5. Test by creating a rotation matrix around the Y axis and applying it to a unit vector.

Implement a mixin function that copies methods from one or more source tables into a target class. Then create three mixins — Serializable (adds :to_json()), Cloneable (adds :clone()), and Observable (adds :on(event, fn) and :emit(event, ...)) — and apply them to your Animal class from the lesson.

What's Next

The foundations from today carry directly into Day 4. In the next session the focus shifts to Day 4 — building directly on everything covered here.

Supporting Videos & Reading

Go deeper with these external references.

Day 3 Checkpoint

Before moving on, verify you can answer these without looking:

Live Bootcamp

Learn this in person — 2 days, 5 cities

Thu–Fri sessions in Denver, Los Angeles, New York, Chicago, and Dallas. $1,490 per seat. June–October 2026.

Reserve Your Seat →
Continue To Day 4
Day 4: Love2D Game Development