__call() metamethod in class() function

@XanDDemoX Interesting thoughts, I’m continuing to iterate on the whole “class() is a callable” thing and see where I end up. I admit atm I tend to like a distinct _Root just for clarity … my brain reaches a point where too many self-referencing metatables simply shuts it down :slight_smile:

And don’t forget that obj:is_a() is just syntactic sugar, there is nothing to stop you calling it directly passing the object as the first argument (and then adding your additional type validations to is_a() of course).

And yes, agreed about the vec2() work-around, though I tend to use: (v * 1.0) as a fake copy constructor as it seems cleaner, though I hanker for the cleaner vec2(v) construct.

I also thought hard about why it is I like dynamic binding more than the shallow copy. I think what it really comes down to is I think the tab-ordering model used by Codea is fine to a point, but also fragile. With dynamic binding, declaration of methods and fields inside a class or pretty much any order you like, the only exception being that the classes themselves must be declared in order (super to sub). What this means is that the ideal way to declare a class in this model is to have a tab with:

If not Button then Button = class(…) end
(… class methods etc go here)

This allow you to use the class normally as-is, but also, if you want, to gather all the class declarations into one tab and thus expose all the class relations next to each other, which I like.

I’ll post my attempt at a “clean” class() as callable here soon, though of course we could keep tinkering away with this till the universe ends :slight_smile:

–Tim

@drtimhill I’m not sure that I’d declare all my classes on a single tab unless I had a good reason. I generally prefer keeping a class nicely contained to a single tab/file unless I’m using a language which requires me to do otherwise, like C/C++ for example, but everyone has their own different preferences :slight_smile:

You do make a good point about statically calling of is_a, which I had definitely overlooked. However there is still unfortunately still a couple of minor usability problems with using it directly ‘out of the box’ which are still likely give the unwary some headaches and/or unnecessary typing.

Firstly, assuming that there is no special knowledge of the implementation, using is_a statically in a consistent way would likely require a user to create some type of base object.

Otherwise they would have to resort to a either checking for the presence of an is_a function which is less clear and obviously wrong in the unlikely circumstance of a table which has an is_a function but isnt a class instance. I suspect a user would probably trivially avoid this.

Or checking by a particular type, which is definitely going to be the long way around for doing something like generically checking whether a table is just a table or is actually a class instance. Assuming that there is potentially multiple types where there isn’t a common base class. It would require every possible type of interest to be checked to be absolutely sure.

Admittedly its trivial to work around, but its not necessarily as intuative as it would be if it was just available natively from Codea’s API. I expect this would be especially true for somone who is new to Codea, Lua or programming in general.

Object = class()

SomeObject = class(Object)

OtherObject = class()

SomeOtherObject = class(OtherObject)

local instance = SomeObject()
local otherInstance = SomeOtherObject()
Object.is_a(instance,Object) -- true
Object.is_a({},Object) -- false
Object.is_a(1,Object) -- false
Object.is_a(otherInstance, Object) -- false 
Object.is_a(otherInstance, OtherObject) -- true

The other problem is another usability issue. Consider the following ignoring the implementation detail of is_a.

Object = class()
local instance = Object()

if Object.is_a(instance) then
   -- do something
end

Taking the code at face value it looks pretty clear, but there is a not so subtle bug when you know how is_a is implemented. But it would be reasonable to expect Object.is_a(instance)==true but its not, and actually its something that all of our proposed improvements so far have still missed.

To get is_a to work how it reads in this circumstance it needs to be implemented specifically for each class table substituting in the class table when the klass parameter is null. For example setting c.is and c.is_a in __call from my example above would be done as follows.

local is = function(instance,klass) return class.is(instance,klass or c) end
c.is = is
c.is_a = is

The equivalent is equally as trivial to in Codea’s current class function.

c.is_a = function(self, klass)
     klass = klass or c
     local m = getmetatable(self) 
...

@XanDDemoX @Simeon So after thinking about your suggestions this is the final model I came up with. It pretty much combines all the things we have been discussing, and is still nearly 100% backward compatible with the existing model. The only time it would break existing code is if someone was doing some odd hacking on inherited methods, which is pretty unlikely (and probably a dubious thing to do anyway).

– Implements class() function as callable that also encapsulates the root class. The implementation uses no upvalues, nor direct references to “class”, allowing the entire model to be enhanced/revised/replaced at run-time for advanced uses. Even class() can be replaced (with care!).

– Dynamic binding to inherited methods at runtime, allowing mixins and flexible ordering for method creation. Base classes can be created that are later augmented in individual projects without the need to copy+paste the base class (works well with the Codea model of sharing classes between projects).

– Objects can access their class via “_class” field in each object. Similarly, classes can access their base class via “_base” field in each class. This allows robust calling to superclass methods without wiring in knowledge of class inheritance.

– Optional class names at creation time, with name accessible in “__name” field of class (useful for debugging). With Lua 5.3.4 this means tostring() and debug output will use the class name of object instead of “table”.

– Direct syntax for calling class methods within a class method: intra-class: “MyClass.foo(self, …)”, superclass: “MyClass._base.foo(self, …)” and virtual: “self:foo(…)”.

– “is_a()” can be called conventionally (“self:is_a(cls)”) or as a static method (“class.is_a(obj, cls)”), the latter being safe for any value of “obj” (even nil). The default for “cls” is “class”, thus allowing “class.is_a(obj)” to be used as an object check (is this an object?).

Here is the code:

--//// CodeaClass.lua: Enhanced Codea class model (@drtimhill)
do  -- Hide locals private to implementation
    local classmt = {}
    class = setmetatable({}, classmt)
    class.__name = "class"
    class.__index = class

    --//// class(): Class factory - create new class (with optional base class)
    -- (As a convenience, we set cls:_base to the base class)
    -- base     Base class, default is class if none specified
    -- name     Optional class name (for debug help)
    -- returns  New class (create objects using class constructor e.g. "Ball(...)")
    classmt.__call = function(root, base, name)
        base = base or root
        local cls = setmetatable({}, base)
        cls.__index = cls
        cls.__call = root.__call
        cls.__name = name or "unknown"
        cls._base = base
        return cls
    end

    --//// someclass(): Object factory - Create new object of this class
    -- (As a convenience, we set obj:_class to the class)
    -- cls      Class of object to create
    -- ...      Arguments to forward to class:init() initializer
    -- returns  Newly constructed object
    class.__call = function(cls, ...)
        local obj = setmetatable({}, cls)
        obj._class = cls
        obj:init(...)
        return obj
    end

    --//// obj:init(): Initialize a new object (class instance)
    -- self     Object to initialize
    -- ...      Arguments forwarded from constructor (e.g. "Ball(...)") 
    function class:init(...)
        -- Dummy init() for root class
    end
    
    --//// obj:is_a(): Check to see if object is instance of class
    -- self     Object to check
    -- cls      The class to check (this class and all its superclasses)
    -- returns  true if the object is an instance of the class/superclass
    function class:is_a(cls)
        cls = cls or class
        if type(self) ~= "table" then return false end
        local m = getmetatable(self)
        while m do
            if m == cls then return true end
            m = getmetatable(m)
        end
        return false
    end
end

Here is some demo code this highlights the features. Note that the final tostring() demo will only with with Lua 5.3.4 or later.

-- Create some classes, note the use of optional class name (for debugging)
MyClass = class()
MyDerivedClass = class(MyClass, "MyDerivedClass")
MyThirdClass = class(MyDerivedClass)

-- Note that we can create base class methods *after* creating derived classes
-- and they will still be inherited by them, thus allowing base class mix-ins
function MyClass:init(x, y)
    print("MyClass:init(): x,y         = " .. tostring(x) .. "," .. tostring(y))
    MyClass._base.init(self)        -- Not strictly needed for class derived from root
    self.x, self.y = x, y
end

function MyClass:draw()
    print("MyClass:draw(): x,y         = " .. tostring(self.x) .. "," .. tostring(self.y))
end

-- The construct "MyClass._base" is the superclass and so can be used to robustly
-- call the base class even if the inheritance model changes (e.g. to call base
-- class init() or draw() as shown here).
function MyDerivedClass:init(x, y, r)
    print("MyDerivedClass:init(): x,y  = " .. tostring(x) .. "," .. tostring(y))
    MyDerivedClass._base.init(self, x, y)
    self.r = r
end

function MyDerivedClass:draw()
    print("MyDerivedClass:draw(): x,y  = " .. tostring(self.x) .. "," .. tostring(self.y))
    MyDerivedClass._base.draw(self)
end

-- Create some objects for each class
-- Note that MyThirdClass() has no "init()" and so the superclass init() is used
o1 = MyClass(10, 100)
o2 = MyDerivedClass(20, 200, 1.0)
o3 = MyThirdClass(30, 300, 2.0)

-- Do some drawing. Again, MyThirdClass will use the superclass draw() function
o1:draw()   -- Calls MyClass:draw()
o2:draw()   -- Calls MyDerivedClass:draw()
o3:draw()   -- Calls MyDerivedClass:draw()

-- Check object classes
-- Note that we can use the form "class.is_a()" to check possible non-objects safely
print("o3:is_a(MyDerivedClass)     = " .. tostring(o3:is_a(MyDerivedClass)))
print("o2:is_a(MyThirdClass)       = " .. tostring(o2:is_a(MyThirdClass)))
print("class.is_a('fred', MyClass) = " .. tostring(class.is_a("fred", MyClass)))

-- tostring() for objects includes class with Lua 5.3.4 or later
print("tostring(o2)                = " .. tostring(o2))

@XanDDemoX As an example of mixins, consider a classic Button class that has a draw() method:

Button = class()
function Button:init(x,y)
    self.x, self.y = x, y
end
function Button:draw()
    -- Draw the button
end

Simple. Now consider a project where we want to draw lots of buttons via some drawall_buttons() function. Clearly we need to keep track of all the buttons to draw (e.g. in a Lua table) and then iterate the table drawing each button. Not very hard. But It would be more elegant to let the Button class keep this list of buttons and move drawall_buttons() into Button.drawall().

Again, not very hard, and one way to do this is to modify the Button class to add this. But if that class is being used elsewhere, this might cause problems. The buttons are added to a table which means in existing projects they “leak” since the table holds a reference to them (that existing projects know nothing about). A weak table of course solves this, but then that means you still need to track the buttons outside of the class to stop them being garbage collected. In other words, you want to customize the Button class only for the current project, but not break any of the others that use the class.

Which means you need to copy the class and modify it. Hmm, not good for code reuse, and a bug fixing mess as the variations proliferate over time.

So, instead, we don’t change the Button class at all, but instead add a mix-in outside of the Button tab (e.g. inside Main tab, but could be elsewhere):

Button.buttons = {}
Button.oinit = Button.init
function Button:init(...)
    Button.buttons[self] = true
    self:oinit(...)
end
function Button.drawall()
    for b in pairs(Button.buttons) do
        b:draw()
    end
end
function Button:remove()
    Button.buttons[self] = nil
end

Now we can draw all the buttons created using Button.drawall() and let the Button class track all those buttons. And without changing a line of code in the Buttons class.

An alternative to this would of course be to create a new derived class that handled the buttons collection. However, the huge advantage of the mixin is that any other class that already is derived from Button also inherits the mixin without needing to hack that class to change the inheritance. So you can use NumberButton etc etc and still gain access to the Button.drawall() function.