this article shows how to implement class-based oop in lua using its prototype-based system.

table - the data structure

first and foremost, always remember this famous quote from programming in lua:

tables in lua are not a data structure; they are the data structure.

therefore, a class is a table, an object is a table, and so is everything else.

metatable and metamethod

a table may have a metatable, which contains a group of functions, called metamethods. metamethods can change the behavior of a table. for example, you can add metamethod __add to allow table addition.

in fact, the core of lua’s oop is the __index metamethod, which has the prototype:

function(table, field)

where table is the table being accessed, and field is the name of the field being accessed.

when access an absent field in a table, the interpreter will look for the __index metamethod. if found, it is called and its result is returned. otherwise, nil is returned.

an example

this example shows the use of metamethod __index:

obj1 = { val = 1024, }
obj2 = {}

print(obj2.val)

mt = {
    __index = function(table, field)
        return obj1[field]
    end,
}
setmetatable(obj2, mt)

print(obj2.val)

the above code will output:

nil
1024

a shortcut

because the use of __index metamethod is common, lua also provides a shortcut for it: rather than being a function, the __index field can also be a table. in that case, the table is used to lookup the absent field. this is to say, writing:

__index = mytable,

is equivalent as:

__index = function(table, field)
    return mytable[field]
end

class-based oop in lua

with the above knowledge in mind, we can now implement class-based oop, despite its prototype-based nature. the root of the class hierarchy is table object:

object = {
    new = function(self)
        local obj = {}
        local mt = copy(getmetatable(self)) or {}
        mt.__index = self
        setmetatable(obj, mt)
        return obj
    end,
    super = function(self)
        return getmetatable(self).__index
    end,
}
  • the new method has two purposes:

    1. create a sub class.

    2. create an instance.

  • the super method returns:

    1. the super class, for a class.

    2. the class, for an instance.

the copy function is merely a simple shallow copy:

copy = function(obj)
    local obj2
    if type(obj) == "table" then
        obj2 = {}
        for k, v in pairs(obj) do
            obj2[k] = v
        end
    else
        obj2 = obj
    end
    return obj2
end

use the class hierarchy

  • to create a derive class:

    dev1 = object:new()
    
  • to create a chain of derive classes:

    dev2 = dev1:new()
    dev3 = dev2:new()
    
  • to add a class method:

    dev1.getval = function(self)
        print("function:dev1.getval:")
        return self.val
    end
    
  • to add a class field:

    dev1.val = 1
    
  • to add an instance field:

    dev1.new = function(self)
        local obj = dev1:super().new(self)
        obj.val = 11
        return obj
    end
    
  • to add a metamethod:

    mt = getmetatable(dev1)
    mt.__add = function(l, r) return l.val + r.val end
    setmetatable(dev1, mt)
    
  • to overwrite a class method:

    dev2.getval = function(self)
        print("function:dev2.getval:")
        return self.val
    end
    
  • to overwrite a class field:

    dev2.val = 2
    
  • to overwrite an instance field:

    dev2.new = function(self)
        local obj = dev2:super().new(self)
        obj.val = 22
        return obj
    end
    
  • to overwrite a metamethod:

    mt = getmetatable(dev2)
    mt.__add = function(l, r) return (l.val + r.val) / 2 end
    setmetatable(dev1, mt)
    

download the full test code here.