Step 1 of 5 · Reading · ~3 min
Learn
Tables — The One Data Structure
Metatables and __index
Every table you have written so far answers a missing key the same way:
local t = {size = 3}
print(t.weight)
--> nil
A metatable lets you change that answer. It is an ordinary table whose keys
have special names, attached with setmetatable, and it is the mechanism behind
inheritance, operator overloading and every Lua "class" you will ever read.
__index as a fallback table
local base = {font = "serif", points = 12}
local heading = setmetatable({points = 20}, {__index = base})
print(heading.points)
print(heading.font)
print(rawget(heading, "font"))
--> 20
--> serif
--> nil
Read the third line carefully, because it is the whole idea. heading.font is "serif", but heading does not have a font field: rawget looks in the table without consulting the metatable and finds nothing. The lookup only falls through to base when the key is genuinely absent, so heading.points still answers 20.
Writes are different. heading.font = "mono" stores into heading itself and leaves base alone, because __index is consulted for reading only. After that write, rawget(heading, "font") answers "mono" as well.
__index as a function
If the fallback is a computation rather than a lookup, give __index a function.
This is the first place the course writes one, so here is all you need: function(a, b) ... end is a block of code stored like any other value, and it runs when something calls it with arguments; return hands a result back. The Defining Functions lesson covers parameters and results in full. Here the function receives the table and the missing key:
local squares = setmetatable({}, {__index = function(_, k) return k * k end})
print(squares[7], squares[9])
--> 49 81
Operators
The same trick names the arithmetic and comparison hooks. __tostring is the one
you will reach for first, because it fixes print:
local P = {}
P.__index = P
P.__tostring = function(p) return "(" .. p.x .. "," .. p.y .. ")" end
P.__add = function(a, b) return setmetatable({x = a.x + b.x, y = a.y + b.y}, P) end
local a = setmetatable({x = 1, y = 2}, P)
local b = setmetatable({x = 10, y = 20}, P)
print(tostring(a))
print(tostring(a + b))
--> (1,2)
--> (11,22)
__eq, __lt, __len and __call work the same way — __len is what #
consults, and __call makes a table usable as a function.
Notice P.__index = P. That line is the class pattern: the metatable points at
itself, so any method stored on P is found by every table created with it as a
metatable. The next lesson builds on exactly that.
Common mistakes
- Expecting
__indexto fire for a key that exists — it is consulted only when the key is absent, which is why an own field always wins. - Confusing
rawgetwith normal indexing —t.kmay come from the metatable;rawget(t, "k")never does. Use it when you need to know which. - Forgetting
setmetatablereturns the table — that is whyreturn setmetatable({...}, P)is the idiom rather than three lines. - Setting
__indexto a function that forgets to return — the caller then seesniland the fallback looks broken. - Assuming metatables are inherited automatically — each table needs its own
setmetatablecall; the constructor is what makes that reliable.
Your exercise
Config With Defaults builds the fallback pattern and then reports which side answered.
The starter gives you a defaults table and a command loop. You create config so that it starts empty with defaults behind it, then handle two commands: one stores into config itself, the other looks a key up and reports whether the answer came from config or from defaults. Plain indexing cannot tell those two apart, because it returns the same value either way, and telling them apart is what rawget is for.
The mistakes the grader will catch: storing into defaults instead of config changes the answer for every later lookup; and reporting by checking config[key] alone labels every default as "own".
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…