Lua: Metatables

From Mario Fan Games Galaxy Wiki
Revision as of 21:50, 16 July 2009 by Xgoff (talk | contribs)
 Standardwikimessagebox.png This page should be syntax highlighted

Code on this page would be more readable and may wrap correctly if it were surrounded by <source> tags using the enclose="div" attribute.

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. We can do it like so, with vararg syntax:

function newTable(...)
  local t = ...
  setmetatable(t, mt)
  return t
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 like:

function newTable(...)
  local t = ...
  setmetatable(t, mt)
  return t
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 (note the added table.concat function to make the table printable):

function newTable(...)
  local t = ...
  setmetatable(t, mt)
  return t
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
}

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

print(table.concat(table3, " "))

Now table3 will contain what you expect; that is, {11, 22, 33}.

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?