Simple Lua Classes

lua-users home
wiki

A Simplified Way to Declare Lua Classes

Out of the box, Lua does not have a class system, but its powerful metaprogramming facilities makes defining classic objects straightforward. In fact, there's a number of ways of doing this; together with the unfamiliar notation, this makes object-orientation in Lua seem a little intimidating at first.

The method described here is the most common and flexible method, using metatables. A table's behavior can be customized by giving it a metatable. For instance, if the metatable has an __index function, then any failed attempt to look up something in the table will be passed to __index. If __index is itself a table, the symbol will be looked up in that table. (Please see the excellent discussion in PiL [1]) Here is the basic idea:

Account = {}
Account.__index = Account

function Account:create(balance)
   local acnt = {}             -- our new object
   setmetatable(acnt,Account)  -- make Account handle lookup
   acnt.balance = balance      -- initialize our object
   return acnt
end

function Account:withdraw(amount)
   self.balance = self.balance - amount
end

-- create and use an Account
acc = Account:create(1000)
acc:withdraw(100)

Here, Account objects are represented by tables, which contain precisely one field, the balance. Lua tries to look up withdraw in acc, and cannot find it. Because acc has a metatable that defines __index, it will then look up withdraw in that metatable. So acc:withdraw(100) is actually the call Account.withdraw(acc,100). We could have actually put withdraw directly into acc, but this would be wasteful and inflexible - adding a new method would require a change to the create function, etc.

A simplified way of creating classes

I'll define a function class which does all this (and more) transparently.

Account = class(function(acc,balance)
              acc.balance = balance
           end)

function Account:withdraw(amount)
   self.balance = self.balance - amount
end

-- can create an Account using call notation!
acc = Account(1000)
acc:withdraw(100)

In this scheme, one supplies an initialization function to the new class, and a 'constructor' is automatically generated.

Simple inheritance is supported. For example, here a base class Animal is defined, and several specific kinds of animals are declared. All classes made using class have a is_a method, which you can use to find out the actual class at runtime:

-- animal.lua

require 'class'

Animal = class(function(a,name)
   a.name = name
end)

function Animal:__tostring()
  return self.name..': '..self:speak()
end

Dog = class(Animal)

function Dog:speak()
  return 'bark'
end

Cat = class(Animal, function(c,name,breed)
         Animal.init(c,name)  -- must init base!
         c.breed = breed
      end)

function Cat:speak()
  return 'meow'
end

Lion = class(Cat)

function Lion:speak()
  return 'roar'
end

fido = Dog('Fido')
felix = Cat('Felix','Tabby')
leo = Lion('Leo','African')

D:\Downloads\func>lua -i animal.lua
> = fido,felix,leo
Fido: bark      Felix: meow     Leo: roar
> = leo:is_a(Animal)
true
> = leo:is_a(Dog)
false
> = leo:is_a(Cat)
true

All Animal does is define __tostring, which Lua will use whenever a string representation is needed of the object. In turn, this relies on speak, which is not defined. So it's what C++ people would call an abstract base class; the specific derived classes like Dog define speak. Please note that if derived classes have their own initialization functions, they must explicitly call init for their base class.

Implementation of class()

class() uses two tricks. It allows you to construct a class using the call notation (like Dog('fido') above) by giving the class itself a metatable which defines __call. It handles inheritance by copying the fields of the base class into the derived class. This isn't the only way of doing inheritance; we could make __index a function which explicitly tries to look a function up in the base class(es). But this method will give better performance, at a cost of making the class objects somewhat fatter. Each derived class does keep a field _base that contains the base class, but this is to implement is_a.

Note that modification of a base class at runtime will not affect its subclasses.

-- class.lua
-- Compatible with Lua 5.1 (not 5.0).
function class(base, init)
   local c = {}    -- a new class instance
   if not init and type(base) == 'function' then
      init = base
      base = nil
   elseif type(base) == 'table' then
    -- our new class is a shallow copy of the base class!
      for i,v in pairs(base) do
         c[i] = v
      end
      c._base = base
   end
   -- the class will be the metatable for all its objects,
   -- and they will look up their methods in it.
   c.__index = c

   -- expose a constructor which can be called by <classname>(<args>)
   local mt = {}
   mt.__call = function(class_tbl, ...)
   local obj = {}
   setmetatable(obj,c)
   if init then
      init(obj,...)
   else 
      -- make sure that any stuff from the base class is initialized!
      if base and base.init then
      base.init(obj, ...)
      end
   end
   return obj
   end
   c.init = init
   c.is_a = function(self, klass)
      local m = getmetatable(self)
      while m do 
         if m == klass then return true end
         m = m._base
      end
      return false
   end
   setmetatable(c, mt)
   return c
end

Comments

If this change is made:

--- class_orig.lua      2009-07-24 20:53:25.218750000 -0400
+++ class.lua   2009-07-24 20:53:49.734375000 -0400
@@ -21,8 +21,8 @@
   mt.__call = function(class_tbl,...)
     local obj = {}
     setmetatable(obj,c)
-    if ctor then
-       ctor(obj,...)
+    if class_tbl.init then
+       class_tbl.init(obj,...)
     else
     -- make sure that any stuff from the base class is initialized!
        if base and base.init then

then we alternately may declare classes in this way:

A = class()
function A:init(x)
  self.x = x
end
function A:test()
  print(self.x)
end

B = class(A)
function B:init(x,y)
  A.init(self,x)
  self.y = y
end

BTW, you may note that class.lua also works for operators:

function A:__add(b)
  return A(self.x + b.x)
end

"init" may alternately be renamed "__init" since it is a private function like __add and resembling Python's __init__ [2].

--DavidManura

In the original version, the argument called 'ctor' is actually the init method, as shown by the (previous) statement
c.init = ctor
. I changed this argument's name to 'init'. --DeniSpir

An alternative class builder (tested with lua 5.3) --ThaBubble
function class(def)
    local class = {}
    local parents = {}
    
    local upv
    local env = _G
    
    local wraps
    local function super(parent_class)
        if not parent_class then
            parent_class = parents[1]
        end
        
        local this = this
        local that = {}
        for k,v in pairs(parent_class) do
            that[k] = type(v) == 'function' and wraps(this, v) or v
        end
        
        return setmetatable(that, that)
    end
    
    function wraps(this, func)
        return function(...)
                local t = env.this
                local s = env.super
                
                env.this  = this
                env.super = super
                
                local ret  = pcall(func, ...)

                env.this  = t
                env.super = s
                
                return ret
            end
    end
    
    function class.__init()end

    for i=1,math.huge do
        inherit, v = debug.getlocal(def, i)
        if not inherit then break end
        
        local parent_class = _G[inherit]
        for i=1,math.huge do
            local  name, pclass = debug.getlocal(2,i,1)
            if not name then break
            elseif name == inherit then
                parent_class  = pclass
                break
            end
        end

        if parent_class  and type(parent_class) == 'table' then
            table.insert(parents, parent_class)
            for k,v in pairs(parent_class) do
                class[k] = v
            end
        else
            error(string.format('Class "%s" not valid.', name))
        end
    end
    
    
    for i=1,math.huge do
        local  name, value = debug.getupvalue(def, i)
        if not name then break
        elseif name == '_ENV' then
            env = value
            upv = i
            break
        end
    end

    local _env = setmetatable({}, {
        __index= function(t, name)
            local  value  = class[name]
            return value ~= nil and value or env[name]
        end,
        __newindex = function(t, name, value)
                class[name] = value
            end
    })
    
    local function senv(env)
        if upv then debug.setupvalue(def, upv, env)
        else _G = env end
    end
    
    senv(_env)
    env.pcall(def, env.table.unpack(parents))
    senv(env)
    
    return setmetatable({}, {
        __ipairs    = function()        return ipairs(class)              end,
        __pairs     = function()        return  pairs(class)              end,
        __index     = function(t, name) return        class[name]         end,
        __index_new = function(t, name, value)        class[name] = value end,
        __call      = function(...)
            local this = {}
            for k,v in pairs(class) do
                this[k] = type(v) == 'function' and wraps(this, v) or v
            end
            this.__class = class
            this.__init(...)
            
            return setmetatable(this, this)
        end
    })
end

Example:
global  = true
Inherit = class(function()
    this_is_a_property_of_Inherit = true
    
    function __init()
        print('Inherit().__init()')
        this.init = true
    end
    
    function __call()
        print('Yay! You\'re calling for me :) init:', this.init, '\n')
    end
end)

Example = class(function(Inherit)
    print('Inherited property:', this_is_a_property_of_Inherit)
    print('Global variable:   ', global, '\n')
    
    function __init()
        print('Example().__init()')
        super().__init()
        print('this.init:', this.init)
    end
    
    function test(...)
        print(..., this.__init, '\n')
    end
end)

example = Example()
example.test('__init:')
example()

example.property = 'I\'m a property of instance "example"'
print('example.property', example.property)
print('Example.property', Example.property)

--    Inherited property:	true
--    Global variable:   	true	

--    Example().__init()
--    Inherit().__init()
--    this.init:	true
--    __init:	function: 0x15dd5f0	

--    Yay! You're calling for me :) init:	true	

--    example.property	I'm a property of instance "example"
--    Example.property	nil

See Also


RecentChanges · preferences
edit · history
Last edited June 6, 2017 7:22 pm GMT (diff)