Skip to content
AgileLogics
6 min read

Object-Oriented Programming

Prerequisites: __index and __newindex

Lua has no class keyword. Classes are built from tables and metatables — a pattern so common it's worth knowing by heart.

local Account = {}
Account.__index = Account

function Account.new(owner, balance)
  local self = setmetatable({}, Account)
  self.owner = owner
  self.balance = balance or 0
  return self
end

function Account:deposit(amount)
  assert(amount > 0, "deposit must be positive")
  self.balance = self.balance + amount
end

function Account:report()
  return self.owner .. ": " .. self.balance
end

local acc = Account.new("Ada", 100)
acc:deposit(50)
print(acc:report())
print(acc.balance)

local other = Account.new("Linus")
print(other:report())
Ada: 150
150
Linus: 0

How it works:

  • Account holds the methods; Account.__index = Account means "instances look up missing keys (their methods) in Account".
  • Each instance is a small table with its own fields (owner, balance), whose metatable is Account.
  • function Account:deposit(amount) is shorthand for function Account.deposit(self, amount).
  • acc:deposit(50) is shorthand for acc.deposit(acc, 50) — the colon passes the object as self.

The colon mistake

local Counter = {}
Counter.__index = Counter
function Counter.new() return setmetatable({n = 0}, Counter) end
function Counter:increment() self.n = self.n + 1 end

local c = Counter.new()
c:increment()
print(c.n)
print(pcall(c.increment))
1
false	main.lua:4: attempt to index a nil value (local 'self')

Calling with a dot (c.increment()) passes no self, so self is nil. When you see "attempt to index a nil value (local 'self')", look for a . that should be a :.

Private state with closures

For true privacy, build objects from closures instead of metatables:

local function newWallet(start)
  local balance = start
  return {
    spend = function(amount)
      if amount > balance then return false end
      balance = balance - amount
      return true
    end,
    balance = function() return balance end,
  }
end

local w = newWallet(20)
print(w.spend(15), w.spend(10), w.balance())
true	false	5

Closure-based objects hide their state completely, but each instance carries its own copy of every method, so they use more memory than the metatable pattern.