Designing a helper module for function-bound properties

General discussion about LÖVE, Lua, game development, puns, and unicorns.
Post Reply
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Designing a helper module for function-bound properties

Post by airstruck »

I'm designing a small module to help with function-bound properties, similar to Object.defineProperty with get/set keys in JS. I wanted to get feedback on the API and the implementation.

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
I'd like to get input on a few details that complicate things. The way __index and __newindex work is a problem when a field has a non-nil value; the accessor and mutator won't be invoked in that case. So, what do we do about this?

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,
})
The solution I have in mind is this: when a property is defined and a field already exists for the same key, nil the field out and then pass the value into the mutator. I haven't done this yet because I'm not sure I like it. Does it seem acceptable? Should the mutator always be called when the property is defined, even if the current value is nil?

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
}
Also, if anyone knows of anything similar to this that already exists, please drop me a link. Thanks!
User avatar
ivan
Party member
Posts: 1919
Joined: Fri Mar 07, 2008 1:39 pm
Contact:

Re: Designing a helper module for function-bound properties

Post by ivan »

I see what you're going for, but I don't think this approach is worth it.
So much code just so you can write something as simple as "player.angle".
Also, in Lua you can return multiple values so "x,y = player.x,player.y" could be reduced to "x,y = player:getPosition()"
The strength of dynamic languages is that you don't have to "define" variable types.
If you are really concerned about type checking, then there are more clever ways.

PS. If you don't agree with me it's ok, just look at "rawget (t,k)" in the Lua docs
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Designing a helper module for function-bound properties

Post by airstruck »

ivan wrote:I see what you're going for, but I don't think this approach is worth it.
I think whether it's worth it depends on how it's used. It probably wouldn't be worth it for someone to use this as-is in a game. I think it will be worth it for library code, because you can write APIs that use the same properties for initialization and manipulation, and only have to document one property instead of a field and two functions per property. For example:

Code: Select all

local rect = Rect { top = 5, left = 10, width = 100, height = 100 }
print(rect.bottom) -- 105
Now say you want to change rect.bottom or rect.height, and your Rect knows to update the other field/property accordingly.

I think `rect.height = 120` is nicer than `rect:setHeight(120)` for the reasons mentioned (authors don't have to document two additional functions, and users can use the same properties they used to initialize the thing). Being able to write much more concise and slightly more readable code is also nice.
So much code just so you can write something as simple as "player.angle".
The examples here aren't really meant to justify its existence, they're just simple usage examples. To extend the example a bit, which of these would you rather write? Which would you rather read?

Code: Select all

player.angle = player.angle + 1
-- or
player:setAngle(player:getAngle() + 1)
Also, in Lua you can return multiple values so "x,y = player.x,player.y" could be reduced to "x,y = player:getPosition()"
Sure, if by "reduced" you mean something that is actually a few characters longer. Anyway, I see your point, I don't necessarily think x and y coordinate components would be a good candidate for this thing (I'd be more inclined to store position as a single field, though, as in player.position = { x, y }, but that's beside the point).
The strength of dynamic languages is that you don't have to "define" variable types.
If you are really concerned about type checking, then there are more clever ways.
Type checking isn't something I'm personally concerned about, it's just a nice side effect (for example if you're going to do some math or string manipulation or something, you'll end up with some sort of type checking, even if it's just an untrapped error). Out of curiosity, what other ways did you have in mind?

By the way, JavaScript, Python, Ruby, etc. are dynamically typed, and support properties, while languages like C#, D, F# and Pascal are statically typed and also support properties. I don't think properties really have much to do with type checking or static vs dynamic typing.
PS. If you don't agree with me it's ok, just look at "rawget (t,k)" in the Lua docs
I'm aware of rawget, but I'm not sure how it's relevant. It will just return nil since there won't be a field with that name. The rawset function would cause problems, but using it in this context seems like an intentional attempt to break things; I think the answer to that is just "don't do that."

Anyway, when you look at the long list of languages that do support properties, the argument that "it's not worth it" in Lua doesn't make much sense to me. I see no reason not to have the same convenience in Lua.
User avatar
ivan
Party member
Posts: 1919
Joined: Fri Mar 07, 2008 1:39 pm
Contact:

Re: Designing a helper module for function-bound properties

Post by ivan »

airstruck wrote:By the way, JavaScript, Python, Ruby, etc. are dynamically typed, and support properties, while languages like C#, D, F# and Pascal are statically typed and also support properties. I don't think properties really have much to do with type checking or static vs dynamic typing.
...
Anyway, when you look at the long list of languages that do support properties, the argument that "it's not worth it" in Lua doesn't make much sense to me. I see no reason not to have the same convenience in Lua.
Lua is a lightweight scripted language. Imagine buying a new Ferrari then attaching a huge semi-trailer to its back. Sure, you can still probably drive it around, but people would look at you funny.
My point is, take a look at the most successful Lua modules out there. They are very narrow in scope and solve specific problems.
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Designing a helper module for function-bound properties

Post by airstruck »

Lua is a lightweight scripted language. Imagine buying a new Ferrari then attaching a huge semi-trailer to its back. Sure, you can still probably drive it around, but people would look at you funny.
Did you really just compare one file containing about 70 sloc to a huge semi-trailer?
My point is, take a look at the most successful Lua modules out there. They are very narrow in scope and solve specific problems.
I'm not sure how you measure success here. If the thing does what it's supposed to do, it's succeeding at its job, isn't it? Or did you really mean "popular?" That's not something I'm interested in.

Do you think this isn't narrow in scope? I think it's about as narrow in scope as you can get. It might not solve a specific problem for you, but it solves one for me. I've used properties for a few different things now and I'm tired of writing this stuff each time. I wanted to move it into a separate, reusable module. If you don't like properties, or think 70 lines of code is too much to support properties, don't use this module.

I didn't ask if anyone thought this was "worth it," or narrow enough in scope, or too many lines of code. I asked fairly specific questions about design and implementation, and would prefer to keep the discussion focused on that kind of thing instead of defending its existence. It's useful to me, so I assumed that it might be useful to someone else, and decided to try to get some feedback on how other people would like something like this to work.
User avatar
ivan
Party member
Posts: 1919
Joined: Fri Mar 07, 2008 1:39 pm
Contact:

Re: Designing a helper module for function-bound properties

Post by ivan »

Look, I'm not trying to convince you of anything. :)
In my opinion: __index and __newindex add unnecessary overhead in this case.
You want to be careful with these two operators since they will be executing Lua code every time you access a table.
Also, I don't believe in using metatables for things like data storage (this could be argued in certain cases, but I digress).

If you need to run Lua code whenever a variable is modified:

Code: Select all

player.angle = player.angle + 1
You could try something like:

Code: Select all

player.angle = player.angle + 1
player.sync() -- trigger relevant Lua code
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Designing a helper module for function-bound properties

Post by airstruck »

ivan wrote:In my opinion: __index and __newindex add unnecessary overhead in this case.
Well, "this case" is a properties implementation, and __index/__newindex seem to be the only reasonable way to implement properties, so as far as I can tell they're actually necessary overhead in this case.

That's not a rhetorical statement. My point is, it doesn't make sense to always choose obsessive hyper-optimization over clean and convenient design. Sure, you can always avoid metamethods, or closures, or abstractions like ECS that favor clean design over performance, and your code will be a bit faster. But at the end of the day, if the game would have hit the target frame rate on the target platform without that extra speed, you would have been better off going for clean design over raw performance.

In other words, you can rip the seats and the air conditioner and the sound system out of your Ferrari and it will go a little faster. Your Ferrari will go 210 MPH, and mine will only go 200... but the speed limit is still only 60, so what's the point?
You want to be careful with these two operators since they will be executing Lua code every time you access a table.
See above. Also I'm sure you're aware that's not exactly true; they'll only fire on misses, and most likely the majority of the time they'll be firing because the client wanted them to.
Also, I don't believe in using metatables for things like data storage (this could be argued in certain cases, but I digress).
Thanks for pointing that out. It seems like meta-information, so I don't see any "philosophical" problem with storing it there, but I made a wrong assumption that each table would have a dedicated metatable, and that's not always going to be the case. I don't want to pollute the tables in question with those descriptors either, though, so I guess they'll probably have to end up in a weakmap (which will probably still be in the metatable unless anyone sees a practical reason not to keep them there).
If you need to run Lua code whenever a variable is modified:

Code: Select all

player.angle = player.angle + 1
You could try something like:

Code: Select all

player.angle = player.angle + 1
player.sync() -- trigger relevant Lua code
I actually had something like this in one project before switching to properties. There were a few problems with it. One, the "sync" function completely broke SRP because it had to know about everything that might need to be updated. Two, things had to either be flagged as dirty, which was a lot of extra cruft, or everything had to be updated even when most of it didn't need to be. Three, if anything depended on anything else to be in a valid state before it could be synced, you'd run into weird problems, so the order things got updated in had to be exactly right, which was a pain. The design was much cleaner and more manageable after moving to properties.
User avatar
zorg
Party member
Posts: 3470
Joined: Thu Dec 13, 2012 2:55 pm
Location: Absurdistan, Hungary
Contact:

Re: Designing a helper module for function-bound properties

Post by zorg »

Look what i found back in time :3
viewtopic.php?f=5&t=3539
Me and my stuff :3True Neutral Aspirant. Why, yes, i do indeed enjoy sarcastically correcting others when they make the most blatant of spelling mistakes. No bullying or trolling the innocent tho.
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Designing a helper module for function-bound properties

Post by airstruck »

zorg wrote:Look what i found back in time :3
viewtopic.php?f=5&t=3539
Thanks for the link! Unfortunately this implementation has a few deal-breakers for me (relying on inheritance, and a specific class library, and string concatenation). I'd like the solution to work on individual tables, with or without existing metatables, including tables representing classes.
User avatar
zorg
Party member
Posts: 3470
Joined: Thu Dec 13, 2012 2:55 pm
Location: Absurdistan, Hungary
Contact:

Re: Designing a helper module for function-bound properties

Post by zorg »

No worries, just thought i'd link it since i'm going over that subforum anyway :3
Me and my stuff :3True Neutral Aspirant. Why, yes, i do indeed enjoy sarcastically correcting others when they make the most blatant of spelling mistakes. No bullying or trolling the innocent tho.
Post Reply

Who is online

Users browsing this forum: Ahrefs [Bot], Google [Bot] and 7 guests