With Statement

lua-users home
wiki

Abstract

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.

Method

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.

Solution

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.

Save a global scope into a variable

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();

Extend an object scope with _G

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();

Test

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?

Lua 5.2

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'
--DavidManura

A dynamically scoped approach

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.

--DirkLaurie

See Also


RecentChanges · preferences
edit · history
Last edited February 26, 2013 7:37 am GMT (diff)