Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

classes with getters and setters

edited April 11 in Code Sharing Posts: 484

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 :)

Sign In or Register to comment.