Pedro Gimeno's gists (pastes with history), each in an independent (orphan) branch
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Pedro Gimeno b07d2623d2 Mention `rawset` and `rawget` 3 months ago
README.md Mention `rawset` and `rawget` 3 months ago

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" then
    error("attempt to perform arithmetic on a " .. type(value1) .. " value")
  end
  return value1 + value2
end

But that's not the case. Instead, the metatable 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. Only if this field doesn't exist, is the above behaviour triggered, so it actually behaves like this:

function operator_plus(value1, value2)
  if type(value1) ~= "number" then
    local mt = getmetatable(value1)
    if mt ~= nil and mt.__add ~= nil then
      return mt.__add(value1, value2)
    end
    error("attempt to perform arithmetic on a " .. type(value1) .. " value")
  end
  return value1 + value2
end

The exception is the number type. The numeric operations on numbers can't be redefined; you can't make e.g. 1 + 1 return 3. In particular, the __add, __sub, __mul, __div, __mod, __pow, __unm, __eq, __lt, __le, __concat metamethods won't be effective on numbers; instead, Lua will use the built-in operations without checking the corresponding metamethods. The rest of metamethods will work on numbers.

Like other operators, 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, 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.