So… I started working on the routing library some more, but I couldn't help but notice how
Event.pull still uses strings as filters. We have an object-oriented system, so I figure it makes more sense to use classes as filters. When I was about to implement that, I realized the type checking for classes would be a bit weird, since they aren't really considered regular objects.
To be clear, I'd want to be able to do things like this:
System.print(MyClass) --> Prints using toString method from see.rt.Class
System.print(typeof(MyClass)) --> Prints see.rt.Class
System.print(MyClass:getClass()) --> ^
ArgumentUtils.check(1, someClass, Class) --> True if someClass is a class.
Turns out there are tons of problems with having classes as first-class objects. You can't just set the metatable of the class to index
Class, because then instances of the class will have access to
Class methods. Additionally,
Class is a class, so
Class would have a metatable which indexes itself. (Yeah, I know that's confusing.)
The solution is to set a metatable for the class in which new indices are put in a helper table and index access is handled by a function which checks whether the table is a class or not and acts accordingly. If the table is a class, the system tries to get the field from
Class and then
Object. If the table is not a class, the system just loops upwards in the super class hierarchy until a matching field is found. With this system, we can call methods from normal objects and from classes, and the underlying system sorts out all the kinks for us!
There's still a small problem, though. Consider the following code which uses the current class model:
function MyClass:init(x)
MySuperClass.init(self, x, "hi")
end
function MyClass:doStuff(x, y)
MySuperClass.doStuff(self, x, y)
print("I told the super class to do some stuff.")
end
This is currently the way of calling super methods and super constructors. This is broken with the new class model, because indexing the class itself doesn't access the class, it accesses
Class and then maybe
Object. Luckily, I didn't like this syntax much anyways and I think the new syntax is a bit prettier.
function MyClass:init(x)
self:super(MySuperClass):init(x, "hi")
end
function MyClass:doStuff(x, y)
self:super(MySuperClass):doStuff(x, y)
print("I told the super class to do some stuff.")
end
Metatable magic is involved in this, and it can break and do weird stuff if you use it incorrectly, so I'm thinking of trying something else. For example, if you just call
self:super without calling a method on it,
self is just left there with a different metatable, which will index that super class you gave it the next time you try to use it.
But other than that, I think it looks nicer than the old way.
Here's some test code I was working with to see if this stuff was even possible (you can run it right inside the standard Lua interpreter if you want):
local Object = { name = "Object", __table = { } }
local Class = { name = "Class", __table = { }, __super = Object }
local MyClass = { name = "MyClass", __table = { }, __super = Object }
local MySubClass = { name = "MySubClass", __table = { }, __super = MyClass }
local classes = { [Object] = true, [Class] = true, [MyClass] = true, [MySubClass] = true }
local function newindex(t, k, v)
--print(k)
t.__table[k] = v
end
local function index(t, k)
-- Class access.
if classes[t] then
local v = rawget(Class, "__table")[k]
if v then
return v
end
v = rawget(Object, "__table")[k]
return v
end
-- Instance access.
local class = rawget(t, "__class")
repeat
local v = rawget(class, "__table")[k]
if v then
return v
end
class = rawget(class, "__super")
until not class
end
local mt = { __index = index, __newindex = newindex }
setmetatable(Object, mt)
setmetatable(Class, mt)
setmetatable(MyClass, mt)
setmetatable(MySubClass, mt)
function Object:toString()
return tostring(self):sub(8, -1)
end
function Object:super(class)
local mt = getmetatable(self)
return setmetatable(self, { __index = function(t, k, v)
local ret = rawget(class, "__table")[k]
setmetatable(t, mt)
return ret
end })
end
function Class:new(...)
local instance = setmetatable({ __class = self }, { __index = mt.__index })
instance:init(...)
return instance
end
function Class:toString()
return self.name
end
function MyClass:init()
print("hi")
self.x = 1337
end
function MyClass:myMethod()
return self.x + 1
end
function MyClass:toString()
return tostring(self.x)
end
function MySubClass:init()
self:super(MyClass):init()
self.x = self.x * 3
end
local myInstance = MyClass:new()
local myOtherInstance = MySubClass:new()
print(myInstance:myMethod())
print(myInstance:toString())
print(MyClass:toString())
print(myOtherInstance.x)
Note the
Object:super method. It's a bit derp.
tl;dr: I get sidetracked really easily.