This first edition was written for Lua 5.0. While still largely relevant for later versions, there are some differences.
The third edition targets Lua 5.2 and is available at Amazon and other bookstores.
By buying the book, you also help to support the Lua project.

13.1 – Arithmetic Metamethods

In this section, we will introduce a simple example to explain how to use metatables. Suppose we are using tables to represent sets, with functions to compute the union of two sets, intersection, and the like. As we did with lists, we store these functions inside a table and we define a constructor to create new sets:

    Set = {}
    function (t)
      local set = {}
      for _, l in ipairs(t) do set[l] = true end
      return set
    function Set.union (a,b)
      local res ={}
      for k in pairs(a) do res[k] = true end
      for k in pairs(b) do res[k] = true end
      return res
    function Set.intersection (a,b)
      local res ={}
      for k in pairs(a) do
        res[k] = b[k]
      return res
To help checking our examples, we also define a function to print sets:
    function Set.tostring (set)
      local s = "{"
      local sep = ""
      for e in pairs(set) do
        s = s .. sep .. e
        sep = ", "
      return s .. "}"
    function Set.print (s)

Now, we want to make the addition operator (`+´) compute the union of two sets. For that, we will arrange that all tables representing sets share a metatable and this metatable will define how they react to the addition operator. Our first step is to create a regular table that we will use as the metatable for sets. To avoid polluting our namespace, we will store it in the Set table: = {}    -- metatable for sets
The next step is to modify the function, which creates sets. The new version has only one extra line, which sets mt as the metatable for the tables that it creates:
    function (t)   -- 2nd version
      local set = {}
      for _, l in ipairs(t) do set[l] = true end
      return set
After that, every set we create with will have that same table as its metatable:
    s1 ={10, 20, 30, 50}
    s2 ={30, 1}
    print(getmetatable(s1))          --> table: 00672B60
    print(getmetatable(s2))          --> table: 00672B60

Finally, we add to the metatable the so-called metamethod, a field __add that describes how to perform the union: = Set.union
Whenever Lua tries to add two sets, it will call this function, with the two operands as arguments.

With the metamethod in place, we can use the addition operator to do set unions:

    s3 = s1 + s2
    Set.print(s3)  --> {1, 10, 20, 30, 50}
Similarly, we may use the multiplication operator to perform set intersection: = Set.intersection
    Set.print((s1 + s2)*s1)     --> {10, 20, 30, 50}

For each arithmetic operator there is a corresponding field name in a metatable. Besides __add and __mul, there are __sub (for subtraction), __div (for division), __unm (for negation), and __pow (for exponentiation). We may define also the field __concat, to define a behavior for the concatenation operator.

When we add two sets, there is no question about what metatable to use. However, we may write an expression that mixes two values with different metatables, for instance like this:

    s ={1,2,3}
    s = s + 8
To choose a metamethod, Lua does the following: (1) If the first value has a metatable with an __add field, Lua uses this value as the metamethod, independently of the second value; (2) otherwise, if the second value has a metatable with an __add field, Lua uses this value as the metamethod; (3) otherwise, Lua raises an error. Therefore, the last example will call Set.union, as will the expressions 10 + s and "hy" + s.

Lua does not care about those mixed types, but our implementation does. If we run the s = s + 8 example, the error we get will be inside Set.union:

    bad argument #1 to `pairs' (table expected, got number)
If we want more lucid error messages, we must check the type of the operands explicitly before attempting to perform the operation:
    function Set.union (a,b)
      if getmetatable(a) ~= or
         getmetatable(b) ~= then
        error("attempt to `add' a set with a non-set value", 2)
      ...  -- same as before