Metatables

Under construction

General

Metatables

Metatables are not a special table type but standard lua tables; they are called metatables because they are used to hold metadata (data about data) and metamethods for other types. Types like string share one common string metatable; tables and userdata however can have individual metatables that can be created and adapted by the scriptor. This page deals exclusively with the metatables for tables. These can be used to add additional functionality to the standard table(s) they are assigned to. Userdata falls outside the target group for this site; this type can only be created by the C/C++ programmers of (in our case) the Celestia development team.

A metatable is a standard lua table that holds references to data and functions. It does not become a metatable until it is assigned as such to a "normal" lua table with the setmetatable(table, metatable) function, like for example:

metatable = {}
metatable.__add = function(a, b) ...... end
metatable.__sub = function(a, b) ...... end
normaltable = { 10, 20, 30, 40 }
setmetatable(normaltable, metatable)

multiple tables can share one metatable or they can each have their own individual metatable and everything inbetween. Lua calls the keys in a metatable events and the values metamethods. In the previous example, the events are "add" and "sub" and the __add() and __sub() metamethods are the functions.

Metamethods

Metatables hold metamethods; again nothing complicated, just straightforward lua functions. The "meta" is not in what they are but in what they are used for and "method" is just another word for function. Metamethods perform standard operations like addition, subtraction, concatenation and equality tests. If the lua interpreter for instance encounters:

  • sum = a + b
  • text = a..b
  • if a == b

it will conclude that it is dealing with an "add", a "concat" or an "eq" event respectively. It will then try to find the corresponding metamethod by prefixing the event by two underscores "__" and using the result as a key into a metatable. Which metatable this is, is defined by the type of the operands; in the above case: a and b. If both are numbers then lua will search the metatable for the number type, if it are strings, the metatable for the string type, etc. If it doesn't find a key corresponding to the event, it wil issue an error message; otherwise it will use the information to translate the scripted syntax to the correct method call. In our case:

  • sum = __add(a, b)
  • text = __concat(a, b)
  • if __eq(a, b)

Examples of other events/metamethods are: __sub(), __len() and __tostring(); for a complete list see: "Lua 5.1 Reference Manual", section 2.8 "Metatables".

Two special events are "index" and "newindex", that both are related specifically to tables. The index event is discussed in "Behaviour extention" further down. The newindex event is "raised" whenever a script tries to add an element - and thus a new index - to a table.

Links

Functionality override

The table type undoubtedly has it's own system metatable, since you can do all sorts of things with tables without ever assigning an indvidual metatable. It is therefore actually better to say, that individual metatables override the table type's default behaviour for specific events. Take the "eq" event for instance, the table's metamethod __eq(A, B) interpretes it's function as: "test if parameter A refers to the same table as parameter B does". This differs considerably from the more natural functionality of __eq() for types like number, string and boolean. The __eq() method for those types tests if the cóntents of A (it's value) is equal to the cóntents of B; nót if both refer to the same object in memory.

The example below shows the deviating functionality of the default __eq() method; the tables a and b are found to be equal, which is correct because both refer to the exact same table. The tables a and c on the other hand are found to be nót equal although any normal human being would conclude that they are.

> a = { 10, 20, 30 }
> b = a
> c = { 10, 20, 30 }
> print("a = ", a)
> print("b = ", b)
> print("c = ", c)
> if a == b then print("a == b") else print("a ~= b") end
> if a == c then print("a == c") else print("a ~= c") end
a =         table: 00000000001FBDF0
b =         table: 00000000001FBDF0
c =         table: 00000000001FBCB0
a == b
a ~= c

You can override this behaviour of the "eq" event by overriding the functionallity of it's default __eq() metamethod by that of your own individual __eq() metamethod. That doesn't take that much doing, like is shown below.

> eqmt = {}                                             -- the metatable

> eqmt.__eq = function(a, b) -- the __eq() metamethod

>                 if #a ~= #b then                      -- if not equal length
>                     return false
>                 else                                  -- check each key, value pair
>                     for key, value in pairs(a) do
>                       if b[key] ~= value then
>                           return false
>                       end
>                     end
>                 return true
>                 end
>             end
> a = { 10, 20, 30 }
> b = a
> c = { 10, 20, 30 }
> print("a = ", a)
> print("b = ", b)
> print("c = ", c)
> setmetatable(a, eqmt)
> setmetatable(c, eqmt)
> if a == b then print("a == b") else print("a ~= b") end
> if a == c then print("a == c") else print("a ~= c") end
a =         table: 00000000001FBDF0
b =         table: 00000000001FBDF0
c =         table: 00000000001FBCB0
a == b
a == c

With the eqmt.__eq() metamethod overriding the default __eq() method the tables a and c are now compared on content. This can be done with just about all table events, but it pays to be carefull since the original method is now not available anymore and you just míght need it. In certain cases it is might be a better idea to implement variations on metamethods as extention methods, as described in the next section.

Functionality extention

Extending a table's functionality is also a question of overriding default behaviour; in this case overriding the __index() metamethod of the index event; the most usefull of all. The index event is triggered when a script refers to an index or key that does not exist in the table; so actually when there is the question of an index- or key error. In pseudo code this is what happens when a key of say: "Pluto" can not be found in the Planets table:

if planet Pluto is not in the Planets table then
    if Planets table has a metatable attached then
        if Planets metatable has a key __index then
            if the type of the __index value is 'function' then
                return __index.function(Planets, Pluto)
            elseif the type of the __index value is 'table' then
                return __index.table[Pluto]
            else
                return nil
            end
        else

return nil

        end
    else

return nil

    end
else
    return Planets[Pluto]
end

This behaviour of the index event can be used to change lua's default behaviour to our advantage. The next example returns "default case" instead of nil when the table is queried with a non-existing key.

> listMetatable = {}
> listMetatable.__index = function(list, key)

> return "default case"

>                         end
> list = {}
> list["one"] = "special case 1"
> list["two"] = "special case 2"
> setmetatable(list, listMetatable)
> print("list element 'something' = ", list["something"])
list element 'something' = default case

The __index() method can do anything you want it to do and thus return anything you want. It gets both the queried table and the key value passed as function arguments. The example below is a small variation on the example above; it's index event returns either "no case" if an empty key was specified or "default case" if an unknown key was specified.

> listMetatable.__index = function(list, key)

>                             if key == nil or key == "" then
>                                 return "no case"
>                             else
>                                 return "default case"
>                             end
>                         end
> print("list element 'something' = ", list["something"])
list element 'something' = default case
> print("list element 'nil' = ", list[nil])
list element 'nil' = no case

The index event is an exeption in as far as it accepts a table as a 'metamethod'; it will query that table as if it were an extension of the actual table. In the example below such an "extention table" holds all the (default) values for the 'lableflags_1' table that itself only holds the exeptions for a certain case. You can use the extention table as a defaults container for as many lableflags tables as you like.

> labelflagsDefault = {
>     planets    = true,
>     moons      = true,
>     -- .....   = ...    => spacecraft, asteroids, etc.; see celx documentation for celestia:setlabelflags(...)
>     globulars  = false
>     }
> labelflagsMetaTable = {

> __index = labelflagsDefault

> }

> labelflags_1 = {
>     spacecraft = true,
>     minormoons = true
>     }

> setmetatable(labelflags_1, labelflagsMetaTable)

> print("Labelflag_1.planets = " ..tostring(labelflags_1.planets))
Labelflag_1.planets = true

! Notice:

    • Celestia's Celx does not honour metatables.
    • It would be nice if one could now do: celestia:setlabelflags(labelflags_1) but celestia completely ignores the metatable and therefore the labelflagsDefault extention table.
    • One can only concatenate strings and 'labelflag.planets' is a boolean; the 'tostring()' function can be used to convert most types into a string

List metatable

Metatables can be used to extend the default functionality of tables. Create a meta-/extention table once, add it to as many tables as you want and use the functionality of the metatable for all of them, without having to write one line of extra code. In Custom types a metatable is available that extends the default functionality of the Lua table type.