2

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).

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • The question you asked was answered. If applying that answer leads you to a different problem, you should ask it as a new question. – Nicol Bolas Jan 14 '23 at 16:40
  • I suggest you to apply my rewrite of your code, which neatly separates metatables and tables. `setmetatable(self, self)` is an absolute antipattern IMO; it requires all metamethods to be set on *each instance* of the "class". In your current form, your code is very high-overhead, not low-overhead, but in terms of costly object creation and instance size. If you only create metatables once, having distinct metatables won't introduce much overhead (only Lua table management overhead). On the plus side, smaller metatables presumably lead to less collisions and thus faster metamethod execution. – Luatic Jan 14 '23 at 16:52
  • You're probably confusing Lua metatables with JS prototypes here, which *require* that a special field of the object (the prototype field) be set. The Lua metatable field is completely hidden - it is only accessible through the `setmetatable` and `getmetatable` API calls. This allows you to separate tables from their metatables. – Luatic Jan 14 '23 at 16:54
  • Setting the metatable of an object to itself doesn't really make sense - the entire point of metatables in Lua is to have multiple objects share the same metatable. Setting `self.__index = self` would cause you to recurse until you hit a stack overflow if a property was not found. This code is broken in multiple ways and needs to be replaced. – Luatic Jan 14 '23 at 16:55

1 Answers1

1

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.

Luatic
  • 8,513
  • 2
  • 13
  • 34
  • cheers. i'll be using your recommended code as a template. however, just for my own sanity and peace of mind, can you explain why, if i use ```lua setmetatable(test, test)``` in my original code. ```lua test.___call``` works, but ```lua test.__tostring``` stops working? i'm also curious about how i'd verify that ```lua test.___index``` is working as expected in that code. – Bernard Langham Jan 15 '23 at 14:53
  • `setmetatable(test, test)` sets the metatable of the `test` "class" table, *not* of "instances" of `test`. Thus if you call `tostring(test)`, that would then call `test.__tostring`. You call `tostring(t)` however, which uses the metatable of `t`. `t` is your `self` instance in `test:new()`. If it does not have a metatable set or its metatable doesn't have a `__tostring` field, `tostring(t)` will default to printing the table address. – Luatic Jan 15 '23 at 15:09
  • okay, light begins to dawn. thanks for your patience. – Bernard Langham Jan 17 '23 at 13:29