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 6:37 am GMT (diff)