First, a quick example:
Code: Select all
local Property = require 'property'
local player = {
name = "Bob",
maxHealth = 100,
}
Property.define(player, 'health', {
get = function (self, key)
return self._health
end,
set = function (self, key, value)
self._health = math.min(math.max(0, value), self.maxHealth)
end,
})
Property.define(player, 'angle', {
get = function (self, key)
return self._angle
end,
set = function (self, key, value)
self._angle = value % 360
end,
})
player.health = 120
player.angle = -20
print(player.health) -- 100
print(player.angle) -- 340
Code: Select all
local player = {
angle = 45,
}
Property.define(player, 'angle', {
get = function (self, key)
return self._angle
end,
set = function (self, key, value)
self._angle = value % 360
end,
})
Another related question: what to do when an accessor is defined but no mutator is defined? Setting a value for that field would break the accessor because field resolution can't fall back to __index now. My current solution is to provide a no-op function for accessors and mutators that are omitted when defining properties. Does that seem reasonable?
Another thing I'm not sure about is the "key" parameter to the get and set functions. Normally you won't need this parameter because you'll be writing dedicated get/set functions and you'll already know what the key is. But, it can also be useful to write a general-purpose get or set function that can be reused for multiple similar properties, and then you'd need that key parameter. I'm not sure whether to remove "key" entirely, or move it after "value" for the setter (seems like an unconventional order), or just leave it as it is.
Here's what I've written so far, it's completely untested at this point but I'm leaving it here in case anyone wants to comment on implementation. The indexStrategy/newindexStrategy stuff feels redundant but should be faster than doing type checking every time an accessor/mutator is invoked. I'm open to suggestions on eliminating that redundancy.
Code: Select all
-- property.lua
local function nothing () end
local function getAccessors (self, key)
local meta = getmetatable(self)
local descriptor = meta.definedProperties[key]
if descriptor then
return descriptor.get or nothing
end
return nil, meta.indexFallback
end
local function getMutators (self, key)
local meta = getmetatable(self)
local descriptor = meta.definedProperties[key]
if descriptor then
return descriptor.set or nothing
end
return nil, meta.newindexFallback
end
local indexStrategy = {
['function'] = function (self, key)
local get, fallback = getAccessors(self, key)
if get then return get(self, key) end
return fallback(self, key)
end,
['table'] = function (self, key)
local get, fallback = getAccessors(self, key)
if get then return get(self, key) end
return fallback[key]
end,
['nil'] = function (self, key)
local get = getAccessors(self, key)
if get then return get(self, key) end
end,
}
local newindexStrategy = {
['function'] = function (self, key, value)
local set, fallback = getMutators(self, key)
if set then return set(self, key, value) end
return fallback(self, key, value)
end,
['nil'] = function (self, key, value)
local set = getMutators(self, key)
if set then return set(self, key, value) end
end,
}
return {
define = function (t, key, descriptor)
local meta = getmetatable(t) or setmetatable(t, {})
if not meta.definedProperties then
meta.definedProperties = {}
meta.indexFallback = meta.__index
meta.newindexFallback = meta.__newindex
meta.__index = indexStrategy[type(meta.__index)]
meta.__newindex = newindexStrategy[type(meta.__newindex)]
end
meta.definedProperties[key] = descriptor
-- TODO: clear field from table if it exists, call mutator with that value?
end,
obtain = function (t, key)
local meta = getmetatable(t)
if not meta then return end
local definedProperties = meta.definedProperties
if not definedProperties then return end
return key == nil and definedProperties or definedProperties[key]
end
}