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