Implement inheritable metamethods without stubs

In __newindex metamethods of each Class detect if
a metamethod is assigned. In that case, propogate assignment
to all child classes that do not overwrite the metamethod.
Track set of metamethods each class defines itself.
On subclassing, copy all parent metamethods to child.

Benefits of this approach:

* Objects only have metamethods when they are, in fact, defined.
  Ignoring overhead, the problem with stubs is that other libraries,
  seeing a stub metamethod, could try to call it and get an error
  if it's missing in the class. E.g. various deep comparison functions
  often try to detect and use __eq.
* Non-function metafields such as __metatable can be used.
* Less overhead when using metamethods (but more when defining them).
This commit is contained in:
mpeterv 2015-11-19 12:23:39 +03:00
parent e7d8bc00d6
commit d9f00351b2
2 changed files with 40 additions and 19 deletions

View File

@ -42,17 +42,51 @@ local function _setClassDictionariesMetatables(aClass)
end
end
local all_metamethods_list = { '__add', '__band', '__bor', '__bxor', '__bnot', '__call', '__concat',
'__div', '__eq', '__ipairs', '__idiv', '__le', '__len', '__lt', '__mod',
'__mul', '__pairs', '__pow', '__shl', '__shr', '__sub', '__tostring', '__unm' }
local all_metamethods = {}
for _, metamethod in ipairs(all_metamethods_list) do
all_metamethods[metamethod] = true
end
local function _propogateMetamethod(aClass, key, value)
for subclass in pairs(aClass.subclasses) do
if not subclass.__metamethods[key] then
subclass.__instanceDict[key] = value
_propogateMetamethod(subclass, key, value)
end
end
end
local function _updateClassDict(aClass, key, value)
if all_metamethods[key] then
if value == nil then
aClass.__metamethods[key] = nil
value = aClass.super and aClass.super.__instanceDict[key]
else
aClass.__metamethods[key] = true
end
_propogateMetamethod(aClass, key, value)
end
aClass.__instanceDict[key] = value
end
local function _setClassMetatable(aClass)
setmetatable(aClass, {
__tostring = function() return "class " .. aClass.name end,
__index = aClass.static,
__newindex = aClass.__instanceDict,
__newindex = _updateClassDict,
__call = function(self, ...) return self:new(...) end
})
end
local function _createClass(name, super)
local aClass = { name = name, super = super, static = {}, __mixins = {}, __instanceDict={} }
local aClass = { name = name, super = super, static = {}, __mixins = {}, __instanceDict = {}, __metamethods = {} }
aClass.subclasses = setmetatable({}, {__mode = "k"})
_setClassDictionariesMetatables(aClass)
@ -61,17 +95,9 @@ local function _createClass(name, super)
return aClass
end
local function _createLookupMetamethod(aClass, name)
return function(...)
local method = aClass.super[name]
assert( type(method)=='function', tostring(aClass) .. " doesn't implement metamethod '" .. name .. "'" )
return method(...)
end
end
local function _setClassMetamethods(aClass)
for _,m in ipairs(aClass.__metamethods) do
aClass[m]= _createLookupMetamethod(aClass, m)
local function _setSubclassMetamethods(aClass, subclass)
for m in pairs(aClass.__metamethods) do
subclass.__instanceDict[m] = aClass.__instanceDict[m]
end
end
@ -97,10 +123,6 @@ end
local Object = _createClass("Object", nil)
Object.static.__metamethods = { '__add', '__band', '__bor', '__bxor', '__bnot', '__call', '__concat',
'__div', '__eq', '__ipairs', '__idiv', '__le', '__len', '__lt', '__mod',
'__mul', '__pairs', '__pow', '__shl', '__shr', '__sub', '__tostring', '__unm' }
function Object.static:allocate()
assert(type(self) == 'table', "Make sure that you are using 'Class:allocate' instead of 'Class.allocate'")
return setmetatable({ class = self }, self.__instanceDict)
@ -117,7 +139,7 @@ function Object.static:subclass(name)
assert(type(name) == "string", "You must provide a name(string) for your class")
local subclass = _createClass(name, self)
_setClassMetamethods(subclass)
_setSubclassMetamethods(self, subclass)
_setDefaultInitializeMethod(subclass, self)
self.subclasses[subclass] = true
self:subclassed(subclass)

View File

@ -171,7 +171,6 @@ describe('Metamethods', function()
describe('An instance', function()
it('has a tostring metamethod, returning a different result from Object.__tostring', function()
assert.not_equal(Peter.__tostring, Object.__tostring)
assert.equal(tostring(peter), 'instance of class Peter')
end)
end)