类似 Lisp ,Julia 自身的代码也是语言本身的数据结构。由于代码是由这门语言本身所构造和处理的对象所表示的,因此程序也可以转换并生成自身语言的代码。元编程的另一个功能是反射,它可以在程序运行时动态展现程序本身的特性。
Julia 代码表示为由 Julia 的 Expr
类型的数据结构而构成的语法树。下面是 Expr
类型的定义:
type Expr |
head::Symbol |
args::Array{Any,1} |
typ |
end |
head
是标明表达式种类的符号;args
是子表达式数组,它可能是求值时引用变量值的符号,也可能是嵌套的 Expr
对象,还可能是真实的对象值。 typ
域被类型推断用来做类型注释,通常可以被忽略。
有两种“引用”代码的方法,它们可以简单地构造表达式对象,而不需要显式构造 Expr
对象。第一种是内联表达式,使用 :
,后面跟单表达式;第二种是代码块儿,放在 quote ... end
内部。下例是第一种方法,引用一个算术表达式:
julia> ex = :(a+b*c+1) |
:(a + b * c + 1) |
julia> typeof(ex) |
Expr |
julia> ex.head |
:call |
julia> typeof(ans) |
Symbol |
julia> ex.args |
4-element Array{Any,1}: |
:+ |
:a |
:(b * c) |
1 |
julia> typeof(ex.args[1]) |
Symbol |
julia> typeof(ex.args[2]) |
Symbol |
julia> typeof(ex.args[3]) |
Expr |
julia> typeof(ex.args[4]) |
Int64 |
:
的参数为符号时,结果为 Symbol
对象,而不是 Expr
:
julia> :foo |
:foo |
julia> typeof(ans) |
Symbol |
指定一个表达式,Julia 可以使用 eval
函数在 global 作用域对其求值。
julia> :(1 + 2) |
:(1 + 2) |
julia> eval(ans) |
3 |
julia> ex = :(a + b) |
:(a + b) |
julia> eval(ex) |
ERROR: a not defined |
julia> a = 1; b = 2; |
julia> eval(ex) |
3 |
eval
表达式。传递给 eval
的表达式不限于返回一个值 - 他们也会具有改变封闭模块的环境状态的副作用:
julia> ex = :(x = 1) |
:(x = 1) |
julia> x |
ERROR: x not defined |
julia> eval(ex) |
1 |
julia> x |
1 |
Julia 使用表达式内插和求值来生成重复的代码。下例定义了一组操作三个参数的运算符: ::
for op = (:+, :*, :&, :|, :$) |
eval(quote |
($op)(a,b,c) = ($op)(($op)(a,b),c) |
end) |
end |
宏有点儿像编译时的表达式生成函数。正如函数会通过一组参数得到一个返回值,宏可以进行表达式的变换,这些宏允许程序员在最后的程序语法树中对表达式进行任意的转化。调用宏的语法为:
@name expr1 expr2 ... |
@name(expr1, expr2, ...) |
@
符号。第一种形式,参数表达式之间没有逗号;第二种形式,宏名后没有空格。
卫生宏是个更复杂的宏。一般来说,宏必须确保变量的引入不会和现有的上下文变量发送冲突。相反的,宏中的表达式作为参数应该可以和上下文代码有机的结合在一起,进行交互。另一个令人关注的问题是,当宏用不同方式定义的时候是否被应该称为另一种模式。在这种情况下,我们需要确保所有的全局变量应该被纳入正确的模式中来。Julia 已经在宏方面有了很大的优势相比其它语言(比如 C)。所有的变量(比如 @assert
中的 msg
)遵循这一标准。
来看一下 @time
宏,它的参数是一个表达式。它先记录下时间,运行表达式,再记录下时间,打印出这两次之间的时间差,它的最终值是表达式的值:
macro time(ex) |
return quote |
local t0 = time() |
local val = $ex |
local t1 = time() |
println("elapsed time: ", t1-t0, " seconds") |
val |
end |
end |
t0
, t1
, 及 val
应为私有临时变量,而 time
是标准库中的 time
函数,而不是用户可能使用的某个叫 time
的变量( println
函数也如此)。
Julia 宏展开机制是这样解决命名冲突的。首先,宏结果的变量被分类为本地变量或全局变量。如果变量被赋值(且未被声明为全局变量)、被声明为本地变量、或被用作函数参数名,则它被认为是本地变量;否则,它被认为是全局变量。本地变量被重命名为一个独一无二的名字(使用 gensym
函数产生新符号),全局变量被解析到宏定义环境中。