Help request: class, inheritance and elegance

@Jmv38 - @toadkick is right, even with metatable you will fall into circular reference. I take a jump into the exercise, thank you for the break :slight_smile:
I think you will see your ā€˜code architectureā€™ differently with metatables in mind, and maybe never use class anymore :stuck_out_tongue:

My heavy loaded attempt:

-- hooked into class but can be applied 'by hand' on each
-- class instances ie:
-- A = class()
-- B = class(A)
-- b = B()
-- b.super = notsuper(A)    
function notsuper(t,base)
    local proxy,sp = {},{}
    
    local function apply(self,base,k,...)
        local fn = proxy[k] == base[k] and base._base[k] or base[k]
        local a = {...}
        return
        function()
            proxy[k] = fn
            local res = {fn(self,unpack(a))}
            proxy[k] = base[k]
            return unpack(res)
        end
    end
    
    sp.__index = function(tbl,k,...)
        return apply(t,base,k,...)
    end
    return setmetatable({},sp)
end
-- override object instanciation
local oclass = class
class = function(base)
    local c = oclass(base)
    if not base then return c end
    local mt = getmetatable(c)
    mt.__call = function(class_tbl, ...)
        local obj = {}
        setmetatable(obj,c)
        if class_tbl.init then
            class_tbl.init(obj,...)
        else
            if base and base.init then
                base.init(obj, ...)
            end
        end
        -- > here < --
        obj.super = notsuper(obj,base)
        return obj
    end
    return c
end

function setup()
    A = class()
    function A:fn()
        print("A called from",self)
        return "A.fn called"
    end
    B = class(A)
    function B:fn()
        print("B called from",self)
        return self.super:fn()
    end

    C = class(B)
    function C:fn()
        print("C called from ",self)
        return self.super:fn()
    end

    local a = A()
    print("a", a)
    local str = a:fn()
    print(str)
    
    print("----------")
    
    local b = B()
    print("b", b)
    local t0 = os.clock()
    local str = b:fn()
    local t1 = os.clock()
    print(str)
    print(t1-t0)
    
    print("----------")

    local c = C()
    print("c", c)
    t0 = os.clock()
    str = c:fn()
    t1 = os.clock()
    print(str)
    print(t1-t0)

end

@toffer: super clever! If it were not so expensive, I would totally consider using it :smiley:

Also: with a small change, I think you can do self.super:init() as well within a subclassā€™ init function:

class = function(base)
    local c = oclass(base)
    if not base then return c end
    local mt = getmetatable(c)
    mt.__call = function(class_tbl, ...)
        local obj = {}
        setmetatable(obj,c)
        -- moved to here
        obj.super = notsuper(obj,base)
        if class_tbl.init then
            class_tbl.init(obj,...)
        else
            if base and base.init then
                base.init(obj, ...)
            end
        end
        return obj
    end
    return c
end

@toffer and @Toadkick you guys are champs!
This is so good to see how impossible things sometimes become actually possible in ā€¦ 15 mn :wink:
But we are not quite there: the second call to the functions fail, for a reason i cant see. Any idea about what is the problem and how to solve it?


-- hooked into class but can be applied 'by hand' on each
-- class instances ie:
-- A = class()
-- B = class(A)
-- b = B()
-- b.super = notsuper(A)    
-- toffer:
function notsuper(t,base)
    local proxy,sp = {},{}

    local function apply(self,base,k,...)
        local fn = proxy[k] == base[k] and base._base[k] or base[k]
        local a = {...}
        return
        function()
            proxy[k] = fn
            local res = {fn(self,unpack(a))}
            proxy[k] = base[k]
            return unpack(res)
        end
    end

    sp.__index = function(tbl,k,...)
        return apply(t,base,k,...)
    end
    return setmetatable({},sp)
end
-- override object instanciation
local oclass = class
local mclass1 = function(base)
    local c = oclass(base)
    if not base then return c end
    local mt = getmetatable(c)
    mt.__call = function(class_tbl, ...)
        local obj = {}
        setmetatable(obj,c)
        if class_tbl.init then
            class_tbl.init(obj,...)
        else
            if base and base.init then
                base.init(obj, ...)
            end
        end
        -- > here < --
        obj.super = notsuper(obj,base)
        return obj
    end
    return c
end
-- toadkick version
local mclass2 = function(base)
    local c = oclass(base)
    if not base then return c end
    local mt = getmetatable(c)
    mt.__call = function(class_tbl, ...)
        local obj = {}
        setmetatable(obj,c)
        -- moved to here
        obj.super = notsuper(obj,base)
        if class_tbl.init then
            class_tbl.init(obj,...)
        else
            if base and base.init then
                base.init(obj, ...)
            end
        end
        return obj
    end
    return c
end

--[[
    local t0 = os.clock()
    local t1 = os.clock()
    print(t1-t0)
--]]
function setup()
    local mclass = mclass1
    local tprint = {}
    local mprint
    A = mclass()
    function A:fn()
        self.x = 1000
    end
    B = mclass(A)
    function B:fn()
        self.super:fn()
        self.x = self.x + 100
    end

    C = mclass(B)
    function C:fn()
        self.super:fn()
        self.x = self.x + 10
    end

    local a = A()
    local b = B()
    local c = C()
    
    -- this is ok
    a:fn()
    b:fn()
    c:fn()
    print("a call: ",a.x)
    print("b call: ",b.x)
    print("c call: ",c.x)

    -- but this is not ???!!!
    a:fn()
    b:fn()
    c:fn()
    print("a call: ",a.x)
    print("b call: ",b.x)
    print("c call: ",c.x)

end

function draw()
    background(0)
end

Also i dont really get what is so expensive about this solution? Can you commment on that? If it is you create functions copies on object instanciation, ok you load the memory. But then, why cant you create just once the series of functions needed, with 2 entries, one for super and one for notsuper, Add them to the super class template, and call this function with the good params thanks to __call? (my text is certainly messy, but i am sure you get the idea)

Btw, reading the code of @toffer, i feel it is about what i had in mind (although i dont understand it fully), but i can see i wouldnt have been able to write such a code before months!

Itā€™s expensive for a couple of reasons, but the biggest (and most frequent) hit will be that the ā€œapplyā€ function gets called every time you access self.super, which will be (at least) once per every overridden function invocation. For starters, that function:

  • creates (and unpacks) 2 tables (the unpacking means it gets more expensive as you pass more arguments)
  • creates and invokes a closure

Again: every single time you access self.super. Compare that toā€¦wellā€¦not doing that, and it adds up very quickly. And thatā€™s not even considering the overhead of just creating all of these ā€œnotsuperā€ closures to begin with (1 per every instantiated object).

As to why itā€™s failing on the second invocation, Iā€™ve not played with this much beyond running it (and noticing the change I suggested above). Further inspection will be necessary to find the problem and (attempt) to solve it. Frankly, as Iā€™ve said before: I just donā€™t think itā€™s worth it (well, beyond the educational value of attempting it anyway) :wink: I donā€™t see any way of doing this that will not be terribly expensive, not to mention a way that gets around the infinite recursion issue in the first place.

@Toadkick thanks for the comment! Now i see it is terribly expensive and cant be used. But i still think there might be a way. That challenge is too pleasant, i wont let go.

[EDIT] @Toadkick thankā€™s for pointing out the good place for the init stuff and well explain why itā€™s so expensive @Jmv38 - Yup itā€™s like a hammer to bend a needle :)). With some sleep I came to this two others. The idea is to catch access from metatable of instanciated classes. (I havenā€™t my IPad so I hooked into the full class function)

First one, it use a flag to route the getter:

function class(base)
    local c = {}    -- a new class instance
    if type(base) == 'table' then
        -- our new class is a shallow copy of the base class!
        for i,v in pairs(base) do
            c[i] = v
        end
        c._base = base
    end

    -- the class will be the metatable for all its objects,
    -- and they will look up their methods in it.
    
    -- ====== ADDED =======
    -- c.__index = c
    local switch
    c.__index = function(tbl,k)
        if k == "super" then
            switch = true
            return tbl
        end
        if switch then
            switch = nil
            local ctx,base = debug.getinfo(2,"nf"),c
            local name,fn = ctx.name,ctx.func
            while base[name] ~= fn do
                base = base._base
            end
            return base._base[k]
        end
        return c[k]
    end
    -- =====================

    -- expose a constructor which can be called by <classname>(<args>)
    local mt = {}
    mt.__call = function(class_tbl, ...)
        local obj = {}
        setmetatable(obj,c)
        if class_tbl.init then
            class_tbl.init(obj,...)
        else 
            -- make sure that any stuff from the base class is initialized!
            if base and base.init then
                base.init(obj, ...)
            end
        end
        
        return obj
    end

    c.is_a = function(self, klass)
        local m = getmetatable(self)
        while m do 
            if m == klass then return true end
            m = m._base
        end
        return false
    end

    setmetatable(c, mt)
    return c
end

Second one, swap __index each time super is requested

function class(base)
    local c = {}    -- a new class instance
    if type(base) == 'table' then
        -- our new class is a shallow copy of the base class!
        for i,v in pairs(base) do
            c[i] = v
        end
        c._base = base
    end

    -- the class will be the metatable for all its objects,
    -- and they will look up their methods in it.
    -- ====== ADDED =======
    -- c.__index = c
    local _index,_super
    _index = function(tbl,k)
        if k == "super" then
            c.__index = _super
            return tbl
        end
        return c[k]
    end
    _super = function(tbl,k)
        c.__index = _index
        local ctx,base = debug.getinfo(2,"nf"),c
        local name,fn = ctx.name,ctx.func
        while base[name] ~= fn do
            base = base._base
        end
        return base._base[k]
    end
    c.__index = _index
    -- =====================

    -- expose a constructor which can be called by <classname>(<args>)
    local mt = {}
    mt.__call = function(class_tbl, ...)
        local obj = {}
        setmetatable(obj,c)
        if class_tbl.init then
            class_tbl.init(obj,...)
        else 
            -- make sure that any stuff from the base class is initialized!
            if base and base.init then
                base.init(obj, ...)
            end
        end
        
        return obj
    end

    c.is_a = function(self, klass)
        local m = getmetatable(self)
        while m do 
            if m == klass then return true end
            m = m._base
        end
        return false
    end

    setmetatable(c, mt)
    return c
end

A = class()
function A:fn()
    print("A.fn from",self)
    return "Yeah I'm A.fn!"
end
B = class(A)
function B:fn()
    print("B.fn from",self)
    return self.super:fn()
end
C = class(B)
function C:fn()
    print("C.fn from",self)
    print(self.super:fn())
end
a = A() b = B() c = C()
print("a",a)
print("b",b)
print("c",c)
print("------")
c:fn()
print("------")
c:fn()

ā€¦mmhh a smaller hammer but still a hammer 8-} . With a super drawback: you canā€™t leave a reference to super without a call to a method ie:

local super = self.super
self:dosomestuff()
-- > bang, lost in metatables space! 

With the current class implementation I believe you can write like this:

function C:fn()
        self._base.fn(self)
end

but if you really want to use : I guess you need to mess about with metatables, which is fun :slight_smile:

Ok @toffer, Your solutions above seem to work perfectly, you are the boss ^:)^
Could you tell me if your solutions above are still too xpensive?
I tried myself the thing below, not to be expensive, but when i make two levels of calls i get stack overflow. I assume it due to circular reference, but i cant even understand where ( shame on meā€¦)


-- 0      test
-- Class.lua
-- Compatible with Lua 5.1 (not 5.0).

function createSuperCopy(base)
    local c,copy= {},{}
    if type(base) == 'table' then
        for name,v in pairs(base) do 
            if type(v)=="function" then 
                copy[name] = v
            end 
        end
        for name,v in pairs(copy) do 
            if type(v)=="function" then 
                local newFunc = function(arg1,...)
                    local newArg1
                    newArg1 = arg1.obj
                    v(newArg1,...)
                end
                c[name] = newFunc
            end 
        end
    end
    c.init = nil
    return c
end

function mclass(base)
    local c = {}    -- a new class instance
    if type(base) == 'table' then
        -- our new class is a shallow copy of the base class!
        for i,v in pairs(base) do
            c[i] = v
        end
        c._base = base
    end
    local superBase = createSuperCopy(base)

    -- the class will be the metatable for all its objects,
    -- and they will look up their methods in it.
    c.__index = c

    -- expose a constructor which can be called by <classname>(<args>)
    local mt = {}
    mt.__call = function(class_tbl, ...)
        local obj = {}
        setmetatable(obj,c)

        if class_tbl.init then
            class_tbl.init(obj,...)
        else 
            -- make sure that any stuff from the base class is initialized!
            if base and base.init then
                base.init(obj, ...)
            end
        end

--        obj.super = c.superBase()

        obj.super = {}
        for name,v in pairs(superBase) do obj.super[name] = v end
        obj.super.obj = obj
        
        return obj
    end

    c.is_a = function(self, klass)
        local m = getmetatable(self)
        while m do 
            if m == klass then return true end
            m = m._base
        end
        return false
    end

    setmetatable(c, mt)
    return c
end

function setClassName(klass)
    local className = "unknown"
    for name,v in pairs(_G) do if v==klass then className = name end end
    klass._className = className
end


C1 = mclass()
function C1:init()
end
function C1:action()
    self.x = (self.x or 0) + 1000
end

C2 = mclass(C1)
function C2:init()
end
function C2:action()
    self.super:action() 
    self.x = self.x + 100
end

C3 = mclass(C2)
function C3:init()
end
function C3:action()
    self.super:action() 
    self.x = self.x + 10
end

function setup()
    obj1=C1()
    obj2=C2()
    obj3=C3()
    parameter.action("1 level ok", function()
        obj1:action()
        obj2:action()
        print(tostring(obj1).." .x = ".. tostring(obj1.x) )
        print(tostring(obj2).." .x = ".. tostring(obj2.x) )    
    end)
    
    parameter.action("2 levels fail", function()
        obj1:action()
        obj2:action()
        obj3:action()
        print(tostring(obj1).." .x = ".. tostring(obj1.x) )
        print(tostring(obj2).." .x = ".. tostring(obj2.x) )    
        print(tostring(obj3).." .x = ".. tostring(obj3.x) )    
    end)
    

end

function draw()
    background(40, 40, 50)
end

@tnlogy thanks for pointing that out. I discovered that this afternoon too, when digging into the class() function.

But if you want to make it really elegant you want to call the supermethod with just
super:fn() like this

function callWithSuper(o, f)
    local env = {}
    local cenv = getfenv(f)
    setmetatable(env, {
        __index = function (t, k)
            if k == "super" then
                return o._base
            end
            return cenv[k]
        end
    })
    setfenv(f, env)
end

A = class()

function A:draw()
    -- Codea does not automatically call this method
    print("A draw")
end

B = class(A)
function B:draw()
    super:draw()
    print("B draw")
end

function setup()
    b = B()
    callWithSuper(b, b.draw)
    b:draw()
end

but with a big overhead :slight_smile:

@Jmv38 - since each property or a method retrieval require to pass through the __index function, yep, I find it expensive, maybe if the __index function was attached to the mt metatable that would be less expensive (all props and methods that does not exists in the class itself ie: the super property would be looked in the mt). by the way, the better is to make some benchmark, but I feel the very first implementation I give (8 november) is fastest. Otherwise will you reconsider really powerfull and flexible approach that lua bring by non binding function to object ? MySuperClass.fn(self) =)). By the way for you implementation, I think the problem is that you do a circular reference with obj.super.obj = obj.

ps: just a thought, I find the class itself expensive. It create one table mt that juste serve the instanciation,copy (reference) all properties of itā€™s base (that is if you move class methods during runtime, the instances are not updated :() and create a new function is_a for each class definitionā€¦ ok, ok I got misplaced.

@tnlogy - nice one, but self is not bind to itā€™s super class method and it fail more than 2 inheritances. Iā€™ve never looked a the cost of changing env context, you ?

@toffer thanks. I still dont quite understand how all this works: are the calls to debug done for each function call, everytime? If so, then it is expensive, cause debug is vey slow. If the call is done just once, at the function instanciation, then that is ok.
I am glad to see this was possible, but for the moment i am going to skick to the lighter syntax for the time being. (super.fn(self) ). Iā€™ll bump the subject if i ever find a better construct.
Thanks you all!

@Jmv38 - Nop, the calls to debug are made each time self.super:fn is accessed. But each time you access a property or a method ie: self.x, self:fn, a check is done in this function:

_index = function(tbl,k)
        if k == "super" then
            c.__index = _super
            return tbl
        end
        return c[k]
    end

to check if the wanted property is self.super (that why I think this we can prevent extra call to __index if this function is moved into mt).
EDIT: nvm, the above solution does not work with more than 3 class, a stack overflow occure.

@Jmv38 - Iā€™ve fixed the stackover flow issue with more than 3 class (the post is upadted).

@Toffer can you indicate which one you have modified? Put [edit] just before. So i known which one you have changed. I had tried both but didnt notice the stack overflow, so i am not quite sure which code you are talking about.

@Jmv38 - done, the one with the 2 solution, switch and swap __index

@Toffer thanks a lot! Got them, tried them: work fine.