Julia:将代码注入函数

Julia: inject code into function

我想将代码注入函数。具体来说,考虑一个简单的模拟器:

function simulation(A, x)
    for t in 1:1000
        z = randn(3)
        x = A*x + z
    end
end

有时我想每十个时间步记录一次x的值,有时每20个时间步记录一次z的值,有时我不想记录任何值。当然,我可以将一些标志作为函数的参数,并使用一些 if-else 语句。但我宁愿保持模拟代码干净,只注入一段代码,如

if t%10 == 0
    append!(rec_z, z)
end

在我需要的时候进入函数的特定位置。为此,我想编写一个宏,以便监视特定值成为

@monitor(:z, 10)
simulation(A, x)

Julia 的元编程功能可以实现吗?

不,您不能使用元编程将代码注入到已编写的函数中。元编程只能做您可以直接在宏本身编写的位置直接编写的事情。这意味着像这样的语句:

@monitor(:z, 10); simulation(A, x)

甚至无法修改 simulation(A, x) 函数调用。它只能扩展为在调用 simulation 之前运行的一些普通 Julia 代码。您或许可以将模拟函数调用作为参数包含在宏中,例如 @monitor(:z, 10, simulation(A, x)),但现在宏所能做的就是更改函数调用本身。它仍然无法 "go back" 并向已编写的函数添加新代码。

但是,您可以仔细而细致地制作一个采用函数定义主体并对其进行修改以添加调试代码的宏,例如,

@monitor(:z, 10, function simulation(A, x)
    for t in 1:1000
        # ...
    end
end)

但是现在你必须在宏中写代码遍历函数体中的代码,并在正确的地方注入你的调试语句。这不是一件容易的事。并且更难以一种不会在您修改实际模拟代码的那一刻中断的健壮方式编写。

遍历代码并将其插入对您来说使用编辑器可以轻松得多。调试语句的一个常见习惯用法是使用一行,如下所示:

const debug = false
function simulation (A, x)
    for t in 1:1000
        z = rand(3)
        x = A*x + z
        debug && t%10==0 && append!(rec_z, z)
    end
end

真正酷的是,通过将 debug 标记为常量,Julia 能够在 false 时完全优化调试代码——它甚至不会出现在生成的代码中!所以当你不调试时没有开销。但是,这确实意味着您必须重新启动 Julia(或重新加载它所在的模块)才能更改 debug 标志。即使 debug 未标记为 const,我也无法测量此简单循环的任何开销。很有可能,您的循环会比这个更复杂。因此,在您实际仔细检查它是否有效之前,不要担心这里的性能。

你可能对我刚刚提出的这个感兴趣。它并没有完全做你正在做的事情,但它很接近。通常安全且一致的代码添加位置是代码块的开头和结尾。这些宏允许您在这些位置注入一些代码(甚至传递代码参数!)

对于可切换的输入检查应该很有用。

#cleaninject.jl

#cleanly injects some code into the AST of a function.

function code_to_inject()
  println("this code is injected")
end

function code_to_inject(a,b)
  println("injected code handles $a and $b")
end

macro inject_code_prepend(f)
  #make sure this macro precedes a function definition.
  isa(f, Expr) || error("checkable macro must precede a function definition")
  (f.head == :function) || error("checkable macro must precede a function definition")

  #be lazy and let the parser do the hard work.
  b2 = parse("code_to_inject()")

  #inject the generated code into the AST.
  unshift!(f.args[2].args, b2)
  #return the escaped function to the parser so that it generates the new function.
  return Expr(:escape, f)
end

macro inject_code_append(f)
  #make sure this macro precedes a function definition.
  isa(f, Expr) || error("checkable macro must precede a function definition")
  (f.head == :function) || error("checkable macro must precede a function definition")

  #be lazy and let the parser do the hard work.
  b2 = parse("code_to_inject()")

  #inject the generated code into the AST.
  push!(f.args[2].args, b2)
  #return the escaped function to the parser so that it generates the new function.
  return Expr(:escape, f)
end

macro inject_code_with_args(f)
  #make sure this macro precedes a function definition.
  isa(f, Expr) || error("checkable macro must precede a function definition")
  (f.head == :function) || error("checkable macro must precede a function definition")

  #be lazy and let the parser do the hard work.
  b2 = parse(string("code_to_inject(", join(f.args[1].args[2:end], ","), ")"))

  #inject the generated code into the AST.
  unshift!(f.args[2].args, b2)
  #return the escaped function to the parser so that it generates the new function.
  return Expr(:escape, f)
end

################################################################################
# RESULTS

#=

julia> @inject_code_prepend function p()
       println("victim function")
       end
p (generic function with 1 method)

julia> p()
this code is injected
victim function

julia> @inject_code_append function p()
       println("victim function")
       end
p (generic function with 1 method)

julia> p()
victim function
this code is injected

julia> @inject_code_with_args function p(a, b)
       println("victim called with $a and $b")
       end
p (generic function with 2 methods)

julia> p(1, 2)
injected code handles 1 and 2
victim called with 1 and 2

=#