Topic 09-meta.md
Metatables
Any table or userdata may have a metatable associated with it. Metatables specify the behaviour of their tables, in a similar way to how classes specify the behaviour of their objects in a traditional object-orientated language. Metatables do this by defining metamethods. In other respects, metatables are plain tables.
Custom String Representation
tostring will try to convert a value into a string; it is used by print when presenting values. It is not particularly useful for tables,
obj = {label = "hello"} print(obj) --> table: 0x80e91f8
tostring does let an object decide how to show itself. If the object has a metatable,
and that metatable contains a function called __tostring
, it will use that function to
get the string:
local MT = {} MT.__tostring = function(obj) return obj.label end setmetatable(obj,MT) print(obj) --> hello
A more realistic example involves making tables print out their contents. In the specialized case of an array or list, it would look like this:
List = {} List.__tostring = function(list) local res = {} for i = 1,#list do res[i] = tostring(list[i]) end return '{'..table.concat(res,',')..'}' end function new_list(t) return setmetatable(t or {},List) end l1 = new_list{10,20,30} print(l2) ---> {10,20,30}
This is genuinely useful when working with lists. The __tostring
implementation is
a little more complicated than it should be, because table.concat itself does not use
tostring and needs a table of strings or numbers. (This is actually deliberate, since
table.concat is the way to build large strings for output in Lua. If you make a mistake
and pass a table value or nil
, then it is better for this to be a runtime error than
to have to look through ten pages of output for the mistake.)
Making a Table Callable and Overriding Equality
The ‘constructor’ new_list
takes an existing table, or makes a new table. (t or {}
is
a common Lua idiom for specifying a default value for the case when something may
be nil
.) It would be more elegant if List
could be used as a constructor, like
List{10,20,30}
. The __call
metamethod makes a table callable. When you try
to ‘call a table’ then Lua looks for a function with this name in the metatable, passing
the original table:
setmetatable(List,{ __call = function(C,t) return new_list(t) end }) l2 = List{10,20,30} print(l2) --> {10,20,30}
It would be useful if lists could be compared element-by-element when using the
equality operator ==
. By defining the metamethod __eq
, the usual behaviour
is overriden.
List.__eq = function(list1,list2) if #list1 ~= #list2 then return false end for i = 1,#list1 do if list1[i] ~= list2[i] then return false end end return true end print(li1 == l2) --> true
List
is starting to resemble what people call a class in other languages; it serves both
as a factory for making new List
objects, and defines shared behaviour in all these
objects.
When a Key is not Found in a Table
The basic table access operations using a key are getting and setting values. If a key
is not found in the table, then nil
is returned. Sometimes this is not an appropriate
default value, or you want this case to really be an error. The __index
metamethod
is called if Lua cannot find a key in a table. It is passed the table and the key:
local ErrorMT = {} function ErrorMt.__index(t,key) error("Cannot find '"..key.."' in table",2) end function new_strict_table(t) return setmetatable(t or {},ErrorMT) end t = new_strict_table {fred = 1} --> case-sensitive! Throws an error "Cannot find 'Fred' in table" print(t.Fred)
Consider counting unique words in a table:
for i = 1,#words do local word = words[i] local count = count_of[word] if not count then count_of[word] = 0 end count_of[word] = count + 1 end
Having to check for the first occurance of a word is irritating; if the key is not found, the value should be zero.
ZeroMT = { __index = function(t,key) return 0 end }
Then tables with this metatable can be used like this:
for i,word = ipairs(words) do count_of[word] = count_of[word] + 1 end
Going back to the List
example, it would be nice if lists had some methods. The extend
method
will append all the elements of another table:
local methods = {} List.__index = function(list,k) -- *note* return methods[k] end function methods.extend(self,list) local k = #self for i = 1,#list do k = k + 1 self[k] = list[i] end return self end ls = List{10,20,30} ls:extend {40,50} print(ls) --> {10,20,30,40,50}
That is, if we can’t find a key such as extend
then __index
assumes that it will be inside
a table of methods.
(This is such a common pattern that Lua allows __index
to be a table, so you can write the
marked line as:
List.__index = methods
A little less flexible, but faster and cleaner.)
It’s easy to keep adding methods. For instance, this works like string.sub , where the second index can be negative to refer to the end of the list:
function methods.sub(self,i1,i2) if i2 == nil then i2 = #self elseif i2 < 0 then i2 = #self + i2 + 1 end local res = List() for i = i1,i2 do res[#res + 1] = self[i] end return res end print(ls:sub(1)) --> {10,20,30,40,50} print(ls:sub(2,-2)) --> {20,30,40}
Such a section of a list is usually called a ‘slice’. Note that calling with a start index of 1 gives us a copy of the full list.
To make this list even more Python-like, we can implement concatenation. This is the new list
made from appending the second list to the first list. l1 .. l2
can be made to do this if
the __concat
metamethod is defined:
List.__concat = function(l1,l2) local res = self:sub(1) --- make a copy return res:extend(l2) --- and append l2 end print(List{10,20} .. List{30,40} .. List{50}) --> {10,20,30,40,50}
To recap: the behaviour of any table can be changed by giving it a metatable, which contains
metamethods. The most important of these is __index
which allows you to handle the case
where a key is not found in the table. It may simply point to a table which is used as the
‘fallback’ table which is examined after the first lookup fails. This is a common way to implement
object-oriented programming in Lua, allowing the methods for all objects of a given ‘class’
to be stored in one place.
You could define l1 + l2
to mean this using the metamethod __add
, but Lua programmers
would not expect addition to work like that. They would expect that l1 + l2
is the list made
by adding the corresponding elements of the two lists (‘element-wise’), and it would
of course only work with lists where the elements could be added.
When a new Key is Initialized
The operation of setting a value can also be customized. __newindex
works like
_index
; it is only called if the key is not in the table. It receives three arguments,
the table itself, the key and the new value.
A source of frustration is Lua’s ‘undefined is nil
’ behaviour. Spelling mistakes become nil,
which might crash immediately – that’s the good outcome. Or these nils travel around and blow
up in some other part of the program. Fortunately, Lua provides the mechanism, you decide the policy.
We would still like to create global variables deliberately, so __newindex
can make a
reference when a new variable is created (even if it is nil
). It must actually set the
value using rawset .
Then __index
merely has to check whether the variable has been already declared. In fact,
we only need to keep those variables which are initially nil
in declared
.
function strict (T) local declared = {} local mt = {} mt.__newindex = function(t,key,value) declared [variable] = true rawset (t,key,value) end mt.__index = function(t,variable) if not declared [variable] then error("variable '"..variable.."' is undeclared",2) end end return setmetatable(T,mt) end strict (_G) print (prnt) --> error: variable 'prnt' is not declared x = nil -- ok, because it's declared print(x) --> nil
No reason why we cannot use this on the standard library tables as well!
strict(math) print(math.sine(1.2)) --> error: variable 'sine' is not declared
What if you want __index
and __newindex
to be always called? Then the keys by definition
cannot be in the table itself; this is the important point about these metamethods – they only
fire if the key does not already exist in the table. (Earlier in the history of Lua these functions
were called fallbacks for this reason.)
Instead, the table must act as a proxy for another table. Say we
have an array and want to raise a ‘out of bounds’ error instead of just silently returning nil
.
local ProxyListMT = {} function new_array(arr) return setmetatable({_data = arr},ProxyListMT) end function ProxyListMT.__index(self,k) if k < 1 or k > #self._data then error("out of bounds",2) end return self._data[k] end function ProxyListMT.__newindex(self,k,value) if k < 1 or k > #self._data then error("out of bounds",2) end self._data[k] = value end
That is, the object just contains a _data
field which refers to the actual table; any time we
access an array element, it must go through __index
because there are no array elements
in the object itself; it is acting as a proxy for the table. The object behaves like a
non-resizable array. It is naturally not quite as efficient, but it is often more important
to be correct than to be fast.