Metatables Tutorial

Written by Greenman

Introduction

Let’s start by defining some terms:

Metamethods always have two underscores __ in their name and the name of the metamethod tells you what it does.

Let’s start by setting up a metatable:

local t = {10, 20, 30}
local mt = {}

setmetatable(t, mt)

The above example doesn’t do much, but it does demonstrate the usage of setmetatable which is a global function that attaches metatables to tables.

Here’s the documentation for the function:

table setmetatable(<table> t, <table> metatable)

The first argument is returned by the function. Now that we know how to set a metatable, let’s try adding a metamethod.

I’m going to start with a simple one called __index:

local t = {10, 20, 30}
local mt = {
  __index = function(t,k)
    return "Attempted to access "..k
  end
}
print(t[9]) --> nil

setmetatable(t,mt)

print(t[1]) --> 10
print(t[5]) --> Attempt to access 5

On line 7, we see this:

print(t[9]) --> nil

This is the expected result of indexing a table with an index that has no value.

After the line where we call setmetatable, the behavior changes:

print(t[1]) --> 10
print(t[5]) --> Attempt to access 5

Note: The first line shown above is to show you that indexing the table with indices where there are values is still the same.

So, what can we conclude from this example?

  1. Metamethods are called when specific events happen to the table
  2. The purpose of metatables/metamethods is to change the behavior of tables
  3. __index is called when you access an index with no value (also called a nil value)

One thing I should mention is the arguments the metamethod recieves. Let’s focus on our metamethod definition:

__index = function(t,k)
  return "Attempted to access "..k
end

__index is getting t which is the table being indexed and k which is the index or key accessed on the table.

If we did:

print(t[9])

Assuming that index 9 has no value and we attached a metatable with __index in it, the metamethod would recieve these arguments:

  1. t (the table we defined)
  2. 9 (the index we tried to access)

__index

If you remember from the previous section, __index is a metamethod that is triggered when you access an index with a nil value in a table.

The arguments the metamethod receives are:

  1. <table> table - The table being indexed
  2. <string, int> key - The key/index being accessed

Example:

local t = {10, 20, 30}
local mt = {
  __index = function(t, k)
    return k
  end
}

print(t[9]) --> nil
setmetatable(t, mt)
print(t[9]) --> 9

__index With A Table

One thing that is unique about the __index metamethod is that you can assign a table instead of a function. In doing so, that table will be accessed when you access a nil value.

Example:

local t = {
  x = 10, 
  y = 20, 
  z = 30
}
local mt = {
  __index = {a = 99}
}
setmetatable(t,mt)

print(t.a) --> 99

__index Chain

One interesting thing about __index with a table is that you can give the table assigned to __index a metatable with __index assigned to another table so if the value is not found in the first backup table, the next table will be searched.

Example:

local t = {
  x = 10,
  y = 20,
  z = 30
}
local mt = {
  __index = setmetatable({a = 99},{ -- {a = 99} is returned
    __index = {b = 25}
  })
}
setmetatable(t,mt)

print(t.a) --> 99
print(t.b) --> 25

rawget

Lua has a global function called rawget which allows you to index a table without triggering __index if the value is nil.

Documentation:

any rawget(<table> t, <int> index)

Example:

local t = {10, 20, 30}
local mt = {
  __index = function(t, k)
    return k
  end
}

setmetatable(t, mt)
print(t[9]) --> 9
print(rawget(t,9)) --> nil

__newindex

__newindex is a metamethod that is triggered when you assign an index a value which was nil when the table was created.

The arguments the metamethod receives are:

  1. <table> table - The table receiving a new value
  2. <string, int> key - The key being assigned a value
  3. <any> value - The value assigned to the key

Example:

local t = {10, 20, 30}
local mt = {
  __newindex = function(t, k, v)
    rawset(t, 5, v*2)
  end
}

t[4] = 10
print(t[4]) --> 10
setmetatable(t, mt)
t[3] = 100
print(t[3]) --> 100
t[5] = 40
print(t[5]) --> 80

In the above example, there’s two things to notice:

  1. The first assignment after the call to setmetatable will not invoke __newindex because index 3 was not originally nil and had a value
  2. The __newindex metamethod uses rawset instead of assigning the value with the usual syntax t[k] = v*2 to prevent a stack overflow

If we didn’t use rawset, this is what would happen:

local t = {10, 20, 30}
local mt = {
  __newindex = function(t, k, v)
    t[k] = v*2
  end
}
setmetatable(t, mt)

t[5] = 40
print(t[5])

Output:

lua: script.lua:5: C stack overflow  
stack traceback:  
	script.lua:5: in function <script.lua:4>  
	...
	script.lua:10: in main chunk  
	[C]: ?

We are invoking __newindex inside the metamethod which results in the metamethod being infinitely invoked. This produces a stack overflow because the space needed to store the variables and information associated with each call (to the metamethod) is more than can fit on the stack.

__call

__call is a metamethod that is invoked when you try to call a table like a function.

The arguments the metamethod recieves are:
<table> table - The table being called
<any> ... - The values passed to the function call

Normally, you can’t call a table without producing an error:

local t = {10, 20, 30}
t()

Output:

lua: script.lua:2: attempt to call local 't' (a table value)  
stack traceback:  
	script.lua:2: in main chunk  
	[C]: ?

But with __call, you can:

local t = {10, 20, 30}
local mt = {
    __call = function(t)
	for k,v in pairs(t) do
	     print(k,v)
	end
    end
}
setmetatable(t, mt)
t()
-- 1 10
-- 2 20
-- 3 30

You can also accept and use arguments when working with __call:

local t = {10, 20, 30}
local mt = {
    __call = function(t, n)
	for k,v in pairs(t) do
	    t[k] = v*n
	end
    end
}
setmetatable(t, mt)
t(10)
for k,v in pairs(t) do
    print(k,v)
end
-- 1 100
-- 2 200
-- 3 300

If you want to accept any amount of arguments, you can use ... and store the ... in a table so you can handle it:

local t = {10, 20, 30}
local mt = {
    __call = function(t, ...)
	local args = {...}
	for k,v in pairs(args) do
	    table.insert(t,v)
	end
    end
}
setmetatable(t, mt)
t(40, 50, 60, 70)
for k,v in pairs(t) do
    print(k,v)
end
-- 1 10
-- 2 20
-- 3 30
-- 4 40
-- 5 50
-- 6 60
-- 7 70

__concat

__concat is a metamethod that is invoked when you concatenate a table to another table using the .. operator.

The arguments the metamethod receives are:
<table> table - The table being concatenated
<any> value - The second value in the concatenation expression

Example:

local t = {'a', 'b', 'c'}
local mt = {
    __concat = function(t, val)
	local copy = {}
	for k,v in pairs(t) do
	    table.insert(copy, v..val)
	end
	return copy
    end
}
setmetatable(t, mt)

local t2 = t .. '1'
for k,v in pairs(t2) do
    print(k,v)
end
-- 1 a1
-- 2 b1
-- 3 c1

Example 2:

local t = {'a', 'b', 'c'}
local t2 = {'1', '2', '3'}
local mt = {
    __concat = function(t, t2)
        local copy = {}
        for k,v in pairs(t) do
	    table.insert(copy, v..t2[k])
        end
        return copy
    end
}
setmetatable(t, mt)

local t3 = t .. t2
for k,v in pairs(t3) do
    print(k,v)
end
-- 1 a1
-- 2 b2
-- 3 c3

__len

__len is a metamethod that is invoked when you use the length operator # on a table.

The arguments the metamethod receives are:
<table> table - The table the # operator is used on

Example:

local t = {10, 20, 30}
local mt = {
    __len = function(t)
        return 0
    end
}
setmetatable(t, mt)
print(#t) --> 0

__metatable

__metatable is a metamethod that is invoked when getmetatable is used on a table.

The arguments the metamethod receives are:
<table> table - The table getmetatable is called on

Example:

local t = {10, 20, 30}
local mt = {
    __metatable = 'The metatable is locked'
}
setmetatable(t, mt)

print(getmetatable(t)) --> The metatable is locked

Keep in mind that it’s still possible to get the metatable without invoking __metatable by using debug.getmetatable.

Arithmetic Metamethods

The arithmetic metamethods are invoked when the operator they are named after is used on the table.
These are all the arithmetic metamethods:
__add - Invoked when + is used
__sub - Invoked when - is used
__mul - Invoked when * is used
__div- Invoked when / is used
__mod - Invoked when % is used
__pow - Invoked when ^ is used
__unm - Invoked when - is used to represent negative

The arguments all the arithmetic metamethods receive (except __unm) are:
<table> table - The table the operation is being performed on
<any> value - The second value in the expression

Let’s say we had a table which stored x, y, and z components and we had other tables which fit the same description and we wanted to perform operations. We could use these metamethods for that:

local vec = {x = 10, y = 20, z = 30}
local vec2 = {x = 30, y = 46, z = 32}
local mt = {
__add =  function(t, t2)
    local copy = {}
    copy.x  = t.x + t2.x
    copy.y  = t.y + t2.y
    copy.z  = t.z + t2.z
    return copy
end,
__sub =  function(t, t2)
    local copy = {}
    copy.x  = t.x - t2.x
    copy.y  = t.y - t2.y
    copy.z  = t.z - t2.z
    return copy
end,
__mul =  function(t, t2)
    local copy = {}
    copy.x  = t.x * t2.x
    copy.y  = t.y * t2.y
    copy.z  = t.z * t2.z
    return copy
end,
__div =  function(t, t2)
    local copy = {}
    copy.x  = t.x / t2.x
    copy.y  = t.y / t2.y
    copy.z  = t.z / t2.z
    return copy
end
}
setmetatable(vec, mt)

function outputVector(v)
    print(v.x..', '..v.y..', '..v.z)
end

outputVector(vec + vec2) --> 40, 66, 62
outputVector(vec - vec2) --> -20, -26, -2
outputVector(vec * vec2) --> 300, 920, 960
outputVector(vec / vec2) --> 0.33333333333333, 0.43478260869565, 0.9375

The arguments the __unm metamethods receives are:
<table> table - The table the operation is being performed on

Example:

local vec = {x = 10, y = 20, z = 30}
local mt = {
    __unm = function(t)
        return {x = -t.x,y = -t.y,z = -t.z}
    end
}
setmetatable(vec, mt)

function outputVector(v)
    print(v.x..', '..v.y..', '..v.z)
end

outputVector(-vec) --> -10, -20, -30

Relational Metamethods

The relational metamethods are invoked when relational operators are used with two tables.
These are all the relational metamethods:
__eq - Invoked when == is used
__lt - Invoked when < is used
__le - Invoked when <= is used

You might notice that the not equal ~=, greater than >, and greater than or equal to >= operators don’t have their own metamethods, but that’s because the metamethods are also invoked for their inverse operations (but the operations are treated differently).

_eq - a ~= b is treated as not (a == b)
__lt - a > b is treated as b < a
__le - a >= b is treated as b <= a

Now let’s look at an example of these metamethods with our vectors:

local mt = {
    __eq = function(t, t2)
        return t.x == t2.x and t.y == t2.y and t.z == t2.z
    end,
    __lt = function(t, t2)
        return t.x < t2.x and t.y < t2.y and t.z < t2.z
    end,
    __le = function(t, t2)
        return t.x <= t2.x and t.y <= t2.y and t.z <= t2.z
    end
}

local vec = setmetatable({x = 10, y = 20, z = 30}, mt)
local vec2 = setmetatable({x = 10, y = 20, z = 30}, mt)
local vec3 = setmetatable({x = 200, y = 340, z = 103}, mt)

print(vec == vec2) --> true
print(vec < vec3) --> true
print(vec <= vec2) --> true
print(vec > vec3) --> false
print(vec > vec2) --> false
print(vec ~= vec3) --> true

Conclusion

This tutorial only shows you how to use each metamethod. Metatables & metamethods are very powerful because not only can they be used on non-table values, but they can be utilized to add new features. Examples of those features include:

If you want to explore metatables and metamethods more, I would suggest reading PIL (Programming in Lua) and looking at posts about it in the Lua Users Wiki.