朱莉娅:向函数注入代码

4

我想把代码嵌入到一个函数中。为了具体说明,考虑一个简单的模拟器:

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

有时,我想每经过10个时间步骤记录x的值,有时想每经过20个时间步骤记录z的值,有时又不想记录任何值。我可以在函数中增加一些标志来实现,然后再用if-else语句进行判断。但是,我更希望保持仿真代码的简洁性,并注入一个像下面这样的代码片段。
if t%10 == 0
    append!(rec_z, z)
end

每当我需要时,将其放入函数的特定位置。为此,我想编写一个宏,使监视特定值变得更容易。

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

使用Julia的元编程功能是否可能实现这一点?
2个回答

5
不,你不能使用元编程来将代码注入到已经写好的函数中。元编程只能做那些你可以直接在宏定义的位置手动编写的操作。这意味着像这样的语句:
@monitor(:z, 10); simulation(A, x)

无法修改simulation(A, x)函数调用,它只能扩展成一些普通的Julia代码在调用simulation之前运行。或许你可以将模拟函数调用作为宏的参数,例如@monitor(:z, 10, simulation(A, x)),但现在宏所能做的只是改变函数调用本身。它仍然不能“回到过去”并向已经编写好的函数添加新代码。
然而,您可以仔细地编写一个宏,取出函数定义体并修改它以添加您的调试代码,例如:
@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,我在这个简单的循环中也无法测量任何开销。而且你的循环很可能比这个更复杂。因此,在您确实检查它是否有影响之前,请不要担心性能问题。

嘿,马特,感谢你的深入回答!我现在正在探索一个稍微不同的路线:我将模拟代码视为表达式,然后通过宏修改它以添加监视器和其他内容,然后在每次运行之前编译整个函数。与运行时间相比,编译成本很小,因此不会影响性能。这样,我可以完全不改动模拟代码。 - user45893
关于函数的仪器化:如果宏用户表示在函数中嵌入额外代码的位置 - 在这个例子中是监视语句 - 那么它难道不会使编写宏变得容易吗? - Dan Getz
肯定是可能的,但我认为这不太易读或易维护……而且指定比“在开头”更复杂的位置会很困难。 - mbauman

0
你可能会对我刚刚编写的东西感兴趣。它并不完全做你所做的事情,但很接近。添加代码的通常安全和一致的位置是代码块的开头和结尾。这些宏允许您在这些位置注入一些代码(甚至传递代码参数!)。 对于可切换的输入检查应该很有用。
#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

=#

我不会认为@inject_code_append是安全的——如果有任何显式的return语句,它将无法工作……否则它会改变返回值。 - mbauman

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