views:

161

answers:

3

I've implemented my own class system and I'm having trouble with __tostring; I suspect a similar issue can happen with other metamethods, but I haven't tried.

(Brief detour: each class has a __classDict attribute, holding all methods. It is used as the class instances' __index. At the same time, the __classDict's __index is the superclass' __classDict, so methods in superclasses are authomatically looked up.)

I wanted to have a "default tostring" behavior in all instances. But it didn't work: the "tostring" behavior doesn't "propagate" through subclasses correctly.

I've done this test exemplifying my issue:

mt1 = {__tostring=function(x) return x.name or "no name" end }
mt2 = {}
setmetatable(mt2, {__index=mt1})
x = {name='x'}
y = {name='y'}
setmetatable(x, mt1)
setmetatable(y, mt2)
print(x) -- prints "x"
print(mt2.__tostring(y)) -- prints "y"
print(y) -- prints "table: 0x9e84c18" !!

I'd rather have that last line print "y".

Lua's "to_String" behaviour must be using the equivalent of

rawget(instance.class.__classDict, '__tostring')

instead of doing the equivalent of

instance.class.__classDict.__tostring

I suspect the same happens with all metamethods; rawget-equivalent operations are used.

I guess one thing I could do is copying all the metamethods when I do my subclassing (the equivalent on the above example would be doing mt2.__tostring = mt1.__tostring) but that is kind of inelegant.

Has anyone fought with this kind of issue? What where your solutions?

+1  A: 

See the Inheritance Tutorial on the Lua Users Wiki.

Doug Currie
Sorry, but I don't see how that tutorial is relevant for my question.
egarcia
@egarcia, you are right, it doesn't address the issue directly, and I am surprised by that. My solution would be to duplicate the instance metatable (which can be shared among instances of the class, and need not contain the class methods themselves which are off the __index). This is called `class_mt` in the wiki page I referenced.
Doug Currie
+1  A: 

I suspect the same happens with all metamethods; rawget-equivalent operations are used.

That is correct. from the lua manual:

... should be read as rawget(getmetatable(obj) or {}, event). That is, the access to a metamethod does not invoke other metamethods, and the access to objects with no metatables does not fail (it simply results in nil).

Generally each class has it's own metatable, and you copy all references to functions into it. That is, do mt2.__tostring = mt1.__tosting

daurnimator
Thanks for confirming this. I didn't notice that part on the manual. Now that I know what it does, I'll see if I can do something a bit more creative than copying the metamethods. That getmetatable gave me an idea.
egarcia
+1  A: 

Thanks to daurnimator's comments, I think I found a way to make metamethods "follow" __index as I want them to. It's condensed on this function:

local metamethods = {
  '__add', '__sub', '__mul', '__div', '__mod', '__pow', '__unm', '__concat', 
  '__len', '__eq', '__lt', '__le', '__call', '__gc', '__tostring', '__newindex'
}

function setindirectmetatable(t, mt) 
  for _,m in ipairs(metamethods) do
    rawset(mt, m, rawget(mt,m) or function(...)
      local supermt = getmetatable(mt) or {}
      local index = supermt.__index
      if(type(index)=='function') then return index(t,m)(...) end
      if(type(index)=='table') then return index[m](...) end
      return nil
    end)
  end

  return setmetatable(t, mt)
end

I hope it is straightforward enough. When a new metatable is set, it initializes it with all metamethods (without replacing existing ones). These metamethods are prepared to "pass on" requests to "parent metatables".

This is the simplest solution I could find. Well, I actually found a solution that used less characters and was a bit faster, but it involved black magic (it involved metatable functions de-referencing themselves inside their own bodies) and it was much less readable than this one.

If anyone finds a shorter, simpler function that does the same, I'll gladly give him the answer.

Usage is simple: replace setmetatable by setindirectmetatable when you want it to "go up":

mt1 = {__tostring=function(x) return x.name or "no name" end }
mt2 = {}
setmetatable(mt2, {__index=mt1})
x = {name='x'}
y = {name='y'}
setmetatable(x, mt1)
setindirectmetatable(y, mt2) -- only change in code
print(x) -- prints "x"
print(mt2.__tostring(y)) -- prints "y"
print(y) -- prints "y"

A little word of warning: setindirectmetatable creates metamethods on mt2. Changing that behavior so a copy is made, and mt2 remains unaltered, should be trivial. But letting them set up by default is actually better for my purposes.

egarcia
You should move that table literal outside the function: no need to recreate it each time the function is used.
daurnimator
@daurnimator: you are right, edited. Thanks!
egarcia