9.8 Metamethods

Metamethods can be used to define how Hollywood's operators shall behave when used with tables. Normally, you cannot use any of Hollywood's operators with tables as operands. For example, the following is not possible:

table_A = {1, 2, 3, 4, 5}
table_B = {5, 4, 3, 2, 1}
result = table_A + table_B   ; generates compiler error!

The code above tries to add table_A to table_B but this does not work because tables may contain any random data (functions, subtables, strings, etc.) so there is no generic way of saying how the add operator should behave on a table. This is where metamethods come into play. Metamethods allow you to define how an operator shall behave when it receives a table operand. In other words, metamethods allow you to define a function that gets executed whenever an operator is used with a table operand. This function then computes the result and it is called a metamethod.

Metamethods are not a global setting but they are private to every table. When you create a table it will not have any metamethods attached. Thus, trying to use an operator on this table will fail because it does not have any metamethods. To assign metamethods to a table you need to use the SetMetaTable() command. A metatable is a table containing a set of metamethods. SetMetaTable() accepts two table argument: The first argument is the table whose metamethods you would like to set, and the second argument is the actual metatable, i.e. the table that contains the metamethods that you would like to set.

Let's have a look at an example now. We will rewrite the code from above using metamethods so that we can add the two tables.

mt = {}  ; create our metatable

Function mt.__add(a, b)
    Local sizeA = ListItems(a)   ; number of elements in table A
    Local sizeB = ListItems(b)   ; number of elements in table B
    Local result = {}            ; create resulting table

    For Local k = 0 To Min(sizeA, sizeB) - 1
       result[k] = a[k] + b[k]                 ; add elements

    Return(result)   ; return resulting table

table_A = {1, 2, 3, 4, 5}
table_B = {5, 4, 3, 2, 1}

SetMetaTable(table_A, mt)   ; set "mt" as table_A's metatable
result = table_A + table_B

The resulting table will have five elements that are all set to 6. Now what did we do in the code above? We first create an empty table that serves as our metatable. Then we add a function called __add (using two underscores) to that table. This function will be the metamethod for the + operator. Note that we must use the name __add for this function because Hollywood uses the function name to detect the operator that is served by the metamethod. Using __add as name defines a metamethod for the add (+) operator. The code in our metamethod simply calculates the length of the two tables, adds the table elements, and stores them in a resulting table that it returns.

Note that the implementation of our __add metamethod above requires that both arguments are tables. And the tables must only contain numbers (or strings that can be converted to numbers). E.g. the following expressions would not work using the above metamethod implementation:

result = table_A + 10       ; --> error because "10" is not a table
result = table_A + "Hello"  ; --> same error

Of course, it is possible to write metamethods which can handle these situations. You would just have to check the types of the parameters that are passed to your metamethod and then you can take custom actions depending on the variable types specified.

Now we have covered the metamethod for the add (+) operator only. Of course, you can set a metamethod for every other Hollywood operator, too. You can also create metamethods for all relational operators (= <> < > <= >=) so that you can compare tables directly. All you need to know is the correct name for the metamethod of the operator so that you can install it. Here is a list of all available metamethods and to their corresponding operators:

As you can see, there are no metamethods for the >, >=, and <> operators. This is because Hollywood handles them by simply reformulating the condition in the following way:

a <> b             is the same as       Not (a = b)
a > b              is the same as       b < a
a >= b             is the same as       b <= a

If you would like to compare two tables that both have associated metatables, but you would like to compare them without invoking the __eq metamethod, you have to use the RawEqual() function. This function will compare both tables just by reference without invoking any metamethod.

Differing metatables with binary operators

As you have seen above every table has its own private metatable setting. When using binary operators, however, it could happen that the two operands do not use the same metatable but different ones. So how does Hollywood choose the metatable for the operator now? This depends on several conditions:

  1. If the operator is a relational operator (= <> < <= > >=), the metamethod will only be called if the two tables that shall be compared use the same metatable. If they have different metatables, the comparison will fail.

  2. If the operator is an arithmetic operator, a bitwise operator, or the string concatenation operator (..), Hollywood will first look in operand A. If operand A has a metatable then this metatable will be used. If operand A does not have a metatable Hollywood will look in operand B. If operand B has a metatable it will be used. If neither operand has a metatable an error message will be raised.

Limitations of the relational metamethods

You have already read above that the relational metamethods will only be called if the two operands use the same metatables. However, there is another limitation when using relational metamethods: They will only be called if the two operands are tables. It is not possible to compare a table with a number, or comparing a string with a table, etc. The arithmetic and bitwise metamethods can be made to work with any variable type but the relational metamethods are limited to comparisons of tables.

Advanced metamethods

So far we have only covered the relational, arithmetic, bitwise and concatenation metamethods. There are, however, a few more metamethods that you can use, namely __index, __newindex, __call and __tostring. Here is a detailed description of these metamethods:

This metamethod is called whenever you try to read from a table index that does not exist. This metamethod could be used to create a default value for all uninitialized table fields. Normally, Hollywood will fail when you read from uninitialized fields. This behaviour could be changed using this metamethod. Here is a code snippet that sets the default value to 0:

mt = {}
Function mt.__index(t, idx)

t = {x = 10, y = 20}
SetMetaTable(t, mt)
NPrint(t.x, t.y, t.z)  ; --> prints 10 20 0

Without our metatable, the call to NPrint() would fail because z has not been initialized. By using the metatable, however, z will automatically fall back to 0 because it does not exist.

Sometimes it might become necessary to read from a table without invoking any metamethod. You can do this using the RawGet() function. RawGet() will never invoke any metamethod. If an index does not exist it will return Nil to you.

This metamethod is called whenever you try to write a value to a table index that does not yet exist in the table. You could use this metamethod for example to create tables that are read-only. The following code will issue an error whenever you try to write to a protected table:

mt = {}
Function mt.__newindex(t, idx, val)
   NPrint("Blocked writing", val, "at index", idx)

t = {x = 10, y = 20}
SetMetaTable(t, mt)
t.z = 45    ; --> "Blocked writing 45 at index z"

The code above sets table t as write-protected. You will not be able to make any modifications to the table.

Sometimes it might become necessary to write to a table without invoking any metamethod. You can do this using the RawSet() function. RawSet() will never invoke any metamethod. You could even write to write-protected tables using the RawSet() function.

This metamethod is called whenever you try to call a table. Normally, trying to call a table will fail because tables are obviously just types of data storage and not a function. However, there are cases where it can come handy if you could also call a table. The following example demonstrates a metamethod that will calculate the average of all table values:

mt = {}
Function mt.__call(t)
   Local c = ListItems(t)
   Local sum = 0

   For Local k = 0 To c - 1 Do sum = sum + t[k]

   Return(sum / c)

t = {10, 23, 45, 5, 107, 45, 18, 46}
SetMetaTable(t, mt)
NPrint(t())   ; --> 37.375

The code above will return 37.375 which is the average of the eight values stored in table t.

This metamethod is used by commands like Print() or DebugPrint(). Normally, when you pass a table to Print() you will receive something as "Table: 1acd432f" as the output. This is the handle Hollywood uses internally to refer to the table and is obviously of not much use for you. Using the __tostring metamethod, however, you can easily change this behaviour. Here is a metamethod which creates a string representation of a table:

mt = {}
Function mt.__tostring(t)
   Local r$
   For Local k=0 To ListItems(t)-1 Do r$=r$..t[k].." "

t = {"Jeff", "Andy", "Mike", "Dave"}
SetMetaTable(t, mt)
NPrint(t)   ; --> Jeff Andy Mike Dave

The code above prints "Jeff Andy Mike Dave" because our __tostring metamethod has simply concatenated all elements of the table.

Show TOC