Pedro Gimeno's gists (pastes with history), each in an independent (orphan) branch

Pedro Gimeno c670aac629 Fix some inaccuracies 3 lat temu
README.md c670aac629 Fix some inaccuracies 3 lat temu

README.md

Lua Metatables tutorial

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

What is a metatable?

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.

So, what's special about a metatable?

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.