[11/10/2025] - metatables!

metatables! they are a really nice feature in the lua programming language. basically, they are tables which describe how other tables should be used

note: i'll be using lua 5.1 here, it is the most recent version luajit supports. it should be mostly unchanged for newer lua releases but you never know

first, to understand what a metatable is, you need to know what a table is. a table is a hashmap aka dictionary, hash table, unordered map mixed with a list. they can accept collections of items and give them a numeric index, like a list:

local t = {"first","second","third"}
table.insert(t, "fourth")
print(t[1]) -- "first" (tables start at 1)
print(t[2]) -- "second"
print(t[3]) -- "third"
print(t[4]) -- "fourth"
print(t[5]) -- "nil" (no value)

and they can also associate a value (key) with another value (value), like a map. usually, this key is a string value, so you can map any text with a value, like this:

-- table with data initialized
local t = {
	-- "foo" is the key, 123 is the value
	foo = 123,
	bar = "the value can be anything!"
}

-- you can add new entries to a table
t.new = false

print(t.foo) -- "123"
print(t.bar) -- "the value can be anything!"
print(t.new) -- "false"
print(t.none) -- "nil" (there is no entry with the key "none")

-- delete an entry
t.new = nil

print(t.new) -- "nil"

tables can accept anything as the value, but so can the keys!

local t = {
	-- number key, common practice
	-- (this is how table as a list works)
	[123] = "works"
	[-123] = "also works"
	-- fractional number key
	[3.1415] = "pi"
	-- boolean value as key. false doesn't work :(
	[true] = "yep!"
}

local dummy = {}

-- table using a table as a key for an entry
t[dummy] = "yummy"

-- new entry using the table itself as the key
t[t] = "my self!"

print(t[123]) -- "works"
print(t[-123]) -- "also works"
print(t[3.1415]) -- "pi"

print(t[true]) -- "yep!"
print(t[dummy]) -- "yummy"
print(t[t]) -- "my self!"

functions also work as keys:

function foo()
	print("foo!")
end

local t = {
	[foo] = 1
	-- the standard print function
	[print] = 1,
}

for key, value in pairs(t) do
	key("bar!") -- calls key as a function, passes "bar!" as an argument
end

-- note: the order may change. read the docs
-- "bar!"
-- "foo!"

i find it really cool. but it isn't that useful. you can't do any operation other than a simple lookup.


or can we?

this is where metatables come in: metatables allow you to define custom behaviour for certain operations. lets start simple: say we want to implement an add operation to a table, so that all the values on our table are added by the other value. something like this:

local meta = {
	-- we create a function called __add in our metatable
	-- this is called a metamethod
	__add = function(self, other)
		local out = {}

		for i, x in pairs(self) do
			out[i] = self[i] + other
		end

		return out
	end
}

local data = {1,2}
-- creates a table using our metatable
local t = setmetatable(data, meta)

-- adding 2 to our table using the metatable. our __add function is called, and t2 is whatever our function returned
local t2 = t + 2 -- t2 = {3,4}

this is a form of operator overloading. a very elegant one, since you define it using a simple and legible data structure: tables. this is the beauty of it, in my opinion. this allows for complex behaviour without needing overly complex syntax, or making the language too hard to understand. it is also efficient enough, since you only need to do some simple lookups, which if you are using lua you are doing everywhere. other programming languages struggle a lot with this. take c#, c++, rust, whatever, as an example and see it for yourself. python comes close

but well, if you are anything like me, you might say operator overloading is bad! it makes code unpredictable! and i agree with that. but if i were to choose an implementation of operator overloading, i'd say lua did it pretty nicely


however, this is just a fraction of their true power. meet: __index!

__index is the metamethod which defines how values are accessed in a table. normally, if a key is not present in a table, it returns nil. if there is no such value in this table, the __index function is used. this allows for complex behaviour with a simple system

local meta = {
	-- simple demonstration: returns the key itself, not a value
	__index = function(self, key)
		return key
	end
}

local t = setmetatable({}, meta)
print(t.message) -- "message"
print(t[1 + 2]) -- "3"

the __index doesn't need to be a function, it can be a table which will be used as a fallback. this is how you build inheritance in lua

local base = {
	name = "base",
	hello = function(self)
		print(self.name .. ": hello!")
	end,
}

-- overrides the name
local child = setmetatable({name = "child"}, {__index = base})

base:hello() -- "base: hello!"
child:hello() -- "child: hello!"

personally i don't enjoy inheritance that much. i prefer a composition-based system. thanks to the simple and flexible design of metatables, we can easily implement it!

local CHealth = {
	health = 5,
	hurt = function(self, amount)
		self.health = self.health - amount
	end
}

local CMeleeAttack = {
	damage = 1,
	attack = function(self, other)
		other:hurt(self.damage)
	end
}

local CRangedAttack = {
	damage = 1,
	ammo = 4,
	attack = function(self, other)
		if self.ammo == 0 then return end
		other:hurt(self.damage)
		self.ammo = self.ammo - 1
	end
}

-- creates a metamethod which decides between different base tables
local function xinherit(...)
	local components = {...}
	return function(self, key)
		for _, x in ipairs(components) do
			if x[key] ~= nil then
				return x[key]
			end
		end

		return nil
	end
end

local swordguy = setmetatable({damage = 2}, {
	__index = xinherit(CHealth, CMeleeAttack)
})

local bowguy = setmetatable({}, {
	__index = xinherit(CHealth, CRangedAttack)
})

bowguy:attack(swordguy)
swordguy:attack(bowguy)

print(swordguy.health) -- "4"
print(swordguy.ammo) -- "nil"

print(bowguy.health) -- "3"
print(bowguy.ammo) -- "3"

this is one of the reasons i'm considering moving from godot to love2d. gdscript doesn't have this flexibility and you work under rigid pre-made structures, like each .gd file necessarily being a optionally named i hate this class, or the tree-based node system, which is flexible but it's a pain in the ass to create nicely reusable components and loosely coupled systems. one positive point of gdscript is the type annotations, which lua doesn't have. helps a lot with maintaining the code predictable