Search code examples
luametatable

How to change a table's metatable but inherit it's own methods in Lua


in Lua we do OO programming like this:

MyClass = {}

function MyClass:new()
    local obj = {}
    setmetatable(obj, self)
    self.__index = self

    obj.hello = "hello world"

    return obj
end

function MyClass:sayHi()
    print(self.hello)
end

function main()
  local obj = MyClass:new()
  obj:sayHi()
end

When working with more compelx stuff, usually I take advantage of Lua's metamethods to proxy function calls and do whatever I need with it, like arguments parsing etc, by adding this:

MyClassMeta = {}

function MyClassMeta.__index(obj, funcName)
    return function (self, ...)
        //do some stuff
        print("you called " .. funcName .. " with args", ...)
    end
end

and changing the line:

setmetatable(obj, self)

to:

setmetatable(obj, MyClassMeta)

every function I call with an instance of MyClass will execute the code implemented in MyClassMeta.__index metamethod.

What I want to do now is inherit the MyClass existing methods, and execute MyClassMeta.__index only for functions that are not part of MyClass.

In the above example, the code will always execute the MyClassMeta.__index metamethod even when calling MyClass:sayHi():

function main()
  local obj = MyClass:new()
  obj:sayHi("hello")
end

you called sayHi with args hello


Solution

  • When you set __index to a table, it will look for properties on that table and return them if they don't exist on the instance. Since sayHi exists on the MyClass table it is used.

    self.__index = self
    

    When you set __index to a function, it can return anything for properties that don't exist on the instance. You can check if the key exists on the MyClass table and return it, then do something else if it doesn't:

    MyClass = {}
    
    MyMetatable = {
      __index = function(obj, key)
        if MyClass[key] ~= nil then return MyClass[key] end
        return function(self, ...)
          print("you called "..tostring(key))
          print("  self.hello is '"..tostring(self.hello).."'")
          print("  with args", ...)
        end
      end
    }
    
    function MyClass:new()
        local obj = {}
        setmetatable(obj, MyMetatable)
    
        obj.hello = "hello world"
        return obj
    end
    
    function MyClass:sayHi()
        print(self.hello)
    end
    
    function main()
      local obj = MyClass:new()
      obj:sayHi()
    end
    
    local obj = MyClass:new()
    obj:sayHi("hello")
    obj:somethingElse(1, 2, 3)
    

    Version with Egor's comment

    MyClass = {}
    
    setmetatable(MyClass, {
      -- if it's not found on MyClass, return a function
      __index = function(self, funcName)
        return function(self, ...)
          print("you called "..funcName.." with args", ...)
        end
      end
    })
    
    function MyClass:new()
      local obj = {}
      -- if it's not found on obj, try self (MyClass)
      setmetatable(obj, { __index = self })
      obj.hello = "hello world"
      return obj
    end
    
    function MyClass:sayHi()
        print(self.hello)
    end
    
    local obj = MyClass:new()
    obj:sayHi()
    obj:somethingElse(1, 2, 3)
    

    When creating an object this sets the __index of the new object's metatable to MyClass, and MyClass's metatable's index to the function that is a fallback. So if the property isn't on your object or on MyClass, it will use the fallback.