Pedro Gimeno's gists (pastes with history), each in an independent (orphan) branch
Pedro Gimeno c670aac629 Fix some inaccuracies | 3 lat temu | |
---|---|---|
README.md | 3 lat temu |
Metatables sound like some kind of obscure voodoo that allows all sorts of magical behaviours. The purpose of this tutorial is to demistify them in order to gain an understanding of their behaviour and utility.
This text is oriented to Lua 5.1 and LuaJIT, but it will probably be extensible to newer versions
A metatable is nothing more than a table associated to a value or type.
Really, it's that simple. This association can be made with the function setmetatable()
or debug.setmetatable()
, and you can only associate one: setting a new metatable will overwrite the previous association. Whether it is associated to a value or to a type depends on the type. Every table and (full) userdata object can have its own metatable, therefore in that case it applies to each value independently. However, when setting the metatable of a number, all numbers will share the same metatable, and the same happens with the string, function, light userdata, thread, boolean and nil types, hence it applies to the type (or to all values of that type, if you prefer). You can query the metatable of any value in order to retrieve its associated metatable, if it has any.
Note that getmetatable
/setmetatable
and debug.getmetatable
/debug.setmetatable
are not the same thing. The debug
versions are "raw", they directly operate on the metatables; the normal ones are more restricted. debug.getmetatable
will always return the metatable of a value; however, getmetatable
will first check the metatable to see if it has a __metatable
field, and if so, it will return its value instead of the metatable. On the other hand, setmetatable
only lets you set the metatable of table values, not of any other type, and only if the __metatable
field of the metatable is unset. That is, they behave something like this:
function getmetatable(obj)
local mt = debug.getmetatable(obj)
if mt ~= nil and mt.__metatable ~= nil then
return mt.__metatable
end
return mt
end
function setmetatable(obj, value)
if type(obj) ~= "table" then
error("bad argument #1 to 'setmetatable' (table expected, got " .. type(obj) .. ")")
end
local mt = debug.getmetatable(obj)
if mt ~= nil and mt.__metatable ~= nil then
error("cannot change a protected metatable")
end
debug.setmetatable(obj, value)
return obj
end
Here's an example of setting and retrieving the metatable of a number:
local mt = {a = 1, b = 2}
local n = debug.setmetatable(0, mt)
print(getmetatable(5).a)
This snippet displays 1
. All values of type number
share the same metatable, which is the same table we have associated as the metatable of the value 0
, and the field a
of that table equals 1
.
However, in most cases, we only need to set the metatable of a table, not of any other type. So, here's another example that does that:
local mt = {a = 1, b = 2}
local t1 = {}
local t2 = setmetatable(t1, mt)
assert(t1 == t2)
print(t1.a)
print(getmetatable(t1).a)
Now, t1
has a metatable associated to it, which is mt
. The setmetatable
function returns the first parameter, t1
in this case, which is why the comparison will be true. The first print
statement will print nil
because t1
has no field called a
(in fact it has no fields whatsoever), but its metatable is a table that contains the field a
and it equals 1
, therefore the second print
statement will print 1
.
OK, so far we've seen that a metatable is just a table associated to a type or value. If it's that simple, what makes it so special?
What makes it special is that some operations, like addition, comparison, indexing, and others, when performed on certain types, consult the metatable of the operand, checking for certain special keys, before returning a value or giving an error.
For example, you may think that the operator +
is simply implemented like this (in Lua pseudocode):
function operator_plus(value1, value2)
if type(value1) ~= "number" or type(value2) ~= "number" then
if type(value1) == "number" then
value1 = value2 -- report the error on the type of value2
end
error("attempt to perform arithmetic on a " .. type(value1) .. " value")
end
return value1 + value2
end
But that's not the case. Instead, the metatable of the first value is first checked, and if it contains a field called __add
, it is taken as a function and called, and the return value is the result of the operation. If this field doesn't exist, the same is done for the second value. Only when both are missing is the error triggered, so it actually behaves like this:
function operator_plus(value1, value2)
if type(value1) ~= "number" or type(value2) ~= "number" then
local mt = getmetatable(value1)
if mt ~= nil and mt.__add ~= nil then
return mt.__add(value1, value2)
end
mt = getmetatable(value2)
if mt ~= nil and mt.__add ~= nil then
return mt.__add(value1, value2)
end
if type(value1) == "number" then
value1 = value2 -- report the error on the type of value2
end
error("attempt to perform arithmetic on a " .. type(value1) .. " value")
end
return value1 + value2
end
In general, except for indexing, normal operators can't be redefined. For example, for the number type, the numeric operations on numbers can't be redefined; you can't make e.g. 1 + 1
return 3. But if it would otherwise raise an error, the metamethod will be called. Here are examples for number and string with __add
and __concat
:
debug.setmetatable(1, {__add = function () print("__add on numbers") end,
__concat = function () print("__concat on numbers") end})
getmetatable("").__add = function () print("__add on strings") end
getmetatable("").__concat = function () print("__concat on strings") end
if 1 + 1 == 2 then print("ok") end -- prints "ok"
if 1 .. 1 == "11" then print("ok") end -- prints "ok"
if 1 .. {} then error() end -- prints "__concat on numbers"
if 1 + "" then error() end -- prints "__add on numbers"
if "" + 1 then error() end -- prints "__add on strings"
if "a" .. "a" == "aa" then print("ok") end -- prints "ok"
if "a" .. {} then error() end -- prints "__concat on strings"
if print + 1 then error() end -- prints "__add on numbers"
The last one happens because we haven't defined a metatable for functions, therefore the __add
method of the metatable for the second argument is checked.
The operation of indexing a table checks the __index
field of the metatable when the field does not exist, and the operation of creating a new field in a table checks the __newindex
field of the metatable. However, the case of __index
and __newindex
is special, in that instead of a function, the value of the field can be a table. When it is a function, __index
accepts two parameters: the table and the field, while __newindex
accepts three: the table, the field and the value. When set to a table, this code:
mt.__index = LookupTable
setmetatable(OriginalTable, mt)
behaves as if we had written this:
mt.__index = function (tbl, key)
return LookupTable[key]
end
setmetatable(OriginalTable, mt)
therefore, it's a shortcut for the very common operation of checking another table when a certain field does not exist in the original table. This access can in turn trigger other metatables. This is commonly used with OriginalTable being an instance or a class, and LookupTable being a class.
Similarly, this code:
metatable.__newindex = MyTable
behaves like this one:
metatable.__newindex = function (tbl, key, value)
MyTable[key] = value
end
which basically redirects the creation of new fields in a certain table to a different table. This isn't so useful for OOP as the __index
property; however, you may find other uses for it.
When writing functions for the __index
or __newindex
metamethods, it's possible that we run into a circular loop, in which accessing a field triggers the metamethod in turn. To avoid that, we have the global Lua functions rawget
and rawset
. rawget
will read, and rawset
will set, a field in a table without checking its metatable.
Here's an example of using __index
for OOP. This example will print the number 5:
-- Define a table to use as a class
local MyClass = {}
MyClass.mt = {__index = MyClass}
function MyClass:printX()
print(self.x)
end
local MyInstance = setmetatable({x = 5}, MyClass.mt)
MyInstance:printX() -- prints 5
How does that work? When we ask Lua to call the printX()
method, Lua first tries to access field printX
in table MyInstance
, and since it notices that this field isn't present in this table, it checks the metatable of MyInstance
for an __index
metamethod. The value of __index
is MyClass
, therefore Lua checks for the field printX
in the table MyClass
. Since it exists, Lua does not continue checking the metatable of MyClass
, and instead uses the value of MyClass.printX
, which is a function. Then it calls that function with the original parameter, which was the table MyInstance
. The end result is that this call:
MyInstance:printX()
is interpreted as if it was written like this:
MyClass.printX(MyInstance)
which prints 5.