Mutable Functions |
|
We first create a decorator function [1] called mutable
, i.e. a function that returns a function that is a variant (wrapper) of the function passed in:
function mutable(func) local currentfunc = func local function mutate(func, newfunc) local lastfunc = currentfunc currentfunc = function(...) return newfunc(lastfunc, ...) end end local wrapper = function(...) return currentfunc(...) end return wrapper, mutate end
Here, the function wrapper provides a level of indirection to the original function (currentfunc) and allows the identity of currentfunc, which is an up-value, to be mutated. During mutation, we allow the function replacing another function to know the identity of the function it is replacing, thereby allowing cascading effects where one function overrides or filters the behavior of the previous functions.
Example usage:
local sqrt, mutate = mutable(math.sqrt) assert(sqrt(4) == 2) assert(sqrt(-4) ~= sqrt(-4)) -- NaN mutate(sqrt, function(old, x) return x < 0 and old(-x) .. "i" or old(x) end) assert(sqrt(4) == 2) assert(sqrt(-4) == "2i")
There's probably not much use doing the above rather than
local function sqrt(x) return x < 0 and math.sqrt(-x) .. "i" or math.sqrt(x) end
However, here's how we can simulate table semantics with functions:
local t, mutate = mutable(function() end) mutate(t, function(old, x) if x == 1 then return "first" else return old(x) end end) mutate(t, function(old, x) if x == 2 then return "second" else return old(x) end end) mutate(t, function(old, x) if x == 3 then return "third" else return old(x) end end) assert(t(1) == "first" and t(2) == "second" and t(3) == "third")
The syntax for setting and the efficiency are lacking of course, but we gain in a more general semantics:
local t, mutate = mutable(function() end) mutate(t, function(old, x,y) if x == 1 then return "first" else return old(x,y) end end) mutate(t, function(old, x,y) if x == 2 then return "second" else return old(x,y) end end) mutate(t, function(old, x,y) if x > 2 then return "large number" else return old(x,y) end end) mutate(t, function(old, x,y) if y ~= 0 then return "off axis", math.sqrt(x^2+y^2) else return old(x,y) end end) assert(t(1,0) == "first" and t(2,0) == "second" and t(5,0) == "large number" and t(3,4) == "off axis") assert(select(2, t(3,4)) == 5)
We now have the fallback semantics of metamethods (e.g. __index
) as well as the ability to index and return multiple values, the latter of which we didn't have before with Lua tables.
Lets clean up the syntax with a few helper functions that wrap mutate
:
local SET = function() end -- unique key local MUTATE = function() end -- unique key -- decorator function for adding methods. function mutable_helpers(func, mutate) mutate(func, function(old_func, ...) if select(1, ...) == SET then local k = select(2, ...) local v = select(3, ...) mutate(func, function(old_func, ...) if select(1, ...) == k then return v else return old_func(...) end end) else return old_func(...) end end) mutate(func, function(old_func, ...) if select(1, ...) == MUTATE then local new_func = select(2, ...) mutate(func, function(old_func, ...) return new_func(old_func, ...) end) else return old_func(...) end end) return func end
The mutable_helpers
is a decorator function that adds support for what are semantically method calls on a mutable function representing an object. These methods are SET
(for setting a table value) and MUTATE
(alternate syntax for the mutate
function). The SET
and MUTATE
are unique keys identifying the methods. These take advantage of the fact that functions are objects, which have unique identities (in special cases, strings might have been used instead for keys--e.g. "set"
and "mutate"
).
So, we can now use method-like calls in the form of message passing to access the simulated table:
local t = mutable_helpers(mutable(function() end)) t(MUTATE, function(old, ...) local x = select(1, ...) if type(x) == "number" and x > 2 then return "large" else return old(...) end end) t(SET, 1, "first") t(SET, 2, "second") assert(t(1) == "first", t(2) == "second", t(5) == "large")
Optionally, we can modify the default metatable on functions to use regular Lua table syntax. (This is the only time a real Lua table is used, but it is only an artifact of the Lua meta mechanism for supporting the table syntax, and it could be avoided by some patch to Lua.)
-- Enable table get/set syntax. -- Warning: uses debug interface function enable_table_access() local mt = { __index = function(t,k) return t(k) end, __newindex = function(t,k,v) return t(SET, k, v) end, } debug.setmetatable(function() end, mt) end
A table constructor helper function will also be defined:
function T() return mutable_helpers(mutable(function() end)) end
Example usage:
local t = T() t[1] = "first" t[2] = "second" t[3] = "third" assert(t[1] == "first" and t[2] == "second" and t[3] == "third" and t[4] == nil)
So, in terms of expression, this suggests that tables are not a necessary feature of Lua, and it may well be possible to remove them entirely from the language, though we likely wouldn't want to do that for efficiency concerns.
More practically, maybe this suggests that the concepts of tables and functions could be further unified in the language, though, for efficiency, preserving the distinction in the underlying implementation.
-- setting properties on an object obj.color = "blue" obj["color"] = "blue" obj:size(10,20,30) -- traditional syntax, method call style obj("size") = (10,20,30) -- multivalued setter syntax, function style obj["size"] = (10,20,30) -- multivalued setter syntax, table style obj.size = (10,20,30) -- multivalued setter syntex, table property style x,y = obj("position") -- multivalued getter syntax, function style x,y = obj["position"] -- multivalued getter syntax, table style x,y = obj.position -- multivalued getter syntex, table property style obj[10,20] = 2 -- multivalued keys, table style obj(10,20) = 2 -- multivalued keys, function style
Related discussion: LuaList:2007-07/msg00177.html - "Multiple return value in __index metamethod"