lualua-tablemeta-method

How to create a simple importable class in Lua?


I'd like to create MyClass class in Lua in a separate file myclass.lua which I can import and use later. It should be working the following way:

local MyClass = require 'myclass'
tab = {1,2,3}
m = MyClass(tab)

However, following the code in Lua docs I can't make it work and am getting errors attempt to call global 'MyClass' (a table value).

The code I have written so far for myclass.lua:

local MyClass = {}
MyClass.__index = MyClass

function MyClass.__init(tab)
    self.tab = tab or {}
    setmetatable({},MyClass)
    return self
end
return MyClass

There is a plethora of examples how to write classes in Lua but I don't think I understand the difference and as a result getting lost in the implementation details. Is there a more or less conventional way to do it?


Solution

  • In Lua, you cannot usually call a table like you would call a function. For example, this code will produce an error of "attempt to call local 't' (a table value)".

    local t = {}
    t()
    

    There is a way of making this work by using metatables, however.

    local hello = {}
    local mt = {} -- The metatable
    mt.__call = function ()
      print("Hello!")
    end
    setmetatable(hello, mt)
    hello() -- prints "Hello!"
    

    When you try and call a table as you would a function, Lua first checks to see whether the table has a metatable. If it does, then it tries to call the function in the __call property of that metatable. The first argument to the __call function is the table itself, and subsequent arguments are the arguments that were passed when the table was called as a function. If the table doesn't have a metatable, or the metatable doesn't have a __call function, then an "attempt to call local 't'" error is raised.

    Your example code has three problems:

    1. You are trying to use __init instead of __call. Lua doesn't have an __init metamethod.
    2. __call takes different parameters than the ones you are using. The first parameter to the __call function is the table itself. You can either use function MyClass.__call(self, tab), or use the colon syntax, function MyClass:__call(tab), which implicitly adds the self parameter for you. These two syntaxes are functionally identical.
    3. You haven't set a metatable for the MyClass table. While you are setting a metatable for MyClass's objects, that doesn't mean that a metatable is automatically set for MyClass itself.

    To fix this, you could do something like the following:

    local MyClass = {}
    setmetatable(MyClass, MyClass)
    MyClass.__index = MyClass
    
    function MyClass:__call(tab)
        local obj = {}
        obj.tab = tab or {}
        setmetatable(obj, MyClass)
        return obj
    end
    
    return MyClass
    

    This sets MyClass to use itself as a metatable, which is perfectly valid Lua.

    The system of metatables is very flexible, and allows you to have just about any class/object scheme you want. For example, if you want, you can do everything inline.

    local MyClass = {}
    
    setmetatable(MyClass, {
        __call = function (class, tab)
            local obj = {}
            obj.tab = tab or {}
            setmetatable(obj, {
                __index = MyClass
            })
            return obj
        end
    })
    
    return MyClass
    

    As well as being concise, this also has the advantage that people can't change the class's metamethods if they have access to the class table.