Julia 元编程
Julia 把自己的代码表示为语言中的数据结构,这样我们就可以编写操纵程序的程序。
元编程也可以简单理解为编写可以生成代码的代码。
元编程(英语:Metaprogramming),是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的资料,或者在编译时完成部分本应在运行时完成的工作。多数情况下,与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译。
编写元程序的语言称之为元语言。被操纵的程序的语言称之为"目标语言"。一门编程语言同时也是自身的元语言的能力称之为"反射"或者"自反"。
-- 维基百科
Julia 源代码执行阶段
1、解析原始 Julia 代码:Julia 解析器首先将源码字符串解析成抽象语法树(AST),AST 是一种以易于操作的格式包含全部代码的结构。
2、执行已解析的 Julia 代码:在这个阶段,执行已解析的 Julia 代码。
当我们在 REPL 中输入代码并按回车键时,会依次执行以上两个阶段。
使用元编程工具,我们就可以访问这两个阶段之间的 Julia 代码,即在源代码解析之后、但在执行之前。
提醒:元编程是强大的工具,但也增加了代码的理解成本。官方建议只有在高阶函数和闭包无法满足需求时才使用宏和 eval,大多数功能应该优先用普通函数实现。
程序表示
Julia 提供了 Meta 模块,可以用 Meta.parse(str) 把字符串解析成表达式对象,解析结果是 Expr 类型:
实例
"1 + 1"
julia> ex1 = Meta.parse(prog)
:(1 + 1)
julia> typeof(ex1)
Expr
Expr 对象包含两个部分(ex1 包含了 head 和 args 属性):
一个是标识表达式类型的符号(Symbol)对象:
实例
:call
另一个是表达式的参数,可能是符号、其他表达式或字面量:
实例
3-element Vector{Any}:
:+
1
1
表达式也可以直接用 Expr 构造:
实例
:(1 + 1)
# 通过解析和直接构造的两个表达式是等价的
julia> ex1 == ex2
true
# 嵌套表达式
julia> ex3 = Meta.parse("(4 + 4) / 2")
:((4 + 4) / 2)
# 用 S 表达式查看嵌套结构
julia> Meta.show_sexpr(ex3)
(:call, :/, (:call, :+, 4, 4), 2)
符号与引用
冒号 : 前缀在 Julia 中有两个用途,注意区分:
第一,:名字 创建 Symbol(符号)。Symbol 是一种"经过内部驻留的字符串",代表一个名字本身而不是它的值:
实例
100
julia> :ABC # 返回符号 :ABC,不是 100
:ABC
julia> typeof(:ABC)
Symbol
第二,:(表达式) 创建未求值的表达式对象(quoting):
实例
:(100 - 50)
julia> ex = :(a + b*c + 1)
:(a + b * c + 1)
julia> typeof(ex)
Expr
# 等价的三种构造方式
julia> :(a + b*c + 1) == Meta.parse("a + b*c + 1") ==
Expr(:call, :+, :a, Expr(:call, :*, :b, :c), 1)
true
引用多个表达式可以在 quote ... end 中包含代码块:
实例
x = 1
y = 2
x + y
end
quote
#= none:2 =#
x = 1
#= none:3 =#
y = 2
#= none:4 =#
x + y
end
julia> typeof(ex)
Expr
执行表达式
表达式解析后,我们可以使用 eval() 函数在全局作用域中执行它:
实例
:(1 + 2)
julia> eval(ex1)
3
# 表达式中的符号在 eval 时才查找
julia> ex = :(a + b)
:(a + b)
julia> eval(ex)
ERROR: UndefVarError: `b` not defined in `Main`
julia> a = 1; b = 2;
julia> eval(ex)
3
抽象语法树(AST)
抽象语法树是源代码语法结构的抽象表示,它以树状形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
我们可以在 dump() 函数查看表达式的层次结构:
实例
Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol *
2: Int64 1
3: Expr
head: Symbol call
args: Array{Any}((2,))
1: Symbol cos
2: Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol /
2: Symbol pi
3: Int64 2
插值
直接构造 Expr 对象虽然强大,但写起来繁琐。Julia 允许把字面量或表达式的值插入到被引用的表达式中,插值由前缀 $ 表示:
实例
# a 的值被插入表达式
julia> ex = :($a + b)
:(1 + b)
对未被引用的表达式进行插值是不支持的,这会导致编译期错误:
julia> $a + b ERROR: syntax: "$" expression outside quote
插值元组时使用 $((1,2,3)) 形式:
julia> ex = :(a in $:((1,2,3)) ) :(a in (1, 2, 3))
表达式插值中的 $ 是有意与字符串插值、命令插值保持一致的,它使复杂 Julia 表达式的程序化构造变得方便和易读。
宏
宏提供了一种机制,可以将生成的代码包含在程序的最终主体中。宏把一组参数(表达式)映射为返回的表达式,返回的表达式被直接编译,而不需要运行时 eval 调用。宏参数可以包括表达式、字面量和符号。
这是一个非常简单的宏:
实例
return :( println("Hello, world!") )
end
@sayhello (macro with 1 method)
宏在 Julia 的语法中有一个专门的字符 @(at-sign),紧接着是宏名。在这个例子中,编译器会把所有的 @sayhello 替换成:
:( println("Hello, world!") )
当 @sayhello 在 REPL 中被输入时,解释器立即执行展开后的代码,因此我们只会看到计算后的结果:
julia> @sayhello() Hello, world!
带参数的宏,用 $ 把参数插值进返回的表达式:
实例
return :( println("Hello, ", $name) )
end
@sayhello (macro with 1 method)
julia> @sayhello("human")
Hello, human
查看宏展开
我们可以使用 @macroexpand 查看宏展开后的表达式,这是调试宏最有用的工具:
实例
:(println("Hello, ", "human"))
# @assert 宏的展开:表达式原样保留,出错时还能打印出表达式文本
julia> @macroexpand @assert 1 == 2
:(if 1 == 2
nothing
else
throw(AssertionError("1 == 2"))
end)
宏与函数的关键区别:宏在代码解析时执行,函数在运行时执行。宏收到的是代码本身(表达式),函数收到的是计算后的值,因此宏能做到函数做不到的事情,比如在错误信息里显示原表达式。
宏的卫生性(Hygiene)
宏展开时,宏内部定义的变量会被自动重命名,不会和调用处的变量冲突,这称为宏的卫生性。如果宏需要操作调用处的变量,需要用 esc() 显式声明:
实例
julia> macro zerox()
return esc(:(x = 0))
end
@zerox (macro with 1 method)
julia> function foo()
x = 1
@zerox
return x
end
foo (generic function with 1 method)
julia> foo()
0
非标准字符串字面量的原理
前面章节用过的 r"..."(正则)、b"..."(字节数组)等非标准字符串字面量,其实就是宏机制:前缀名加 _str 后缀就是对应的宏名。例如正则字面量的实现只有一行:
实例
Regex(p)
end
r"^\s*$" 会被解析成 @r_str 宏调用,宏在编译期把字符串变成 Regex 对象,比运行时每次 Regex("...") 更高效。
代码生成
当需要大量重复的样板代码时,可以用 @eval 宏在程序内生成定义。例如给自定义类型批量添加方法:
实例
x::Float64
end
# 批量为 MyNumber 定义 sin/cos/tan/log/exp 方法
julia> for op = (:sin, :cos, :tan, :log, :exp)
@eval Base.$op(a::MyNumber) = MyNumber($op(a.x))
end
julia> x = MyNumber(pi)
MyNumber(3.141592653589793)
julia> sin(x)
MyNumber(1.2246467991473532e-16)
julia> cos(x)
MyNumber(-1.0)
这个模式让 Julia 充当了自己的预处理器,可以在语言内部完成代码生成。
