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 = true6:}7:end8: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 105:print(b.y) -- prints 206: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 107:print(b.y) -- prints 208: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 102:print(b.y) -- prints 203:print(a.y) -- also prints 204:5:b.y = 506: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 = true5:}6:7:-- Here is where the metamethods get defined!8:local meta = {9:__index = Vector210:}11:12:function Vector2.new(x, y)13:14:local components = { x = x, y = y }15:setmetatable(components, meta)16:17:return components18:end19: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 Vector22:Vector2 = {3:x = 0,4:y = 0,5:isVector = true,6:7:-- Let's define 2 functions that will add two Vectors component-wise8: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/sub20:-- methods to the __add and __sub metamethods respectively21:__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 components30:end31: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 - v238: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 Vector22: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 like14: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 method25:__tostring = Vector2.stringify26:}27:28:function Vector2.new(x, y)29:local components = { x = x, y = y }30:setmetatable(components, meta)31:32:return components33:end34: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.