Lua Classes With Metatable |
|
Lua has matured from an application extension language into a wonderfully flexible scripting language. Lua 5.0 is not an object oriented language like Java or Ruby. Instead, Lua gives you the ability to implement classes however you wish. This is both a bonus and a bane. Power users love the freedom, but newbies are sometimes baffled.
Metatables are Lua's way of adding magic to tables. Let us assume that
t
is a regular table like so:
Lua 5.0.3 Copyright (C) 1994-2006 Tecgraf, PUC-Rio > t = { 11, 22, 33, you='one', me='two' } > table.foreach(t,print) 1 11 2 22 3 33 me two you one > > = t[2] 22 > = t.me two > = t.fred nil
Indexing the table with a valid index returns the value stored at that index.
Indexing the table with an undefined index returns nil
.
If we add some magic,
we can try indexing another table instead of returning nil
.
We can even provide our own function to handle the undefined indexes
however we wish.
These functions that customize certain Lua behaviour were called fallbacks in the first versions of Lua. In Lua 4.0 they were called tag methods. Now in Lua 5.0 (thanks largely to Edgar Toernig) these function are called metamethods and they are stored in tables called metatables.
The behaviours that we can customize have special names and are referred to as events. Adding a new index to a table is called the newindex event. Attempting to read an undefined index from a table is called the index event.
To see what happens when we access an undefined index, lets print out the arguments which are passed to the metamethod for the index event.
Lua 5.0.3 Copyright (C) 1994-2006 Tecgraf, PUC-Rio > t = { 11, 22, 33, you='one', me='two' } > mt = { __index = print } > = t.you one > = t.fred nil > setmetatable(t, mt) > x = t.fred table: 0x8075e80 fred > = x nil > = t table: 0x8075e80 >
Notice that the first argument is the table t
and the second argument is
the index fred
.
If we do the same for the newindex event, we see that there is a third argument which is the new value to be stored at the index.
Lua 5.0.3 Copyright (C) 1994-2006 Tecgraf, PUC-Rio > t = { 11, 22, 33, you='one', me='two' } > mt = { __newindex = print } > setmetatable(t, mt) > t[4] = 'rat' table: 0x8075e80 4 rat >
As mentioned earlier, we can specify a table instead of a function, and that table will be accessed instead.
Lua 5.0.3 Copyright (C) 1994-2006 Tecgraf, PUC-Rio > t = { 11, 22, 33, you='one', me='two' } > s = { } > mt = { __newindex = s, __index = _G } > setmetatable(t, mt) > = t.you one > x = 'wow' > = t.x wow > t[5] = 99 > table.foreach(s, print) 5 99 >
The following shows how to implement a class of vectors. We have one table for the methods, and one metatable. There is an additional table for each object. All object share the same table of methods and the same metatable.
Remember that v1:mag()
is like v1.mag(v1)
, so Lua tries
to lookup mag
in v1
, which will trigger the index
event, which
then lookups mag
in the table Vector
.
Lua 5.0.3 Copyright (C) 1994-2006 Tecgraf, PUC-Rio > Vector = {} > Vector_mt = { __index = Vector } > > function Vector:new(x,y) >> return setmetatable( {x=x, y=y}, Vector_mt) >> end > > function Vector:mag() >> return math.sqrt(self:dot(self)) >> end > > function Vector:dot(v) >> return self.x * v.x + self.y * v.y >> end > > v1 = Vector:new(3,4) > table.foreach(v1,print) y 4 x 3 > = v1:mag() 5 > v2 = Vector:new(2,1) > = v2:dot(v1) 10 > > = Vector table: 0x8076028 > table.foreach(Vector,print) mag function: 0x8078008 dot function: 0x8078b58 new function: 0x80773e8 > = v1, v2 table: 0x8079110 table: 0x8079a80 > = Vector_mt, getmetatable(v1), getmetatable(v2) table: 0x80763b8 table: 0x80763b8 table: 0x80763b8 > table.foreach(Vector_mt,print) __index table: 0x8076028 >
If you want a default constructor and a copy constructor, you can create a
file called Class.lua
as follows:
function Class(members) members = members or {} local mt = { __metatable = members; __index = members; } local function new(_, init) return setmetatable(init or {}, mt) end local function copy(obj, ...) local newobj = obj:new(unpack(arg)) for n,v in pairs(obj) do newobj[n] = v end return newobj end members.new = members.new or new members.copy = members.copy or copy return mt end
Then put our Vector class in a file called Vec.lua
:
require'Class' Vector = {} local Vector_mt = Class(Vector) function Vector:new(x,y) return setmetatable( {x=x, y=y}, Vector_mt) end function Vector:mag() return math.sqrt(self:dot(self)) end function Vector:dot(v) return self.x * v.x + self.y * v.y end
Then test it as follows:
$ lua -lVec -i -v Lua 5.0.3 Copyright (C) 1994-2006 Tecgraf, PUC-Rio > v1 = Vector:new(3,4) > table.foreach(v1,print) y 4 x 3 > = v1:mag() 5 > v2 = Vector:new(2,1) > = v2:dot(v1) 10 > > table.foreach(Vector,print) copy function: 0x80692c0 dot function: 0x8069300 mag function: 0x80692e0 new function: 0x8069398 > > v3 = v1:copy() > = v1, v2, v3 table: 0x80779d0 table: 0x8078428 table: 0x807a050 > table.foreach(v1,print) y 4 x 3 > table.foreach(v3,print) y 4 x 3 >
If we apply the Class
function to Lua's table lib, we can create table
objects.
require'Class' Class(table) function table:push(x) assert( x ~= nil, 'will not push nil into table') self:insert(x) return self, x end function table:map(func, ...) local R = table:new{} for name,value in pairs(self) do func(R,name,value,unpack(arg)) end return R end function table:imap(func, ...) local R = table:new{} for index,elem in ipairs(self) do func(R,index,elem,unpack(arg)) end return R end
Then you no longer have to type table.foreach
or table.getn(t)
.
$ lua -lTable -i -v Lua 5.0.3 Copyright (C) 1994-2006 Tecgraf, PUC-Rio > t = table:new{ 11, 22, 33, you='one', me='two' } > = t:getn() 3 > t:foreach(print) 1 11 2 22 3 33 me two you one > > = t:concat',' 11,22,33 > = table table: 0x8067808 > = getmetatable(t) table: 0x8067808 > > s = t:copy() > s:foreach(print) 1 11 2 22 3 33 me two you one > = s, t table: 0x8079a58 table: 0x8077bb8 >