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 10:32 pm GMT (diff)