ezb.sh logo
Back to Home

What are metatables?

A walkthrough of the basics of Lua's metatables
Written by ezb
lua
Tables in Lua are amorphous and vague by design. You can make a table behave in many different ways using a metatable, which is a table which includes metamethods. Metamethods control what happens when a table is used in different situations, allowing us to make tables behave like classes, numbers, strings - anything, really. We can also control operator overloading with metamethods. Throughout this post I'll be using metatables/methods to create a simple Vector2 library.
Say I wanted to create a 2D vector data type in Lua. A simple approach could look like this:
1:
function createVector(x, y)
2:
return {
3:
x = x,
4:
y = y,
5:
isVector = true
6:
}
7:
end
8:
9:
local vec1 = createVector(0, 1)
10:
local vec2 = createVector(1, 0)
This returns tables which have the fields we defined in the createVector function. However, there's nothing really connecting these two tables together, apart from the fact that they happen to share the same shape. We can use metatables to emulate inheritance in Lua, which can create this link that ties the two tables together.

The __index metamethod

We'll get back to the Vectors, but for the sake of illustration, consider the following two tables:
1:
local a = { x = 10 }
2:
local b = { y = 20 }
3:
4:
print(a.x) -- prints 10
5:
print(b.y) -- prints 20
6:
print(a.y) -- prints nil
It's pretty obvious that a.x and b.y are the only defined fields, so trying to access a.y will return nil. However, using metatables, we can use table B's indices as a "backup" of sorts for table A, using the __index metamethod.
1:
local a = { x = 10 }
2:
local b = { y = 20 }
3:
local meta = { __index = b }
4:
setmetatable(a, meta)
5:
6:
print(a.x) -- prints 10
7:
print(b.y) -- prints 20
8:
print(a.y) -- also prints 20!!
This relationship is one of references, so if we update b.y and then print a.y later, it will print the updated value:
1:
print(a.x) -- prints 10
2:
print(b.y) -- prints 20
3:
print(a.y) -- also prints 20
4:
5:
b.y = 50
6:
print(a.y) -- now prints 50
So we've seen that we can tie two tables together using metatables. Let's use this to create a reusable Vector2 "class". Before showing you the code, here's a question for you to think about - what are the vectors a, b, and c's components set to?
1:
local Vector2 = {
2:
x = 0,
3:
y = 0,
4:
isVector = true
5:
}
6:
7:
-- Here is where the metamethods get defined!
8:
local meta = {
9:
__index = Vector2
10:
}
11:
12:
function Vector2.new(x, y)
13:
14:
local components = { x = x, y = y }
15:
setmetatable(components, meta)
16:
17:
return components
18:
end
19:
20:
local a = Vector2.new(5, 10)
21:
local b = Vector2.new()
22:
local c = Vector2.new(nil, 2)
Because we explicitly define x and y for vector a, it's x and y will be set to 5 and 10 respectively. However, b was given no arguments, so both its x and y are 0, since Vector2 defines the default x and y to 0. c passed nil into x and 2 into y, so its x value falls back to 0, and is y value is set to 2.

Mathematic metamethods

__index isn't the only metamethod. I mentioned that we can accomplish operator overloading, which would work very nicely with our Vector2 type. The code above includes Vector2.add, which is nice and all, but wouldn't it be really cool if we could just use the + symbol? Lua has a list of mathematic metamethods, including the basis: __add, __sub, __mul, and __div. We'll use the first two for this:
1:
local Vector2
2:
Vector2 = {
3:
x = 0,
4:
y = 0,
5:
isVector = true,
6:
7:
-- Let's define 2 functions that will add two Vectors component-wise
8:
add = function(vec1, vec2)
9:
return Vector2.new(vec1.x + vec2.x, vec1.y + vec2.y)
10:
end,
11:
sub = function(vec1, vec2)
12:
return Vector2.new(vec1.x - vec2.x, vec1.y - vec2.y)
13:
end,
14:
}
15:
16:
local meta = {
17:
__index = Vector2,
18:
19:
-- Now, we can override the + and - operators by assigning our add/sub
20:
-- methods to the __add and __sub metamethods respectively
21:
__add = Vector2.add,
22:
__sub = Vector2.sub,
23:
}
24:
25:
function Vector2.new(x, y)
26:
local components = { x = x, y = y }
27:
setmetatable(components, meta)
28:
29:
return components
30:
end
31:
32:
local v1 = Vector2.new(10, 10)
33:
local v2 = Vector2.new(5, 5)
34:
local v3 = v1 + v2 -- THIS NOW WORKS!!
35:
print(v3.x, v3.y) -- prints "15 15"
36:
37:
local v4 = v1 - v2
38:
print(v4.x, v4.y) -- prints "10 10"
While I didn't include them in the example above, __mul and __div work in the same way. We can now use mathematic operators on tables. How neat!
The other mathematic metamethods that exist include:
  • __unm - unary minus. Controls what happens when someone writes -tbl. For our vectors, we could use this to return a new vector whose components are negative
  • __idiv - integer division (added Lua 5.3). Overrides the // operator
  • __mod - modulo
  • __pow - exponents. Overrides the ^ operator
  • __concat - concatenation. Overrides Lua's .. concatenation operator

The __tostring metamethod

You might have noticed in the previous section that I had to print out each component of the vectors to see their values. The __tostring metamethod allows us to control how we want our table to be stringified. I'd like my result to look like "Vector2<x, y>", so we can program a function to stringify a vector in that format, and use it as the __tostring metamethod.
1:
local Vector2
2:
Vector2 = {
3:
x = 0,
4:
y = 0,
5:
isVector = true,
6:
add = function(vec1, vec2)
7:
return Vector2.new(vec1.x + vec2.x, vec1.y + vec2.y)
8:
end,
9:
sub = function(vec1, vec2)
10:
return Vector2.new(vec1.x - vec2.x, vec1.y - vec2.y)
11:
end,
12:
13:
-- Here's where we define what we want our output to look like
14:
stringify = function(vec)
15:
return string.format("Vector2<%d, %d>", vec.x, vec.y)
16:
end,
17:
}
18:
19:
local meta = {
20:
__index = Vector2,
21:
__add = Vector2.add,
22:
__sub = Vector2.sub,
23:
24:
-- Use Vector2.stringify as the stringification method
25:
__tostring = Vector2.stringify
26:
}
27:
28:
function Vector2.new(x, y)
29:
local components = { x = x, y = y }
30:
setmetatable(components, meta)
31:
32:
return components
33:
end
34:
35:
local v1 = Vector2.new(10, 10)
36:
print(v1) -- now prints "Vector2<10, 10>"

Conclusion

This wraps up my introduction to metamethods. There are more metamethods to discover, but most of them get really into the weeds of how Lua operates.
If you're looking to use inheritance in Lua, I recommend evolbug's clasp.lua. It's only a few lines and supports inheritance and class extension.