Lua: Metatables

From Mario Fan Games Galaxy Wiki
Revision as of 03:25, 27 September 2009 by Xgoff (talk | contribs) (was i drunk when i wrote this)
Lua
Lua.gif
Basics
Intermediate
Advanced
XLua
Add to this template
 Standardwikimessagebox.png This article assumes the use of Lua 5.1.

Information may not be accurate or may need revision if you are using a different version.

Metatables are a way to define behavior in cases where no behavior is defined. Try this code:

table1 = {1, 2, 3}
table2 = {10, 20, 30}
table3 = table1 + table2 -- error!

you'll just get an error. You can't add tables. Well, that's wrong; you CAN add tables, but you have to define just what 'add' means when tables are involved. Does it mean adding up the values in each row, or appending one table onto the end of another? It can mean either; which one is up to you.

To do this, we must assign a metatable to the table. Metatables are nothing special, they're just regular tables. A table can have only one metatable, but that metatable can have its own metatable, which can have its own metatable, and so on. However, for performance and complexity reasons, you'll usually just have a single "level". Also, multiple tables can share the same metatable. An interesting note: a table can be its own metatable, which is useful when you start dealing with methods.

To set the metatable for a table, you use the setmetatable function, like so:

setmetatable(table, metatable)

So going back to our original code, we need to add a new table to use as a metatable:

mt = { }
table1 = {1, 2, 3}
table2 = {10, 20, 30}
table3 = table1 + table2 -- error!

Now we're ready to assign the tables to the metatable. But to make this more flexible, we should write a constructor function which automatically creates a table and sets its metatable:

function newTable(t_data)
  return setmetatable(t_data, mt)
end

Then we call it like a regular function, but we pass a table as its parameter, basically like: newTable({1,2,3}). Actually, we can take advantage of another syntax feature to make this more like a regular table construction: if you have a function that takes a single table parameter or single string parameter, you don't need to use the parenthesis for the function call. So we can rewrite our above code's constructors like:

function newTable(t_data)
  return setmetatable(t_data, mt)
end

mt = { }
table1 = newTable {1, 2, 3}
table2 = newTable {10, 20, 30}
table3 = table1 + table2 -- error!

Well, that didn't seem to help, because we still get an error. Despite what we did, we still didn't define how addition works, so let's do that now. We do this by using a metamethod, a special function used within metatables that are called during certain operations. It turns out that the metamethod for addition is __add (those are two underscores). __add is a function that has two parameters passed to it—the two added tables. Even if you're adding more than two tables, Lua takes them two at a time. So let's define __add:

mt = { 
    __add = function (a, b)
        local sum = newTable { }
        for i = 1, math.max(#a, #b) do -- run as many loops as there are entries for the largest table
            sum[i] = (a[i] or 0) + (b[i] or 0) -- the actual addition definition
        end
        return sum
    end
}

I'm going to point this out right now: notice how we used our newTable function with the sum table. If you don't do this, that table you just generated can't be added to other tables, because it won't have this metatable associated with it. Don't forget about this when you define other types of metamethods! Also those or 0 parts... those are there to make sure 'holes' in a shorter table won't throw errors. Anyway, our code is ready; this is what it should now look like:

function newTable(t_data)
  return setmetatable(t_data, mt)
end

mt = { 
    __add = function (a, b)
        local sum = newTable { }
        for i = 1, math.max(#a, #b) do -- run as many loops as there are entries for the largest table
            sum[i] = (a[i] or 0) + (b[i] or 0) -- the actual addition definition
        end
        return sum
    end,

    __tostring = function (t)
        return table.concat(t, " ")
    end,
}

table1 = newTable {1, 2, 3}
table2 = newTable {10, 20, 30}
table3 = table1 + table2 -- valid!

print(table3)

Now table3 will contain what you expect; that is, {11, 22, 33}. If you noticed, there is an added __tostring metamethod; this is automatically called when an attempt to coerce the table into a string is made, which happens to be the case during print().

This was just a small example of how metatables and metamethods work. Again, when dealing with methods, you will be using them a lot. For now, try defining the __sub and __mul metamethods (for subtraction and multiplication, respectively). There are quite a few metamethods, so why not try them out?