什么是元编程
维基百科上的解释为:
元编程(英语:Metaprogramming),又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的资料,或者在运行时完成部分本应在编译时完成的工作。多数情况下,与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译。
是不是没看懂?没关系,因为我刚开始也没看懂。知乎上有一个关于元编程的解释是比较直观的。
知乎原回答
Meta- 这个前缀在希腊语中的本意是「在…后,越过…的」,类似于拉丁语的 post-,比如 metaphysics 就是「在物理学之后」,这个词最开始指一些亚里士多德的著作,因为它们通常排序在《物理学》之后。但西方哲学界在几千年中渐渐赋予该词缀一种全新的意义:关于某事自身的某事。比如 meta-knowledge 就是「关于知识本身的知识」,meta-data 就是「关于数据的数据」,meta-language 就是「关于语言的语言」,而 meta-programming 也是由此而来,是「关于编程的编程」。弄清了词源和字面意思,可知大陆将 meta- 这个前缀译为「元」并不恰当。中国台湾译为「后设」,稍微好一点点,但仍旧无法望文生义。也许「自相关」是个不错的选择,「自相关数据」、「自相关语言」、「自相关编程」——但是好像又太罗嗦了。Anyway。先看看 meta-data:「我的电话是 +86 123 4567 8910」 ——这是一条数据;「+86 123 4567 8910 有十三个数字和一个字符,前两位是国家代码,后面是一个移动电话号码」 —— 这是关于前面那条数据的数据。
那么照猫画虎,怎样才算 meta-programming 呢?泛泛来说,只要是与编程相关的编程就算是 meta-programming 了——比如,若编程甲可以输出 A - Z,那么写程序甲算「编程」;而程序乙可以生成程序甲(也许还会连带着运行它输出 A - Z),那么编写程序乙的活动,就可以算作 meta-programming,「元编程」。
那我们看看Julia中的元编程到底是什么及如何应用?
每个Julia程序都是以字符串开始的
str1 = "1 + 1"
>>"1 + 1"
# 把字符串解析成表达式
ex1 = Meta.parse(str1)
>>:(1 + 1)
typeof(ex1)
>>Expr
# 我们也可以直接定义成parse的形式
ex2 = :(1 + 1)
typeof(ex2)
>>Expr
Expr对象包含两个部分,一个标识表达式类型的Symbol,一个是表达式的参数
fieldnames(typeof(ex1))
>>(:head, :args)
ex1.head
>>:call
ex1.args
>>3-element Array{Any,1}:
:+
1
1
Meta.show_sexpr(ex1)
>>(:call, :+, 1, 1)
所以我们也可以用 prefix notation来构造
ex3 = Expr(:call, :+, 1, 1)
:(1 + 1)
当然我们也可以求出表达式的结果
eval(ex3)
>>2
dump函数可以显示Expr对象
dump(ex1)
>>Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol +
2: Int64 1
3: Int64 1
构造一个Symbol
:foo
>>:foo
typeof(ans)
>>Symbol
:foo == Symbol("foo")
>>true
Symbol中如果是多个参数,则表示将这些参数连起来
Symbol("foo",123)
>>:foo123
我们前面定义的表达式都是数值表达式,元编程也允许字符表达式
ex4 = :(a+b*c+1)
>>:(a + b * c + 1)
typeof(ex4)
>> Expr
如果是多行表达式,可以在quote...end
中实现
ex4 = quote
x = 1
y = 2
x + y
end
typeof(ex4)
>>Expr
可以使用$符号,把数值代入到符号表达式中
a = 1
ex = :($a + b)
>>:(1 + b)
如果是array的表达式,则要采用如下形式
args = [:x, :y, :z]
:(f(1, $(args...)))
>>:(f(1,x,y,z))
x = :(1 + 2)
>>e = quote quote $x end end
此时我们用eval
查看此表达式的结果
eval(e)
>>quote
#= none:1 =#
1 + 2
end
也就是说,该表达式的内容仍然是一个quote;那我们怎么取出最里层的内容呢?
e = quote quote $$x end end
eval(e)
>>quote
#= none:1 =#
3
end
function math_expr(op, op1, op2)
expr = Expr(:call, op, op1, op2)
return expr
end
ex = math_expr(:+, 1, Expr(:call, :*, 4, 5))
>>:(1 + 4 * 5)
eval(ex)
>>21
function make_expr2(op, opr1, opr2)
opr1f, opr2f = map(x -> isa(x, Number) ? 2*x : x, (opr1, opr2))
retexpr = Expr(:call, op, opr1f, opr2f)
return retexpr
end
make_expr2(:+, 1, 2)
>>:(2 + 4)
ex = make_expr2(:+, 1, Expr(:call, :*, 5, 8))
>>:(2 + 5 * 8)
eval(ex)
>>42
Macro也是Julia元编程的一个重要应用,Macro是一种规则,或称为语法替换,这种替换在预编译时进行,Macro在运行时没有像函数似的调用时间。
macro HelloWorld()
return :( println("Hello World!")
end
编译器会把所有的@HelloWorld换成:( println("Hello World!")
@HelloWorld()
>>Hello World!
Macro也可以带参数
macro ILike(str)
return :( println("I like ", str))
end
@ILike("Julia")
>>I like Julia
我们可以用macroexpand
看到返回的quote 表达式,这也是宏调试时一个非常重要的工具
ex = macroexpand(Main, :(@ILike("Julia")))
>>:((Main.println)("I like ", "Julia"))
typeof(ex)
>>Expr
也可以用@macroexpand
查看返回的quote表达式
@macroexpand @ILike "Julia"
>>:((println)("I like ", "Julia"))
@macroexpand
还有一个重要用途,我们看下面的例子
macro testExpend(arg)
println("I execute at parse time. The argument is: ", arg)
return :(println("I execute at runtime. The argument is: ", $arg))
end
两个println语句,一个是julia语句,一个表达式,如果我们直接运行@testExpand((1,2))
@testExpand((1,2,3))
>>I execute at parse time. The argument is: (1, 2, 3)
I execute at runtime. The argument is: (1, 2, 3)
两个println语句都有输出;如果我们定义一个表达式
ex = macroExpand(Main, :(@testExpand :(1,2,3)))
>>I execute at parse time. The argument is: $(Expr(:quote, :((1, 2, 3))))
可以看出,当表达式被定义时,第一个println的内容就打印出来了
eval(ex)
>>I execute at runtime. The argument is: (1, 2, 3)
而第二个println函数则在表达式被运行时才执行
前面我们也调用Macro,比如@ILike("Julia")
,但宏调用的语法是很有讲究的,下面我们来具体看一下。
两种常用的宏调用方式
@name expr1 expr2 ...
@name(expr1, expr2, ...)
需要注意的是:第一种用法两个参数之间是用空格隔开,且参数之间没有逗号;第二种用法的name和()之间是没有空格的,且参数之间有逗号隔开。
如果写成了
@name (expr1, expr2, ...)
则表示参数为一个tuple。
当调用array时,可采用如下的方式
@name[a b] * v
@name([a b]) * v
对于Macro的参数,我们可以用show
打印出来
macro showarg(x)
show(x)
end
@showarg(a)
>>:a
@showarg(1+1)
>>:(1 + 1)
每个Macro都会被传入两个额外的参数:__source__
和__module__
,他们可以指示出location,在调试的时候很有用处。
由于Julia是多重派发的,Macro也支持类似于函数一样的多种方法。下面我们来看一个用Macro调试的例子。
debugging = true
macro debug1(msg...)
if debugging
:(println("DEBUG> ", $(msg...)))
else
:nothing
end
end
macro debug1(lineno, msg...)
if debugging
:(println("DEBUG ", basename(@__FILE__),":",$lineno, "> ", $(msg...)))
else
:nothing
end
end
function bubble_sort!(xs::Vector)
println("bubble_sort starting...")
n = length(xs)
swapped = true
while swapped
swapped = false
for i in 1:n-1
if xs[i] > xs[i+1]
# println(xs)
xs[i+1], xs[i] = xs[i], xs[i+1]
@debug1(@__LINE__, join(xs, " "))
swapped = true
end
end
end
xs
end
xs = Vector([1, 3, 4, 5, 8, 2, 7])
res = bubble_sort!(xs)
运行结果为
bubble_sort starting...
DEBUG bubble_sort.jl:34> 1 3 4 5 2 8 7
DEBUG bubble_sort.jl:34> 1 3 4 5 2 7 8
DEBUG bubble_sort.jl:34> 1 3 4 2 5 7 8
DEBUG bubble_sort.jl:34> 1 3 2 4 5 7 8
DEBUG bubble_sort.jl:34> 1 2 3 4 5 7 8
@time
@time 1 + 2
@time println("Hello world!")
@time sleep(1)
@time就是用下面的方法实现的。
macro tid(expre)
quote
local t0 = time()
local val = $expre
elapsedtime = time() - t0
println("$elapsedtime seconds")
val
end
end
@tid map(x->x^2, 1:10000)
@which
@which 1+2
@which sleep(2)
@show
x = rand(10)
@show sum(x)
@show cumsum(x)