Lua Classes With Metatable

lua-users home
wiki

This page shows how to implement classes in Lua using metatables. The examples below work in both Lua 5.0 and 5.1.

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
> 

See Also


RecentChanges · preferences
edit · history
Last edited October 18, 2008 11:32 pm GMT (diff)