Skip to content
Objects: the colon, self, and the class pattern
step 1/5

Reading — step 1 of 5

Learn

~4 min readTables — The One Data Structure

Objects: the colon, self, and the class pattern

Metatables and __index finished on a line it did not explain: P.__index = P. That assignment is how every Lua class is built, and the colon you have been typing since String Basicss:upper() — is the other half of the trick. Lua has no class keyword. It has these two things, and they are enough.

The colon passes one extra argument

t:m(x) means t.m(t, x). The table on the left is handed to the function as its first argument, and that is the whole feature.

local counter = {n = 0}

function counter.bump(self, by)
    self.n = self.n + by
end

counter.bump(counter, 5)   -- spelled out
counter:bump(3)            -- the colon passes counter for you
print(counter.n)

--> 8

The definition side has the matching shorthand. function counter:bump(by) silently declares a parameter named self ahead of the ones you wrote, so it is the same function as the function counter.bump(self, by) above — one spells self out, the other does not.

self is not a keyword either. Name that first parameter this and everything still works; self is simply the name the colon form generates, and every Lua programmer expects to see it.

A class is a table that is its own fallback

local Account = {}
Account.__index = Account

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

function Account:deposit(amount)
    self.balance = self.balance + amount
    return self.balance
end

local a = Account.new("ada")
print(a:deposit(50))
print(a:deposit(25))
print(rawget(a, "deposit"), type(a.deposit))

--> 50
--> 75
--> nil	function

Account.__index = Account makes the class its own fallback, so a lookup that misses on the instance finds the method on the class. new uses a dot because there is no instance yet — it is the thing that makes one; deposit uses a colon because there is. The last printed line is the proof: a has no deposit field of its own, and the call reached it through the metatable exactly as item.colour reached defaults. Instances hold data, the class holds behaviour, __index is the wire.

The mistake everybody makes once

Call a method with a dot and nothing is passed for self, so your first argument lands there instead. One more line on the end of that same file:

print(pcall(function() return a.deposit(5) end))

--> false	script.lua:9: attempt to index a number value (local 'self')

Line 9 is self.balance = self.balance + amount, up in the class: 5 became self, and the body tried to index a number. Read that message as "you wrote . where you meant :" — it is almost never anything else.

Never put a mutable value on the class

The class table is shared by every instance, so a table stored there is one table, not one per object.

local Bag = {}
Bag.__index = Bag
Bag.items = {}                -- one list, shared by every bag

function Bag.new() return setmetatable({}, Bag) end
function Bag:add(v) self.items[#self.items + 1] = v end

local x, y = Bag.new(), Bag.new()
x:add("apple")
y:add("pear")
print(#x.items, #y.items)

--> 2	2

self.items missed on both instances and fell through to Bag.items — the same table each time. Build it inside the constructor instead, setmetatable({items = {}}, Bag), and the identical program prints 1 1. A number on the class is harmless — self.n = self.n + 1 reads the class value and writes a new field on the instance — so it is shared mutable values that bite.

Common mistakes

  • Calling with .a.deposit(5) passes nothing for self, so 5 becomes the receiver and the body dies with attempt to index a number value (local 'self').
  • Forgetting Class.__index = Class — instances are then plain tables, and the first method call fails with attempt to call a nil value (method 'deposit').
  • A constructor defined with a colonfunction Account:new(owner) called as Account.new("ada") puts "ada" into self and leaves owner nil. No error; just an object with no owner.
  • A table field on the class — every instance shares it. Create per-instance tables inside the constructor.
  • Detaching a methodlocal d = a.deposit then d(10) drops the receiver and fails like a dot call.

Your exercise

Two Stacks hands you two independent stacks, a and b, and a stream of commands. You write the Stack class: the constructor plus push, pop and size.

Two mistakes the grader will catch, both from this page. Define the methods with a colon so they receive self — a function Stack.push(v) that the driver invokes as s:push(value) binds the stack to v and loses the value entirely. And create items inside Stack.new rather than as Stack.items = {} on the class: that version passes the single-stack test, then reports y when you pop a, because both stacks have been appending to one list.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…