Object-Oriented Programming in Lua can be a little bit tricky. Existing online materials don’t cover this topic well and their examples fail when things get more complicated. After some extensive reading I came up with a piece of code that actually works and is easy to understand.

The core of OOP in Lua is its metatable and the __index metamethod in the metatable. Don’t forget this famous quote from Programming in Lua:

Tables in Lua are not a data structure; they are the data structure.

So how do metatable and metamethod work in Lua’s OOP exactly? First of all, all classes and objects are tables. When we access an absent field in a table, it triggers the interpreter to look for an __index metamethod: If there is no such method, the access results in nil; otherwise, the metamethod will provide the result. (From here.)

The prototype of the __index metamethod is:

function (table, key)
    ...
end

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

Also, Lua provides a shortcut for the __index metamethod: It can also be a table instead of a function. If so, Lua redoes the access in that table.

Knowing these, we can define an Object class which acts as the root class of all user-defined classes:

--  The root class of all user-defined classes.
Object = {
    --  The `new` function serves 2 purposes:
    --
    --  1.  Create an instance of this class.
    --  2.  Subclass.
    --
    --  They both use the syntax `Object:new()`.
    new = function(self)
        --  Create the subclass and its metatable.
        local o = {}
        local mt_o = {}

        --  Get metatable of the base class and copy all values there into
        --  metatable of the subclass.
        local mt_self = getmetatable(self)
        if mt_self ~= nil then
            for k, v in pairs(mt_self) do
                mt_o[k] = v
            end
        end

        --  Create/Overwrite metamethod `__index` for the subclass.
        --  This also works:
        --      mt_o.__index = function(table, key) return self[key] end
        mt_o.__index = self

        --  Set metatable for the subclass.
        setmetatable(o, mt_o)

        -- Return the subclass.
        return o
    end
}

Basically, the new function creates a new empty object, fills its metatable and sets its __index metamethod to the correct value.

This class is easy to use because all you need to do is Object:new(). Below are some testing code:

--  Base class.

Base = Object:new()

function Base:foo()
    return "Base:foo"
end

function Base:bar()
    return "Base:bar"
end

--  Test: Overwrite class methods.

Derive1 = Base:new()

function Derive1:foo()
    return "Derive1:foo"
end

Derive2 = Base:new()

function Derive2:foo()
    return "Derive2:foo"
end

b = Base:new()
d1 = Derive1:new()
d2 = Derive2:new()
print(b.foo())              --  Base:foo
print(b.bar())              --  Base:bar
print(d1.foo())             --  Derive1:foo
print(d1.bar())             --  Base:bar
print(d2.foo())             --  Derive2:foo
print(d2.bar())             --  Base:bar

-- Test: Subclass with metamethods.

MyAdd = Object:new()
mt = getmetatable(MyAdd)
mt.__add = function (l, r) return l.v + r.v end
mt.__mul = function (l, r) return l.v * r.v end
setmetatable(MyAdd, mt)

myadd1 = MyAdd:new()
myadd2 = MyAdd:new()
myadd1.v = 5
myadd2.v = 9
print(myadd1 + myadd2)      --  14
print(myadd1 * myadd2)      --  45

--  Test: Chaining classes.

Chain1 = Base:new()
Chain2 = Chain1:new()
Chain3 = Chain2:new()

function Chain2:foo()
    return "Chain2:foo"
end

c1 = Chain1:new()
c2 = Chain2:new()
c3 = Chain3:new()
print(c1.foo())             --  Base:foo
print(c2.foo())             --  Chain2:foo
print(c3.foo())             --  Chain2:foo

They all work as expected.