与 JS 相比,Lua 是否有任何“奇怪”的函数参数变量范围?

dv1*_*729 5 javascript lua

当使用 Lua 使用闭包和递归从树状结构生成字符串时,我发现了意外的行为。

我把代码简化成这样,做了一个JS版本。JS 版本按预期工作,但我不明白为什么或到底如何。

代码是:

function union(children)
    local union = {}
    function union:method(p)
        print("union method", p, #children)

        -- rename p2 to p to make it work as expected
        function f(p2, children)
            print("union f", p, #children)
            local result = nil
            if #children == 1 then
                result = children[1]:method(p)
            elseif #children == 2 then
                result = children[1]:method(p) .. children[2]:method(p) 
            else
                local first_child = table.remove(children)
                result = first_child:method(p) .. f(p, children)
            end
            return result
        end
        return f(p, children)
    end        
    return union
 end

 function transform(children)
    local child = children[1]
    local transform = {}
    function transform:method(p)
        print("transform start")
        res = child:method(p .. "TRANSFORMED  ")
        print("transform end")
        return res
    end        
    return transform
 end

 function leaf()
    local leaf = {}
    function leaf:method(p)
        return p
    end
    return leaf
 end

root = union({leaf(), leaf(), transform({union({leaf()})}) })
print(root:method("p"))
Run Code Online (Sandbox Code Playgroud)

输出为:“pTRANSFORMED pTRANSFORMED pTRANSFORMED”

但我期望:“pTRANSFORMED pp”

总之,我不明白为什么“转换”节点影响三片叶子而不是仅仅影响一片。

JS代码是:

function union(children){
    const union = {}
    union.getField = (p) =>{
        function f(p2, children){
            console.log("union", p, p2, children.length)
            let result = null
            if (children.length == 1 ){
                result =  children[0].getField(p)
            }else if ( children.length == 2 ){
                result = children[0].getField(p) + children[1].getField(p) 
            }else {
                const first_child = children.pop()
                result = first_child.getField(p) + f(p, children) 
            }
            return result
        }
        return f(p, children)
    }      
    return union
}

 function transform(children){
    const child = children[0]
    const transform = {}
    transform.getField = (p)=>{
        return child.getField(p + "TRANSFORMED  ")
    }      
    return transform
 }

 function leaf(){
    const leaf = {}
    leaf.getField = (p) =>{
        return p
    }
    return leaf
 }

const root = union([leaf(), leaf(), transform([union([leaf()])]) ])
console.log(root.getField("p"))
Run Code Online (Sandbox Code Playgroud)

Oka*_*Oka 3

The function f is not lexically scoped to each union:method.

function foo()
end
Run Code Online (Sandbox Code Playgroud)

is equivalent to

foo = function ()
end
Run Code Online (Sandbox Code Playgroud)

Every a time a union:method is called, the function f is redefined in the lexical scope of the chunk.

This creates a closure around the upvalue p when last redefined.

A couple of print statements bring this to light:

function union(children)
    local union = {}

    function union:method(p)
        print("union method", p, #children)

        function f(p2, children)
            -- ...
            print('f<p>', p)
            -- ...
        end

        print('f redefined', f)

        return f(p, children)
    end

    return union
end
Run Code Online (Sandbox Code Playgroud)
function union(children)
    local union = {}

    function union:method(p)
        print("union method", p, #children)

        function f(p2, children)
            -- ...
            print('f<p>', p)
            -- ...
        end

        print('f redefined', f)

        return f(p, children)
    end

    return union
end
Run Code Online (Sandbox Code Playgroud)

root:method("p") causes f to close around the value of p as "p", but the first child method called is the transformed union,

union method    p   3
f redefined function: 0x5652063c80c0
f<p>    p
union method    pTRANSFORMED    1
f redefined function: 0x5652063c8780
f<p>    pTRANSFORMED
f<p>    pTRANSFORMED
pTRANSFORMED  pTRANSFORMED  pTRANSFORMED
Run Code Online (Sandbox Code Playgroud)

which causes the redefinition of f to close around its argument p .. "TRANSFORMED". The pseudo-recursive call is made right after, executing

local first_child = table.remove(children)
result = first_child:method(p) .. f(p, children)
Run Code Online (Sandbox Code Playgroud)

which the incorrect value of p.

Changing p2 to p brings the correct value back into scope via an argument, instead of being ignored for an incorrect upvalue. At this point the function does not close around anything, and could actually be placed completely outside the definition of union.

Alternatively, lexically scoping each function to each union:method causes it to create the correct closures,

result = children[1]:method(p) .. children[2]:method(p)
Run Code Online (Sandbox Code Playgroud)

evidenced by needing no arguments.

local function f(_, __)
Run Code Online (Sandbox Code Playgroud)

In JavaScript, function declarations

union method    p   3
f redefined function: 0x55b789c4f0a0
f<p>    p
union method    pTRANSFORMED    1
f redefined function: 0x55b789c4f7a0
f<p>    pTRANSFORMED
f<p>    p
pTRANSFORMED  pp
Run Code Online (Sandbox Code Playgroud)

are lexically scoped (and hoisted) within their enclosing block.