Imitate Nesting behaviour in variable functions

Viewed 24

Is it possible to have a function called inside another keep access to the local variable scoping of the function it was called inside without passing data, the same way as a nested function does?

function x()
    DoSomething(modified)
end
function y()
    DoSomethingElse(modified)
    DoSomethingElseToo(something)
end

local scopedFunction = x
function main(input)
    local modified = GetModifiedVersion(input)
    local something = GetSomething(modified)
    scopedFunction()
end

This avoids having to copy "Modified" or pack the table and transfer unnecessary data.

1 Answers

Closures

Is it possible to have a function called inside another keep access to the local variable scoping of the function it was called inside [...]

Yes.

[...] without passing data, the same way as a nested function does?

The "nested function" is in that case called a closure and the variables that enter the closure's scope are called "upvalues" of the function. Example:

local function outer()
    local var = "hello world"
    local function closure()
        print(var)
    end
    closure() -- will print "hello world"
end

note that these upvalues are references to the variables, not copies of them! So modifying the variable even after creating the closure works just fine:

local function outer()
    local var = "hello world"
    local function closure()
        print(var)
    end
    closure() -- will print "hello world"
    var = "new value"
    closure() -- will print "new value"
end

So my first question would be: Why can't you use a closure here? Are you afraid of the cost of creating a new closure each function call? The alternatives - aside from passing the variables - won't be much cheaper:

Shared upvalue

You could make var a shared upvalue, setting it before calling the "inner" function:

local var
local function callee()
    print(var)
end
local function caller()
    var = "hello world"
    callee() -- will print "hello world"
end

The disadvantage of this is that var bleeds into the enclosing scope of the two functions. You could use a do-end block to prevent this.

Shared (usually global) environment

You don't even need to use an upvalue for this - you can just use the (usually global) environment both functions reside in:

local function callee()
    print(var)
end
local function caller()
    var = "hello world"
    callee() -- will print "hello world"
end

I would heavily recommend against this kind of so-called "global pollution" as the global environment will bleed into the scope of everything; in a constrained function environment this might be fine, but still you probably won't want to constantly index a table.

Function environments & debug hacks

Finally it is even possible to implement exactly the kind of scoping you're looking for by abusing the debug library - specifically its getlocal & setlocal functions as well as the getinfo function to get the function currently on the stack; you'll have to furthermore set the environment to one that leverages this by setting an __[new]index metatable which accesses the appropriate locals (or filling the environment table) _ENV of your function (you'd have to use setfenv for this on Lua 5.1, LuaJIT and older).

I am not providing the code for this as it is both (1) horribly inefficient and (2) a major code smell, but making the below work in Lua is possible:

-- implementing inherit_local_var_env is left as an exercise to the reader
local function callee()
    _ENV = inherit_local_var_env()
    print(var) -- accesses local var from the calling function
end
local function caller()
    local var = "hello world"
    callee() -- will print "hello world"
end
local function another_caller()
    local var = "another message"
    callee() -- will print "another message"
end
Related