Codea had ever since a class
function that helps tables with inheritance. The odd thing about that algorithm it is that it shallow-copies properties on subclassing (instead of when actually instancing from a class) and therefor that inheritance chain between the base class and the new subclass becomes kinda useless at some cases because newly added and updated properties don’t propagate into that subclass anymore. Purely logically it appears to be wrong either because subclasses should to be loosely coupled as long a possible until they finally get instantiated. This is also the reason for increased memory consumption, because the more subclasses you create the more copies of your base class properties you will have (although that memory increase is not very high).
The second annoying thing is that we can not have static properties - that are properties that get assigned once and then can only be read, not overridden. (This is not Codea’s fault by any means just another point that drove my to (try to) improve this.)
I just recently worked on a project where I needed this concept of immutable variables. I though a lot about this and tried different approaches to solve the problem; from simple ones to highly cached and optimized ones, but eventually settled on the fallowing solution, which is a mix in between the two:
-- (c) 2018 kontakt@herrsch.de
if _VERSION:match("[%d/.]+") <= "5.1" then -- Lua version
local _pairs = pairs
function pairs(array)
local mt = getmetatable(array)
return (mt and (mt.__pairs or _pairs) or _pairs)(array)
end
end
local wrapper = {__call = table.unpack or unpack}
local function wrap(value, permission) return setmetatable({value, tostring(permission)}, wrapper) end
local function unwrap(value, permission) if type(value) == "table" and getmetatable(value) == wrapper then return unwrap(value()) end return value, permission end -- recursive
function get(value) return wrap(value, "get") end
function set(value) return wrap(value, "set") end
function class(base)
local proxy = {}
local stash = {}
local getters = {}
local setters = {}
function traverse(array)
return pairs(stash or array)
end
function copy(array) -- shallow (recursive)
if type(array) ~= "table" then return {} end
local properties = copy(array.super)
for k, v in pairs(array) do
if k ~= "super" then
properties[k] = unwrap(v)
end
end
return properties
end
function instantiate(array, ...)
assert(type(array) == "table", string.format("attempt to inherit from invalid base `%s`", array))
local instance = class()
for k, v in pairs(copy(array)) do instance[k] = v end
if instance.init then instance:init(...) end
return instance
end
function index(array, property)
return string.format("%s: %s", array, property)
end
function convert(value)
if type(value) == "table" and not getmetatable(value) then
return instantiate(value) -- convert table value into class instance value
end
return value
end
function peek(array, property)
local value = stash[property] or (stash.super and stash.super()[property])
local id = index(array, property)
local getter = getters[id]
return type(getter) == "function" and getter() or value
end
function poke(array, property, value)
local value, permission = unwrap(value)
local id = index(array, property)
local getter, setter = getters[id], setters[id]
local is_getter = type(getter) == "function"
local is_setter = type(setter) == "function"
assert(not permission or type(value) == "function", string.format("getter/setter property `%s` must be a function value", property))
assert(not permission or not ((permission == "get" and is_getter) or (permission == "set" and is_setter)), string.format("attempt to redefine permission of property `%s`", property))
if permission == "get" then
getters[id] = value -- cache get method
stash[property] = value() -- make property visible to public (e.g. pairs iterator function)
return stash[property]
elseif permission == "set" then
setters[id] = value -- cache set method
return nil
end
if is_setter then
stash[property] = setter(convert(value)) -- update publicly visible value of a setter property
elseif not is_getter and not is_setter then
stash[property] = convert(value) -- assign value of a property which is not a getter or a setter
end
return stash[property]
end
poke(proxy, "super", get(function() return convert(base) end))
return setmetatable(proxy, {__index = peek, __newindex = poke, __pairs = traverse, __call = instantiate})
end
This code comes from my personal löve2d project and should work with any Lua version. I didn’t test it in Codea yet but it should work just fine.
The first if
block overrides the default pairs()
iterator function to be aware of classes own implementation __pairs
. This should happen automatically on Lua 5.3 (which is used by Codea) but in case it doesn’t, that code accounts for it.
Note that if you paste the code into your project, Codea’s class
function will be overridden by this one as well.
Just like before you create new classes with `Human = class()`.
You add your properties like before with Human.message = "hello world"
.
You read your properties like before with print(Human.message)
.
However, whenever you need a property to have restricted read/write access you must use getters and setters (get()
and set()
functions respectively). Internally these functions simply mark the properties to behave a little different. NOTE that getters and setters must be defined as functions. I prefer to use closures, e.g. get(function() ... end)
If you want a property to store a read only value then call Human.foobar = get(function() return prop end)
. From now on you can only access it to read like print(Human.foobar)
. Trying to (re)assign it however, will fail: e.g. Human.foobar = "new value"
.
If you want a property to be able to update itself then use a setter like Human.foobar = set(function(new_value) prop = new_value end)
**Ok, but how are these getters and setters any different from regular property assignment, you ask?** Here is an example of hidden/private properties...
local Human = class{foobar = "foobar"} -- custom base class closure
local Thief = class(Human)
function Human:init(msg)
local hidden_message = msg -- this variable is private and can not be seen from outside the constructor!
self.msg = get(function() -- define a getter method
return hidden_message
end)
self.msg = set(function(value) -- define a setter
hidden_message = value
end)
end
function Thief:init()
Human.init(self)
end
function Thief:speak(message)
self.msg = message -- assign value to property through the setter method
print(self.msg) -- call getter (which accesses a private variable to get the value)
end
local Bob = Thief()
Bob:speak("whatever you say, sir.")
print(Thief.foobar)
The most interesting part about this classes approach is the proxy. Whenever you assign a property the __newindex
metamethod is called. But because internally all properties are saved into a local variable (upvalue) the proxy always remains empty, allowing the __newindex metamethod to be invoked repeatedly. Same applies to the __index metamethod…
Btw. if you have suggestions for improvements or if you find bugs, please report them here. Have fun