朱莉娅:把代码注入功能

时间:2015-09-30 16:50:00

标签: macros metaprogramming julia

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

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)

这可能与朱莉娅的元编程能力有关吗?

2 个答案:

答案 0 :(得分:5)

不,你不能使用元编程将代码注入已经编写的函数中。元编程只能做你可以直接在宏编写宏的位置自己编写的东西。这意味着如下声明:

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

甚至无法修改simulation(A, x)函数调用。它只能扩展到调用simulation之前运行的一些正常Julia代码。或许,您可以将模拟函数调用作为宏的参数包括在内,例如@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,我也无法衡量此简单循环的任何开销。而且很有可能,你的循环将比这个更复杂。所以,在你真正仔细检查它是否有影响之前,不要担心这里的表现。

答案 1 :(得分: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

=#