Page 1 of 1

Lua - Metatables Tutorial An introduction to metatables in Lua

#1 athlon32  Icon User is offline

  • D.I.C Regular
  • member icon

Reputation: 117
  • View blog
  • Posts: 363
  • Joined: 20-August 08

Posted 30 May 2010 - 05:11 PM

Lua - Metatables Tutorial
By athlon32


1. Introduction


1.1 The Often Unseen Use of Tables
The guiding and most fundamental data structure in Lua is the table. They are hashed heterogeneous associative arrays[1] that can be used for almost anything. In fact they are so useful, that actual features void in the Lua language can be emulated (to a certain degree) with them. Without doubt, tables are an exigent feature that definitely needs to be taken into accountancy. Yet, a table is nothing but a container without its requisite controller—the metatable.

The prefix "meta" comes from a Greek word meaning "after" or "beyond"[2]. It is many times used to describe something that is about itself (For example, a metalanguage is a set of notations used to describe other languages). In this same manner, a metatable is a table holding information about other tables (This isn't one hundred percent true, but we'll clarify as we go along).

Exercises:
1.1 How many distinct uses of tables can you think of?

1.2 Consider different design changes to tables in Lua (e.g. they are no longer heterogeneous or associative). How would these resulting structures change the way Lua would be structured?



2. Metatables


2.1 Metamethods
We begin our lesson on metatables by introducing the concept of metamethods. A metamethod is a function that is called when a specific operation is invoked upon a particular type. Behind the scenes, all types have these special functions (or the general idea); however only tables and user defined types allow users to specify and customize these functions[3]. Metamethods are comparable to operator overloading (such as that of C++), but it also allows us to overload operations uncommon in other languages (even garbage collection routines!).


2.2 Precise Definition of Metatables
A metatable is simply a regular table, but it is special because it holds metamethods. A metatable can be applied or set to any other table, and the recipient will embody the metamethods when operations are performed on her. Metatables are installed with the setmetatable function, and can be retrieved with a call to getmetatable.

2.3 The Simplest Metamethod
To make some sense of all this abstruse theory, a snippet is very convenient. Please turn your attention to this:
-- Example 2.1
-- Simple demonstration of metatables in Lua. 
-- Explanation is in accompanying text.

mt = {} -- this will be our metatable
local t  = {2,4,5} -- mt will be set to this

-- To keep this simple, we will only write
-- a metamethod for the addition operation.
-- Note this is a binary operation, so it
-- takes two parameters.
function mt.__add( a, b )
    local x = {} 
    x[1] = a[1] + b[1];
    return x;
end

-- Now we set our metatable so we can use it
-- with enhanced ambiance.
setmetatable(t, mt);

-- Now, what would normally cause an error can be done
-- without reproach.
local y = t + t; 

-- Remember, our metamethod adds the two first
-- elements of two tables. 
print(y[1]); -- Should be 4


The program begins by creating two tables—one empty, and one with randomly chosen values for illustrative purposes. We then create a function that is part of mt. Since we are overriding the addition operation, we must call our metamethod __add (As an aside, it should be noted that all the metamethods begin with "__"; that is, two underscores—followed by the actual method name. These names are important, and must match. If they differ, the method will not be recognized, nor called).

Our method is a rather interesting one to say the least. Given two operands (a being the left-side of the '+' and b the right), it returns a table that contains the sum of the first elements from both. Remember, we could have made our method do almost anything (or return anything). Like all operator overloading, it is important (if not necessary) to keep natural semantics. Causing the addition operation to return the result of a multiplication—or worse, a subtraction—would be confusing and make code harder to read. But, I digress.

Returning our thoughts to our program, we will realize the next line is a function call. The called routine is named setmetatable, and we pass it two parameters[4]. The first parameter (t) is the table to whom we want to assign our metatable. The second specification passed (mt) is the metatable that we want to assign to our table. This table will then set the latter parameter to the former, and will return the table in its newly assigned form. Congratulations, you have successfully set your first metatable.

The penultimate statement in our example is very peculiar—and without metatables, it simply could not exist. We are creating a variable y, and it is assigned the result of an addition between two tables! Normally this would result in an error somewhere along the lines of: "attempt to perform arithmetic on global 't' (a table value)". However, with our new metamethod, this is completely legal. The actual operation is trivial to say the least, but it nevertheless clearly demonstrates the concept. When any value is attempted to be added along side table t, the first element from both is retrieved and a table containing their sum is returned. An important thing to note is that t can be successfully added to any table (or type that supports indexing), and not only tables that have similar (or identical) metatables. This can cause some difficulties when clients start experimenting with what they can assign, and it is up to code authors to enforce proper type-checking, as well as verifying an argument meets all required conditions.

Our final line confirms what was said above, by printing out the first value in the table returned by our metamethod. Since we added t to itself—and the first element in t is 2, we get a result of four (4). A final cognizance we must take is the fact that the only value in y is the sum result. The metamethod we used only returned a table with that value, and all others were lost. While they technically still exist in t, it is impossible to find them in y. It will be helpful is the reader imagines this as a truncation.

To conclude this section, we will take one more example. This one should be of no problem to you now, and though much longer, it applies all the same basic principles:
-- Example 2.2
-- A more in-depth example of metatables. 
-- Explanation is in accompanying text.

mt = {} -- our metatable
local set  = {2,4,5} -- recipient
local _Set = {5,4}
-- This example works with the metatable 
-- in a way that is more natural. All 
-- values are affected, and indexes that
-- are present in one, but void in another
-- are still used, but with '0' as it's
-- co-operator.

-- Adds all values in the two tables. If
-- one table is larger than the other, then 
-- overlapping values will be added to zero.
function mt.__add( a, b )
    local x = {}  -- return value
    
    -- get both lengths and compare them
    local lenA = #a;
    local lenB = #b;

    if lenA == lenB then -- normal copy
        for i = 1, lenA do
            x[i] = a[i] + b[i];
        end
    elseif lenA > lenB then
        for i = 1, lenA do
            -- checks if we're out of bounds with table 
            -- and if we need to replace its nils with
            -- zero.
            local j = b[i];
            if j == nil then
                j = 0;
            end

            x[i] = a[i] + j;
        end
    else -- b is larger than a
        for i = 1, lenB do
            local j = a[i];
            if j == nil then
                j = 0;
            end

            x = b[i] + j;
        end
    end
    return x;
end

-- Same as addition, but with subtraction
function mt.__sub( a, b )
    local x = {}  -- return value
    
    -- get both lengths and compare them
    local lenA = #a;
    local lenB = #b;

    if lenA == lenB then -- normal copy
        for i = 1, lenA do
            x[i] = a[i] - b[i];
        end
    elseif lenA > lenB then
        for i = 1, lenA do
            -- checks if we're out of bounds with table 
            -- and if we need to replace its nils with
            -- zero.
            local j = b[i];
            if j == nil then
                j = 0;
            end

            x[i] = a[i] - j;
        end
    else -- b is larger than a
        for i = 1, lenB do
            local j = a[i];
            if j == nil then
                j = 0;
            end

            x = b[i] - j;
        end
    end
    return x;
end

-- handle multipication
function mt.__mul( a, b )
    local x = {}  -- return value
    
    -- get both lengths and compare them
    local lenA = #a;
    local lenB = #b;

    if lenA == lenB then -- normal copy
        for i = 1, lenA do
            x[i] = a[i] * b[i];
        end
    elseif lenA > lenB then
        for i = 1, lenA do
            -- checks if we're out of bounds with table 
            -- and if we need to replace its nils with
            -- zero.
            local j = b[i];
            if j == nil then
                j = 0;
            end

            x[i] = a[i] * j;
        end
    else -- b is larger than a
        for i = 1, lenB do
            local j = a[i];
            if j == nil then
                j = 0;
            end

            x = b[i] * j;
        end
    end
    return x;
end

-- and finally division
function mt.__div( a, b )
    local x = {}  -- return value
    
    -- get both lengths and compare them
    local lenA = #a;
    local lenB = #b;

    if lenA == lenB then -- normal copy
        for i = 1, lenA do
            x[i] = a[i] / b[i];
        end
    elseif lenA > lenB then
        for i = 1, lenA do
            -- checks if we're out of bounds with table 
            -- and if we need to replace its nils with
            -- zero.
            local j = b[i];
            if j == nil then
                j = 0;
            end

            x[i] = a[i] / j;
        end
    else -- b is larger than a
        for i = 1, lenB do
            local j = a[i];
            if j == nil then
                j = 0;
            end

            x = b[i] / j;
        end
    end
    return x;
end

-- Now we set our metatable so we can use it
-- with enhanced ambiance.
setmetatable(set, mt);

-- Test it.
local y = set * _Set; 

for i,v in ipairs(y) do
    print(i, v); 
end


As has been stated, this snippet is essentially the same as the previous one. The only real difference is the introduction of three new metamethods: "__sub", "__mul", and "__div", which each handle subtraction, multiplication and division respectively.

Exercises
2.1 Have you ever encountered something similar to metatables in other languages? How are they similar? How do they differ.

2.2 The code snippet in example 2.2 has a rather embarrassing, but subtle flaw in it. Experiment with it and try to find, and correct this design error.



3. The Access Metamethods


3.1 How Data Access Works in Lua Tables
If you recall, we earlier stated that tables are associative arrays. This means every value in them is paired up with a key. These keys can be numerical (like a normal array), or string-based (like a map). All this should be at least vaguely familiar. What may not be so prosaic though, is the metamethods behind these structures.

3.2 Meeting the Indexers
Now, answer this question for me: "What happens if you try to access a key that's not in the table"? For example:
local t = {x = 3, y = 7}
print(t.x); -- ok, x is a key
print(t.z); -- z is not in our table!


If you are not absolutely sure, then a quick review of tables is key here. Any good Lua tutorial covers these, so look there if needed (See references [5], and [6]). As for the answer, whenever Lua tries to access a table, and can't find a key, it creates the index event. This event causes a metamethod to be called and handle the unknown request.

The second metamethod behind tables is called when a new index event happens. To put it bluntly, a new index happens everytime a table is modified (e.g. new keys are added). As to not smother the reader with boring and rather useless nitty-gritty details, we'll move onto the actual metamethods and using them. The more zealous can find good discussions of all these things in the Official Lua Documentation[7].

Now, the two metamethods we'll be dealing with are called—and this should be no surprise—"__index" and "__newindex". It is obvious what each one does, so we won't waste time explaining. Instead, we'll start with a good example that will clarify anything uncertain at this point. We begin with this:
-- Example 3.1
-- Demonstrating the __index and __ newindex
-- metamethods. Explanation is in accompanying 
-- text.

mt = {}
local digits = {1,2,3,4,5,6,7,8,9,0}

function mt.__index(table, key)
    if key == "length" then
        local str = ""; -- empty string
        return str..#table
    end
end

function mt.__newindex(table, key, value)
        print("Added value "..value.." at key "..key); 
end

setmetatable(digits, mt);

-- Example of __index 
print(digits.length); -- length is not a member of digits

-- Example of __newindex.
digits["10"] = 4;


We begin as usual by making our two tables. Next we declare and define a function (part of mt)—__index. As you can easily see, it takes two parameters. The first is the table that is being indexed, and the second is the actual unknown key. This routine is nothing except an if statement—as it should be. In general, the __index function always takes the form of a switch statement. That is, it goes through a series of checks to try and see how it should handle this unknown request. All of this is of course, up to you.

The second metamethond we define is __newindex. Its signature is identical to __index, but it takes an additional specifier, the value to add (and this makes great sense). We kept ours very simple, and thus it only prints out some information to the user.

We finish up our trifling example by demonstrating these two methods in action. First we try to print out the value of length. length is not a member of digits, so an index event is created. __index is called, and it looks to see if it can handle it, and it can. In this case, it returns the value of #digits as a string. After that, we go onto modify digits, by adding a new key and value. This generates a new index event, and __newindex is called. All she does is give us some information at the console. Nifty nevertheless.

Exercises
3.1 Here's a challenge for you. Using __index and __newindex, try to make a table constant and unmodifiable. As a hint, take notice that you can save state with __index.



4. Conclusion
As we come to the end of this article, it's time to reflect upon what has been taught. At first, these methods may seem somewhat inane—and they are. The truth is, by themselves, metamethods are nothing but event-handling functions. However, it is when combined with other programming techniques, that their power is at its zenith.

Unfortunately, this is a topic for another discussion. That said, you are now fully prepared for such topics. Armed with a good knowledge of metatables, you are now ready to enter the realm of true Lua power.




References and External Links

[1] "Lua (programming Language)." Wikipedia, the Free Encyclopedia. Web. 30 May 2010. <http://en.wikipedia.org/wiki/Lua_(programming_language)>.
[2] "Meta." Wikipedia, the Free Encyclopedia. Web. 30 May 2010. <http://en.wikipedia.org/wiki/Meta>.
[3] "Lua-users Wiki: Metamethods Tutorial." Lua-users.org. Web. 30 May 2010. <http://lua-users.org/wiki/MetamethodsTutorial>.
[4] Function,, Using This. "Lua 5.1 Reference Manual." The Programming Language Lua. Web. 30 May 2010. <http://www.lua.org/manual/5.1/manual.html#pdf-setmetatable>.
[5] KYA. "Introduction To Lua - Other Language Tutorials | Dream.In.Code." Programming and Web Development Help | DreamInCode.net. Web. 30 May 2010. <http://www.dreamincode.net/forums/topic/98241-introduction-to-lua/>.
[6] "Lua-users Wiki: Tutorial Directory." Lua-users.org. Web. 30 May 2010. <http://lua-users.org/wiki/TutorialDirectory>.
[7] "Lua: Documentation." The Programming Language Lua. Web. 30 May 2010. <http://www.lua.org/docs.html>.

Is This A Good Question/Topic? 3
  • +

Page 1 of 1