With Statement

lua-users home
wiki

Showing revision 3

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

See Also


RecentChanges · preferences
edit · history · current revision
Edited July 5, 2010 3:12 pm GMT (diff)