编写一个 Julia 宏,它返回一个函数。

8

首次发布,在此感谢您的阅读!

问题:我有一个Vector{String},称之为A,其中每个元素都是方程式的一部分,例如A的第一个元素是"x[1] - (0.8*x[1])"。我想编写一个宏,该宏以以下参数作为输入:i)一个字符串-称之为fn_name- 带有函数名,ii)向量A,并返回一个名为fn_name的函数,其形式如下

function fn_name(f, x)
  f[1] = x[1] - (0.8*x[1])
  f[2] = (exp(x[4]) - 0.8*exp(x[3]))^(-1.1) - (0.99*(exp(x[4]) - 0.8*exp(x[4]))^(-1.1)*(1.0 - 0.025 + 0.30*exp(x[1])*exp(x[2])^(0.30 - 1.0)))
  f[3] = exp(x[2]) - ((1.0 - 0.025)*exp(x[2]) + exp(x[1])*exp(x[2])^0.30 - exp(x[4]))
  f[4] = x[3] - (x[4])
end 

其中每个 rhs 都是一个元素

A = ["x[1] - (0.8*x[1])", "(exp(x[4]) - 0.8*exp(x[3]))^(-1.1) - (0.99*(exp(x[4]) - 0.8*exp(x[4]))^(-1.1)*(1.0 - 0.025 + 0.30*exp(x[1])*exp(x[2])^(0.30 - 1.0)))", "exp(x[2]) - ((1.0 - 0.025)*exp(x[2]) + exp(x[1])*exp(x[2])^0.30 - exp(x[4]))", "x[3] - (x[4])"]

我尝试过的方法:我最努力的尝试解决这个问题的方式是:

macro make_fn(fn_name, A)
    esc(quote
        function $(Symbol(fn_name))(f, x)
            for i = 1:length($(A))
                f[$i] = Meta.parse($(A)[$i])
            end
        end
    end)
end

然而,这并不起作用:当我运行@make_fn("my_name", A)时,我收到错误信息LoadError: UndefVarError: i not defined

我发现理解Julia元编程非常困难,虽然我很想避免使用它,但我认为在这个问题上是不可避免的。

请您帮助我理解我的错误出现在哪里?

谢谢


3
非常好的第一个问题!请注意,在这里问候和感谢并不必要,而且省略它们也不被视为不礼貌——一个好的问题本身就足够表达。 - phipsgabler
1个回答

4
在这种情况下,宏不仅是可避免的,而且甚至不适用,除非A在编译时是已知的字面值。
我可以提供一种使用eval和一些闭包的解决方案:
julia> function make_fn2(A)
           Af = [@eval(x -> $(Meta.parse(expr))) for expr in A]
           function (f, x)
               for i in eachindex(A, f)
                   f[i] = Af[i](x)
               end
               return f
           end
       end
make_fn2 (generic function with 1 method)

julia> fn_name = make_fn2(A)
#46 (generic function with 1 method)

julia> fn_name(zeros(4), [1,2,3,4])
4-element Array{Float64,1}:
  0.19999999999999996
 -0.06594092302655707
 49.82984401122239
 -1.0

有如下限制:

  1. eval 将在定义该函数的模块的全局作用域中执行表达式(因此可能与调用函数所在的作用域不同)
  2. 新创建的函数仅在返回到全局作用域后才能使用(即如果您尝试在创建它的函数内部运行它,它将不起作用)。

但我真的建议考虑一种比字符串更好的输入格式。


1
我完全同意这个建议和答案。两点小小的补充说明(特别是为什么这样做不太理想):1)eval会在模块的全局作用域中评估(因此它是调用函数作用域不同的作用域),2)新创建的函数只能在返回到全局作用域后才能使用(也就是说,如果您尝试在创建它的函数中运行它,它将无法工作)。 - Bogumił Kamiński
感谢@BogumiłKamiński,非常正确。我已经添加了这些要点。(而且,通过改变看似无害的事情,比如不将Af的定义提升出内部函数,你很容易遇到世界年龄问题。) - phipsgabler
2
我可能有一个解决方案针对第一点(https://gist.github.com/phipsgabler/8c80e3af2c06581329f67d88ffd8944a),但它太长了不能在评论中测试。 - phipsgabler
@phipsgabler 我会看一下这里链接的解决方案,因为在全局范围内进行评估确实是一个大问题,但我同意理想情况下不应该从一开始就使用字符串作为输入(我正在尝试找到一种解决方法,以便使用其他人编写的包中的函数,该包依赖于include() txt文件来生成工作区中的其他函数)。 - albep
1
您可以使用Base.invokelatest来避免世界年龄问题,但这会阻止编译器进行优化。 - lungben

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接