By a proxy table I mean a table that fires some sort of (bindable) event when a change is made
By invisible I mean that, aside from having a metatable attached, the table should behave exactly like a normal table: t[k] returns the value at key ‘k’, t[k]=v assigns value ‘v’ to key ‘k’, next(t) returns the first key-value pair, and next(t,k) returns the next key-value pair after key ‘k’ and its value t[k]
I currently know how to do everything but assignment (to do this you store the data inside of the table itself and attach a metatable with the __call function updating the key-value pair) and everything but iteration (to do this you store the data inside of table.Data and attach a mt with custom __index and __newindex, but if you want to iterate you have to do next(table.Data)) (or if you don’t care about table being an actual table then it can be a userdata (newproxy) which will error on next so that can help a little with debugging), but I don’t know how to do both at once
Is this possible?
The motivation over annoying setter and getter functions is brevity and modularity (I don’t want to worry if something is a normal table or a proxy table)
As more of a theoretical interest, proxies can be completely transparent as seen here. They can even work with next, pairs, ipairs, type, rawset, rawget, setmetatable, getmetatable, and for function proxies setfenv and getfenv can work too. However it should be noted that doing so does incur some overhead. Also, if a bug exists in the sandbox, it can sometimes manifest in strange, unpredictable ways that seem to defy logic and truth itself .
For catching assignment the keys cannot be present in the table and the __newindex metamethod must be defined. If the keys are present then it is a simple assignment and isn’t a ‘new index’. You can store the table’s data elsewhere, perhaps a hashmap local table at the topic of the proxy’s file. As for the next and similar rawset, rawequal, rawget, setmetatable, getmetatable, and other global functions, that is more difficult. You could use the full sandbox method as seen above, but I only recommend the sandbox for use while debugging and never in production. I’d recommend creating alternate versions of these functions that in a way are a proxy to those original functions. They will check if their argument is a proxy and if it is, call the original function on the proxy’s data otherwise just call the original function. In this way, your code doesn’t have to worry about what is a proxy and what isn’t because the alternate functions handle it for you.
I don’t really like the idea of sandboxing (even if its just making a custom next function) because it requires me to remember to copy and paste some code at the top of each module
Well I guess I will mark your response as the solution seeing that it’s probably impossible from the lack of responses
Generally new languages are written around ideas like this. Eons ago, only assembly exists. Then, by the grace of the computer gods that be, abstraction came into existence. Since C, language designers like those of C++, Java, Python, and Rust have all tried to make a specific feature more natural, and beautiful in their own paradigm. For someone who likes OOP, Java is a dream come true. For those of us who know how much overhead and abstraction goes into any OOP implementation, Java makes us want to go running back to C.
The point being, you could extend Lua to hide proxies, or make a new language to do it. Generally languages don’t hide pass-by-reference when one would expect pass-by-value because it is a recipe for bugs, but it can be done. If you want to take the dive, the tools are here.