Why doesn't the __call metamethod work in this Lua class?

431 Views Asked by At

in the code below, i set up a __call metafunction which, in theory, should allow me to call the table as a function and invoke the constructor, instead of using test.new()

test = {}
function test:new()
  self = {}
  setmetatable(self, self)
  --- private properties
  local str = "hello world"
  -- public properties
  self.__index = self
  self.__call = function (cls, ...) print("Constructor called!") return cls.new(...) end
  self.__tostring = function() return("__tostring: "..str) end
  self.tostring = function() return("self:tstring(): "..str) end
  return self
end

local t = test:new()
print(t)             -- __tostring overload works
print(tostring(t))   -- again, __tostring working as expected
print(t:tostring())  -- a public call, which works
t = test()           -- the __call metamethod should invoke the constructor test:new()

output:

> __tostring: hello world
> __tostring: hello world
> self.tostring(): hello world
> error: attempt to call global `test` (a table value) (x1)

(i'm using metatable(self, self) because i read somewhere it produces less overhead when creating new instances of the class. also it's quite clean-looking. it may also be where i'm getting unstuck).

1

There are 1 best solutions below

3
Luatic On

You're setting the __call metamethod on the wrong table - the self table - rather than the test table. The fix is trivial:

test.__call = function(cls, ...) print("Constructor called!") return cls.new(...) end
setmetatable(test, test)

After this, test(...) will be equivalent to test.new(...).


That said, your current code needs a refactoring / rewrite; you overwrite the implicit self parameter in test:new, build the metatable on each constructor call, and don't even use test as a metatable! I suggest moving methods like tostring to test and setting the metatable of self to a metatable that has __index = test. I'd also suggest separating metatables and tables in general. I'd get rid of upvalue-based private variables for now as they require you to use closures, which practically gets rid of the metatable benefit of not having to duplicate the functions per object. This is how I'd simplify your code:

local test = setmetatable({}, {__call = function (cls, ...) return cls.new(...) end})
local test_metatable = {__index = test}

function test.new()
    local self = setmetatable({}, test_metatable)
    self._str = "hello world" -- private by convention
    return self
end

function test_metatable:__tostring()
    return "__tostring: " .. self._str
end

function test:tostring()
    return "self:tstring(): " .. self._str
end

If you like, you can merge test and test_metatable; I prefer to keep them separated however.