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
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) local function _setClassMetatable(aClass)
setmetatable(aClass, { setmetatable(aClass, {
__tostring = function() return "class " .. aClass.name end, __tostring = function() return "class " .. aClass.name end,
__index = aClass.static, __index = aClass.static,
__newindex = aClass.__instanceDict, __newindex = _updateClassDict,
__call = function(self, ...) return self:new(...) end __call = function(self, ...) return self:new(...) end
}) })
end end
local function _createClass(name, super) 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"}) aClass.subclasses = setmetatable({}, {__mode = "k"})
_setClassDictionariesMetatables(aClass) _setClassDictionariesMetatables(aClass)
@ -61,17 +95,9 @@ local function _createClass(name, super)
return aClass return aClass
end end
local function _createLookupMetamethod(aClass, name) local function _setSubclassMetamethods(aClass, subclass)
return function(...) for m in pairs(aClass.__metamethods) do
local method = aClass.super[name] subclass.__instanceDict[m] = aClass.__instanceDict[m]
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)
end end
end end
@ -97,10 +123,6 @@ end
local Object = _createClass("Object", nil) 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() function Object.static:allocate()
assert(type(self) == 'table', "Make sure that you are using 'Class:allocate' instead of 'Class.allocate'") assert(type(self) == 'table', "Make sure that you are using 'Class:allocate' instead of 'Class.allocate'")
return setmetatable({ class = self }, self.__instanceDict) 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") assert(type(name) == "string", "You must provide a name(string) for your class")
local subclass = _createClass(name, self) local subclass = _createClass(name, self)
_setClassMetamethods(subclass) _setSubclassMetamethods(self, subclass)
_setDefaultInitializeMethod(subclass, self) _setDefaultInitializeMethod(subclass, self)
self.subclasses[subclass] = true self.subclasses[subclass] = true
self:subclassed(subclass) self:subclassed(subclass)

View File

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