如何动态地重载一个函数?

现在有这样一个场景:
我有一个 module A 中有两个函数foobarbar会调用foo
在另外一个 module B 中,通过 import A:foo 重载 foo 函数之后,再调用bar可以实现bar函数的替换,但是,似乎并不能动态地在一个函数内部的作用域里重载 foo 函数。举例如下:

module MyTest

module A

export foo, bar

function foo(x::Int)
    println("A.foo")
end

function bar()
    println("A.bar")
    foo(1)
end

end

module B

using ..A
import ..A:foo
export test_1, test_2

function foo(x::Int)
    println("B.foo")
end

function test_1()
    bar()
end

function test_2()
    function foo(x::Int)
        println("in test_2 B.foo")
    end
    bar()
end
function test_3()
    function foo(x::Int)
        println("in test_3 B.foo")
    end
    bar()
end
end

end

我希望在执行 MyTest.B.test_2()MyTest.B.test_3() 的时候,能够根据内部的foo函数不同进行重载。不过B中的三个函数执行结果并没有差别:

julia> MyTest.B.test_1()
A.bar
B.foo

julia> MyTest.B.test_2()
A.bar
B.foo

julia> MyTest.B.test_3()
A.bar
B.foo

问题:

  1. 在Julia中能否实现上述功能呢?
    a. (若不能)为什么不能呢?
    b. (若能)如何实现呢?
  2. 上述功能是否是个伪需求?

函数local scope里定义函数是local的,根据lexical scope,bar()调用的是在定义时的那个foo()。加global的话,Julia就会提示要用 eval: ERROR: syntax: Global method definition around REPL[2]:33 needs to be placed at the top level, or use "eval".

我尝试用@eval,不过还是没生效,是用法不对?

function test_2()
    @eval A function foo(x::Int)
        println("in test_2 B.foo")
    end
    bar()
end
1 个赞

不知道为什么要第二次才会生效:

julia> using .MyTest

julia> MyTest.B.test_3()
A.bar
A.foo

julia> MyTest.B.test_3()
A.bar
in test_3 B.foo

可能和#265的具体实现有关:
https://github.com/julialang/julia/issues/265

应该就是265实现的预期行为,需要用Base.invokelatest调用最新的bar,原来貌似会提示world age error什么的:

function test_3()
    @eval A function foo(x::Int)
        println("in test_3 B.foo")
    end
    Base.invokelatest(bar)
end
1 个赞

:+1:

好古老的一个issue :disappointed_relieved:

我晚上又想了想,似乎这样子也没法很好地实现我的一个需求。

之前我的预期是,在 test_3 函数体内部,通过重载某个函数临时替换核心库中的某个部件,该次替换只在test_3函数内部有效,执行完之后,自动恢复原有的功能(也许能通过某种很hack的方式实现)。

Base.invokelatest相当于更新了 Abar 函数的绑定,并不会自动再恢复。

julia> MyTest.B.test_1()
A.bar
B.foo

julia> MyTest.B.test_2()
A.bar
in test_2 B.foo

julia> MyTest.B.test_1()
A.bar
in test_2 B.foo

不过如果自动恢复的话,问题似乎又会复杂很多,可能后面有空的时候得琢磨琢磨相关的issue

需求的这种行为表面上看相当于是 dynamic scope,在 lexical scope 里模拟 dynamic scope 感觉不是一件容易的事。

动态修改代码使用Cassette,你想要修改的那部分代码其实是因为某个context才需要进行修改的(比如测试/debug),在正常情况下应该按照原来的逻辑运行。所以你只要用Cassette在你的Context下混入代码就行了。

这样scope等问题都是清晰的(因为其实是dispatch到了另外一个函数上去)