With Statement |
|
In several object oriented languages a with statement is implemented.
The structure like "with ... as" has a simple solution with a temporary variable, when one assigns the scope to the temporary variable and uses it whenever is needed.
The more complicated case, when "with" is used in order to extend the scope implicitly, like in the following example:
with (obj) { some_method(); }
Lua doesn't provide such a structure by design. A simple solution is presented on this page.
A basic library in lua provides enough instruments to implement something like "with" statement.
There's two functions to operate with environment: setfenv() and getfenv(), as well as a table _G exists.
"with" structure in common sense extends a scope with the provided object. It is possible with metadata manipulations (see getmetatable() and setmetatable()).
Lets see how to get such a thing in a Lua.
The subject structure can be implemented with the following function:
function with(env) local oldenv = getfenv(2); setfenv(2, env); return function() setfenv(2, oldenv) end; end;
The return value is a function to restore the initial environment. Then, an outline of the structure will be:
local endwith = with (env)
...
any_method();
...
endwith();
The main drawback of the method here is that we have no access to a variables from the initial scope. There're two simple ways to overcome the problem.
A slightly modified function:
function with(env) local oldenv = getfenv(2); setfenv(2, env); return function() setfenv(2, oldenv) end, _G; end;
Now the global scope is available:
local endwith, _G = with (env) ... any_method(); ... _G.print("a function from a global scope"); ... endwith();
Another solution extends a specified scope with a _G:
function with(env) local oldenv = getfenv(2); local mt = getmetatable(env) or {}; mt.__index = _G; setmetatable(env, mt); setfenv(2, env); return function() setfenv(2, oldenv) end, _G; end;
Here the second return value may be omitted.
A global scope is available implicitly, like in othe languages:
local endwith = with (env) ... any_method(); ... print("a function from a global scope"); ... endwith();
And a final test code:
-- tiny environment with the only function Test = { output = function() print("\tTest.output()") end }; -- function for environment test function output() print("Top-level output()") end; -- the tricky with function function with(env) local oldenv = getfenv(2); local mt = getmetatable(env) or {}; mt.__index = _G; setmetatable(env, mt); setfenv(2, env); return function() setfenv(2, oldenv) end, _G; end; function main() output(); --[[ *** local function output() print("*** the substituted function!"); end; --]] local endwith, _G = with(Test); --[[ global environment still in _G table ]] _G.print("\texplicit print() invocation"); --[[ implicit invocation ]] print("\timplicit print() invocation"); --[[ call output here ]] output(); endwith(); --[[ environment restored outside of "with" ]] output(); end; main();
You can uncomment the function marked with "***" for fun. It reveals a limitation, that one must keep in mind.
--IgorBogomazov?
LuaFiveTwo replaces getfenv
and setfenv
with _ENV
, allowing with
to be implemented as follows.
function with(...) local envs = {...} local f = (type(envs[#envs]) == 'function') and table.remove(envs) local env if #envs == 1 then env = envs[1] else local mt = {} function mt.__index(t, k) for i=1,#envs do local v = rawget(envs[i], k) if v ~= nil then return v end end end env = setmetatable({}, mt) end if f then return f(env) else return env end end -- test local function print2(...) print('printing', ...) end print 'one' with({print=print2}, _ENV, function(_ENV) print('two', math.sqrt(4)) end) print 'three' do local _ENV = with({print=print2}, _ENV) print('four', math.sqrt(4)) end print 'five'
Instead of using a do...end block to limit the scope of a 'with' statement, which does lexical scoping, one could explicitly switch it on or off, as in the following example.
with(math,string,table) print("sin(1) = "..sin(1)) --> 0.8414709848079 print(format("The answer is %d",42)) --> The answer is 42 print(concat({"with","table","library"}," ")) --> with table library without(string) print(pcall(format,"The answer is %d",42)) --> false attempt to call a nil value
The way in which this sort of 'with' statement works, is by chaining the __index fields of the metatables for _ENV, math, string and table. Here is the code.
with = function(...) local ENV = _ENV local mt = getmetatable(ENV) for k=1,select('#',...) do local tbl=select(k,...) local tblmt = getmetatable(tbl) if not mt then setmetatable(ENV,{__index=tbl}) elseif not tblmt then setmetatable(tbl,{__index=mt.__index}); mt.__index=tbl; elseif tbl~=mt.__index then error("bad argument to 'with': metatable already in use") end ENV, mt = tbl, tblmt end end
The arguments appearing in the same 'with' statement are inserted in oder of decreasing priority. When 'concat' is not found in _ENV, math is searched; not in math, then string; not in string, then table.
However, the most recent 'with' statement takes precedence over all previous ones.
Note that, because of the "fallback" nature of metamethods, _ENV itself is always searched first.
The 'without' statement simply looks for the table in the chain, removes it, and rejoins the rest of the chain.
without = function(...) for k=1,select('#',...) do local mt = getmetatable(_ENV) if mt==nil then return end local tbl=select(k,...) local tblmt = getmetatable(tbl) while mt do local index = mt.__index if index == nil then mt=nil elseif index == tbl then mt.__index = (tblmt and tblmt.__index) or nil; mt=nil else mt=getmetatable(index) end end end end
A side effect of this form of 'with' is that it implies an object hierarchy. After 'with(math,string,table)', for example 'math.sort' would be recognized until such time as 'without(table)' is executed.
It is also possible to insert a table in the chain directly below any table that is already in, or to remove a table from the 'with' chain only if it has lower priority than another table, thus:
do local with, without = with, without with_this = function(_ENV,...) with(...) end without_this = function(_ENV,...) without(...) end end with_this(table,string) -- string comes below table without_this(table,string) -- string is disabled only if it -- is below table
It is important to make upvalues for 'with' and 'without', otherwise they will not be found inside the functions since _ENV is being redefined.