类似 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
函数产生新符号),全局变量被解析到宏定义环境中。