指南
π的 Metaprogramming 位和 bobs
目标:
-
通过在适当的环境中引入概念的最小目标功能/有用/非抽象示例(例如
@swap
或@assert
)进行教学 -
更喜欢让代码说明/演示概念而不是解释段落
-
避免将必读链接到其他页面 - 它会中断叙述
-
以合理的顺序呈现事物,使学习变得最容易
资源:
julialang.org wikibook
(@Cormullion)
5 层(Leah Hanson)
SO-Doc Quoting(@TotalVerb)
SO-Doc - 非合法标识符的符号(@TotalVerb)
SO:Julia(@StefanKarpinski)
话语中 的符号是什么 线程(@ pi-) 元编程
大多数材料来自话语频道,其中大部分都来自 fcard …如果我忘记了归因,请告诉我。
符号
julia> mySymbol = Symbol("myName") # or 'identifier'
:myName
julia> myName = 42
42
julia> mySymbol |> eval # 'foo |> bar' puts output of 'foo' into 'bar', so 'bar(foo)'
42
julia> :( $mySymbol = 1 ) |> eval
1
julia> myName
1
将标志传递给函数:
function dothing(flag)
if flag == :thing_one
println("did thing one")
elseif flag == :thing_two
println("did thing two")
end
end
julia> dothing(:thing_one)
did thing one
julia> dothing(:thing_two)
did thing two
一个哈希键示例:
number_names = Dict{Symbol, Int}()
number_names[:one] = 1
number_names[:two] = 2
number_names[:six] = 6
(高级) (@ fcard):foo
aka :(foo)
如果 foo
是有效的标识符,则生成符号,否则为表达式。
# NOTE: Different use of ':' is:
julia> :mySymbol = Symbol('hello world')
#(You can create a symbol with any name with Symbol("<name>"),
# which lets us create such gems as:
julia> one_plus_one = Symbol("1 + 1")
Symbol("1 + 1")
julia> eval(one_plus_one)
ERROR: UndefVarError: 1 + 1 not defined
...
julia> valid_math = :($one_plus_one = 3)
:(1 + 1 = 3)
julia> one_plus_one_plus_two = :($one_plus_one + 2)
:(1 + 1 + 2)
julia> eval(quote
$valid_math
@show($one_plus_one_plus_two)
end)
1 + 1 + 2 = 5
...
基本上你可以将符号视为轻量级字符串。这不是他们的目的,但你可以做到,所以为什么不呢。Julia’s Base 本身就是这样做的,print_with_color(:red, "abc")
打印出一个红色的 abc。
Expr (AST)
(几乎)Julia 中的所有内容都是一个表达式,即 Expr
的一个实例,它将保存 AST 。
# when you type ...
julia> 1+1
2
# Julia is doing: eval(parse("1+1"))
# i.e. First it parses the string "1+1" into an `Expr` object ...
julia> ast = parse("1+1")
:(1 + 1)
# ... which it then evaluates:
julia> eval(ast)
2
# An Expr instance holds an AST (Abstract Syntax Tree). Let's look at it:
julia> dump(ast)
Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol +
2: Int64 1
3: Int64 1
typ: Any
# TRY: fieldnames(typeof(ast))
julia> :(a + b*c + 1) ==
parse("a + b*c + 1") ==
Expr(:call, :+, :a, Expr(:call, :*, :b, :c), 1)
true
嵌套 Expr
s:
julia> dump( :(1+2/3) )
Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol +
2: Int64 1
3: Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol /
2: Int64 2
3: Int64 3
typ: Any
typ: Any
# Tidier rep'n using s-expr
julia> Meta.show_sexpr( :(1+2/3) )
(:call, :+, 1, (:call, :/, 2, 3))
使用 quote
的多线 Expr
s
julia> blk = quote
x=10
x+1
end
quote # REPL[121], line 2:
x = 10 # REPL[121], line 3:
x + 1
end
julia> blk == :( begin x=10; x+1 end )
true
# Note: contains debug info:
julia> Meta.show_sexpr(blk)
(:block,
(:line, 2, Symbol("REPL[121]")),
(:(=), :x, 10),
(:line, 3, Symbol("REPL[121]")),
(:call, :+, :x, 1)
)
# ... unlike:
julia> noDbg = :( x=10; x+1 )
quote
x = 10
x + 1
end
…所以 quote
在功能上是相同的,但提供额外的调试信息。
(*) 提示 :使用 let
将 x
保留在块内
quote
-ing a quote
Expr(:quote, x)
用于表示引号内的引号。
Expr(:quote, :(x + y)) == :(:(x + y))
Expr(:quote, Expr(:$, :x)) == :(:($x))
QuoteNode(x)
类似于 Expr(:quote, x)
,但它可以防止插值。
eval(Expr(:quote, Expr(:$, 1))) == 1
eval(QuoteNode(Expr(:$, 1))) == Expr(:$, 1)
是 $ 和 :( …) 以某种方式相互反转?
:(foo)
的意思是“不看价值,看表达”$foo
的意思是将表达式改为其价值
:($(foo)) == foo
。$(:(foo))
是个错误。$(...)
不是一个操作,它本身不做任何事情,它是“插入这个!” 表示引用语法使用的符号。即它只存在于引号中。
是 $
foo
一样 eval(
foo
)
?
没有! $foo
被交换为编译时值 eval(foo)
意味着在运行时这样做
eval
将在全局范围内发生插值是本地的
eval(:<expr>)
应该返回与 <expr>
相同(假设 <expr>
是当前全局空间中的有效表达式)
eval(:(1 + 2)) == 1 + 2
eval(:(let x=1; x + 1 end)) == let x=1; x + 1 end
macro
s
准备? :)
# let's try to make this!
julia> x = 5; @show x;
x = 5
让我们自己制作 @show
宏:
macro log(x)
:(
println( "Expression: ", $(string(x)), " has value: ", $x )
)
end
u = 42
f = x -> x^2
@log(u) # Expression: u has value: 42
@log(42) # Expression: 42 has value: 42
@log(f(42)) # Expression: f(42) has value: 1764
@log(:u) # Expression: :u has value: u
expand
降低了一个人
5 层(Leah Hanson) < - 解释 Julia 如何将源代码作为字符串,将其标记为 Expr
-tree(AST)
,扩展所有宏(仍为 AST),降低 (降低 AST),然后转换为 LLVM (以及更远 - 目前我们不需要担心什么是超越!)
问:code_lowered
作用于功能。是否可以降低 Expr
?A:是的!
# function -> lowered-AST
julia> code_lowered(*,(String,String))
1-element Array{LambdaInfo,1}:
LambdaInfo template for *(s1::AbstractString, ss::AbstractString...) at strings/basic.jl:84
# Expr(i.e. AST) -> lowered-AST
julia> expand(:(x ? y : z))
:(begin
unless x goto 3
return y
3:
return z
end)
julia> expand(:(y .= x.(i)))
:((Base.broadcast!)(x,y,i))
# 'Execute' AST or lowered-AST
julia> eval(ast)
如果你只想展开宏,可以使用 macroexpand
:
# AST -> (still nonlowered-)AST but with macros expanded:
julia> macroexpand(:(@show x))
quote
(Base.println)("x = ",(Base.repr)(begin # show.jl, line 229:
#28#value = x
end))
#28#value
end
…返回一个非降低的 AST 但扩展了所有宏。
esc()
esc(x)
返回一个说不要对此应用卫生的 Expr,它与 Expr(:escape, x)
相同。卫生是保持宏观自足的一种方式,如果你想让它们泄漏,你可以做到这一点。例如
示例: swap
宏来说明 esc()
macro swap(p, q)
quote
tmp = $(esc(p))
$(esc(p)) = $(esc(q))
$(esc(q)) = tmp
end
end
x,y = 1,2
@swap(x,y)
println(x,y) # 2 1
$
让我们’逃离’quote
。那么为什么不简单地说 $p
和 $q
呢?即
# FAIL!
tmp = $p
$p = $q
$q = tmp
因为那将首先看到 p
的 macro
范围,它会找到一个本地 p
即参数 p
(是的,如果你随后访问 p
而没有 esc
-ing,则宏将 p
参数视为局部变量)。
所以 $p = ...
只是一个分配到当地的 p
。它不会影响调用上下文中传入的任何变量。
那么怎么样:
# Almost!
tmp = $p # <-- you might think we don't
$(esc(p)) = $q # need to esc() the RHS
$(esc(q)) = tmp
所以 esc(p)
正在泄漏p
进入调用环境。 “传递给我们收到的宏作为 p
的东西 ”
julia> macro swap(p, q)
quote
tmp = $p
$(esc(p)) = $q
$(esc(q)) = tmp
end
end
@swap (macro with 1 method)
julia> x, y = 1, 2
(1,2)
julia> @swap(x, y);
julia> @show(x, y);
x = 2
y = 1
julia> macroexpand(:(@swap(x, y)))
quote # REPL[34], line 3:
#10#tmp = x # REPL[34], line 4:
x = y # REPL[34], line 5:
y = #10#tmp
end
正如你所看到的,tmp
获得卫生处理 #10#tmp
,而 x
和 y
没有。Julia 正在为 tmp
制作一个唯一的标识符,你可以用 gensym
手动完成,即:
julia> gensym(:tmp)
Symbol("##tmp#270")
但是: 有一个问题:
julia> module Swap
export @swap
macro swap(p, q)
quote
tmp = $p
$(esc(p)) = $q
$(esc(q)) = tmp
end
end
end
Swap
julia> using Swap
julia> x,y = 1,2
(1,2)
julia> @swap(x,y)
ERROR: UndefVarError: x not defined
julia 宏观卫生的另一件事是,如果宏来自另一个模块,它会使任何变量(在宏的返回表达式中没有分配,如本例中的 tmp
)当前模块的全局变量,所以 $p
变成了 Swap.$p
,同样是 $q
- > Swap.$q
。
一般来说,如果你需要一个超出宏范围的变量,你应该考虑它,所以你应该知道它们是否在表达式的 LHS 或 RHS 上,甚至是它们自己。
人们已经多次提到过 gensym
s 很快你就会被默认的黑暗面所诱惑,以便在这里和那里点缀几个 gensym
来转义整个表达,但是…确保在试图变得更聪明之前了解卫生是如何工作的比它! 它不是一个特别复杂的算法,所以它不应该花太长时间,但不要急于求成! 在你理解它的所有后果之前不要使用那种力量…(@fcard)
示例: until
macro
(@伊斯梅尔 -VC)
"until loop"
macro until(condition, block)
quote
while ! $condition
$block
end
end |> esc
end
julia> i=1; @until( i==5, begin; print(i); i+=1; end )
1234
(@fcard)|>
是有争议的。我很惊讶暴徒还没有来争论。 (也许每个人都厌倦了)。建议大多数宏(如果不是全部)只是对函数的调用,所以:
macro until(condition, block)
esc(until(condition, block))
end
function until(condition, block)
quote
while !$condition
$block
end
end
end
……是一种更安全的选择。
## @ fcard 简单的宏挑战
任务:交换操作数,所以 swaps(1/2)
给 2.00
即 2/1
macro swaps(e)
e.args[2:3] = e.args[3:-1:2]
e
end
@swaps(1/2)
2.00
从 @fcard 更宏观的挑战,在这里
插值和 assert
宏
http://docs.julialang.org/en/release-0.5/manual/metaprogramming/#building-an-advanced-macro
macro assert(ex)
return :( $ex ? nothing : throw(AssertionError($(string(ex)))) )
end
问:为什么最后的 $
?答:它插入,即强制 Julia 到 eval
,string(ex)
作为执行通过调用此宏。即如果你只是运行该代码,它将不会强制进行任何评估。但是当你做的时候,Julia 会调用这个宏来替换它的’AST token / Expr’,无论它返回什么,$
都会开始行动。
使用{}获取块的有趣黑客
(@fcard)我认为没有任何技术可以保持 {}
不被用作块,事实上,人们甚至可以使用残余的 {}
语法来使其工作:
julia> macro c(block)
@assert block.head == :cell1d
esc(quote
$(block.args...)
end)
end
@c (macro with 1 method)
julia> @c {
print(1)
print(2)
1+2
}
123
*(如果/当{}语法被重新利用时,不太可能仍然有效
所以首先 Julia 看到了宏令牌,所以它会读取/解析令牌直到匹配 end
,并创建什么?一个 Expr
与 .head=:macro
或什么?它将 a+1
存储为字符串还是将其分解为:+(:a, 1)
?怎么看?
?
(@fcard)在这种情况下,由于词法范围,a 在 @M
s 范围内是未定义的,因此它使用全局变量…我实际上忘记了在我的愚蠢示例中转义 flipplin’表达式,但 “只能在同一个模块中工作“ 它的一部分仍然适用。
julia> module M
macro m()
:(a+1)
end
end
M
julia> a = 1
1
julia> M.@m
ERROR: UndefVarError: a not defined
原因在于,如果宏在任何模块中使用,而不是在其中定义的模块中,则未在要扩展的代码中定义的任何变量将被视为宏模块的全局变量。
julia> macroexpand(:(M.@m))
:(M.a + 1)
高级
### @伊斯梅尔 -VC
@eval begin
"do-until loop"
macro $(:do)(block, until::Symbol, condition)
until ≠ :until &&
error("@do expected `until` got `$until`")
quote
let
$block
@until $condition begin
$block
end
end
end |> esc
end
end
julia> i = 0
0
julia> @do begin
@show i
i += 1
end until i == 5
i = 0
i = 1
i = 2
i = 3
i = 4
斯科特的宏观:
"""
Internal function to return captured line number information from AST
##Parameters
- a: Expression in the julia type Expr
##Return
- Line number in the file where the calling macro was invoked
"""
_lin(a::Expr) = a.args[2].args[1].args[1]
"""
Internal function to return captured file name information from AST
##Parameters
- a: Expression in the julia type Expr
##Return
- The name of the file where the macro was invoked
"""
_fil(a::Expr) = string(a.args[2].args[1].args[2])
"""
Internal function to determine if a symbol is a status code or variable
"""
function _is_status(sym::Symbol)
sym in (:OK, :WARNING, :ERROR) && return true
str = `string(sym)`
`length(str)` > 4 && (str[1:4] == "ERR_" || str[1:5] == "WARN_" || str[1:5] == "INFO_")
end
"""
Internal function to return captured error code from AST
##Parameters
- a: Expression in the julia type Expr
##Return
- Error code from the captured info in the AST from the calling macro
"""
_err(a::Expr) =
(sym = a.args[2].args[2] ; `_is_status(sym)` ? Expr(:., :Status, `QuoteNode(sym)`) : sym)
"""
Internal function to produce a call to the log function based on the macro arguments and the AST from the ()->ERRCODE anonymous function definition used to capture error code, file name and line number where the macro is used
##Parameters
- level: Loglevel which has to be logged with macro
- a: Expression in the julia type Expr
- msgs: Optional message
##Return
- Statuscode
"""
function _log(level, a, msgs)
if `isempty(msgs)`
:( log($level, $(esc(:Symbol))($(`_fil(a)`)), $(`_lin(a)`), $(`_err(a)`) )
else
:( log($level, $(esc(:Symbol))($(`_fil(a)`)), $(`_lin(a)`), $(`_err(a)`), message=$(esc(msgs[1]))) )
end
end
macro warn(a, msgs...) ; _log(Warning, a, msgs) ; end
垃圾/未加工……
查看 / 转储宏
(@ pi-)假设我只是用新鲜的 REPL 做 macro m(); a+1; end
。没有定义 a
。我怎么能’看’它?比如,有没有办法’转储’一个宏?没有实际执行它
(@fcard)宏中的所有代码实际上都被放入函数中,因此你只能查看它们的降低或类型推断的代码。
julia> macro m() a+1 end
@m (macro with 1 method)
julia> @code_typed @m
LambdaInfo for @m()
:(begin
return Main.a + 1
end)
julia> @code_lowered @m
CodeInfo(:(begin
nothing
return Main.a + 1
end))
# ^ or: code_lowered(eval(Symbol("@m")))[1] # ouf!
获取宏功能的其他方法:
julia> macro getmacro(call) call.args[1] end
@getmacro (macro with 1 method)
julia> getmacro(name) = getfield(current_module(), name.args[1])
getmacro (generic function with 1 method)
julia> @getmacro @m
@m (macro with 1 method)
julia> getmacro(:@m)
@m (macro with 1 method)
julia> eval(Symbol("@M"))
@M (macro with 1 method)
julia> dump( eval(Symbol("@M")) )
@M (function of type #@M)
julia> code_typed( eval(Symbol("@M")) )
1-element Array{Any,1}:
LambdaInfo for @M()
julia> code_typed( eval(Symbol("@M")) )[1]
LambdaInfo for @M()
:(begin
return $(Expr(:copyast, :($(QuoteNode(:(a + 1))))))
end::Expr)
julia> @code_typed @M
LambdaInfo for @M()
:(begin
return $(Expr(:copyast, :($(QuoteNode(:(a + 1))))))
end::Expr)
^看起来我可以用 code_typed
代替
如何理解 eval(Symbol("@M"))
?
(@fcard)目前,每个宏都有一个与之关联的功能。如果你有一个名为 M
的宏,那么宏的函数叫做 @M
。一般来说你可以用例如 eval(:print)
获得一个函数的值,但是你需要做一个宏的函数 Symbol("@M")
,因为只是:@M
变成了一个 Expr(:macrocall, Symbol("@M"))
并且评估它会导致宏扩展。
为什么 code_typed
没有显示参数?
(@ PI-)
julia> code_typed( x -> x^2 )[1]
LambdaInfo for (::##5#6)(::Any)
:(begin
return x ^ 2
end)
^我在这里看到一个::Any
param,但它似乎没有与令牌 x
连接。
julia> code_typed( print )[1]
LambdaInfo for print(::IO, ::Char)
:(begin
(Base.write)(io,c)
return Base.nothing
end::Void)
^同样在这里; 没有什么可以将 io
与::IO
联系起来所以这肯定不能完全转储特定的 print
方法的 AST 表示……?
(@fcard)print(::IO, ::Char)
只告诉你它是什么方法,它不是 AST 的一部分。它甚至不再存在于大师中:
julia> code_typed(print)[1]
CodeInfo(:(begin
(Base.write)(io,c)
return Base.nothing
end))=>Void
(@ pi-)我不明白你的意思。它似乎是倾倒 AST 的那个方法的身体,不是吗?我以为 code_typed
给出了一个函数的 AST。但它似乎错过了第一步,即为 params 设置令牌。
(@fcard)code_typed
只是为了显示身体的 AST,但是现在它确实给出了方法的完整 AST,形式为 LambdaInfo
(0.5)或 CodeInfo
(0.6),但很多信息被省略了当打印到 repl。你需要逐个字段检查 LambdaInfo
以获取所有详细信息。dump
将淹没你的 repl,所以你可以尝试:
macro method_info(call)
quote
method = @code_typed $(esc(call))
print_info_fields(method)
end
end
function print_info_fields(method)
for field in fieldnames(typeof(method))
if isdefined(method, field) && !(field in [Symbol(""), :code])
println(" $field = ", getfield(method, field))
end
end
display(method)
end
print_info_fields(x::Pair) = print_info_fields(x[1])
其中给出了方法 AST 的命名字段的所有值:
julia> @method_info print(STDOUT, 'a')
rettype = Void
sparam_syms = svec()
sparam_vals = svec()
specTypes = Tuple{Base.#print,Base.TTY,Char}
slottypes = Any[Base.#print,Base.TTY,Char]
ssavaluetypes = Any[]
slotnames = Any[Symbol("#self#"),:io,:c]
slotflags = UInt8[0x00,0x00,0x00]
def = print(io::IO, c::Char) at char.jl:45
nargs = 3
isva = false
inferred = true
pure = false
inlineable = true
inInference = false
inCompile = false
jlcall_api = 0
fptr = Ptr{Void} @0x00007f7a7e96ce10
LambdaInfo for print(::Base.TTY, ::Char)
:(begin
$(Expr(:invoke, LambdaInfo for write(::Base.TTY, ::Char), :(Base.write), :(io), :(c)))
return Base.nothing
end::Void)
看到 lil’def = print(io::IO, c::Char)
?你去! (也是 slotnames = [..., :io, :c]
部分)同样是,输出的差异是因为我在 master 上显示结果。
???
(@ Ismael-VC)你的意思是这样的吗?带符号的通用调度
你可以这样做:
julia> function dispatchtest{alg}(::Type{Val{alg}})
println("This is the generic dispatch. The algorithm is $alg")
end
dispatchtest (generic function with 1 method)
julia> dispatchtest(alg::Symbol) = dispatchtest(Val{alg})
dispatchtest (generic function with 2 methods)
julia> function dispatchtest(::Type{Val{:Euler}})
println("This is for the Euler algorithm!")
end
dispatchtest (generic function with 3 methods)
julia> dispatchtest(:Foo)
This is the generic dispatch. The algorithm is Foo
julia> dispatchtest(:Euler)
这是针对 Euler 算法的! 我想知道 @fcard 对通用符号发送的看法是什么! — ^:天使:
模块陷阱
@def m begin
a+2
end
@m # replaces the macro at compile-time with the expression a+2
更准确地说,只能在定义宏的模块的顶层内工作。
julia> module M
macro m1()
a+1
end
end
M
julia> macro m2()
a+1
end
@m2 (macro with 1 method)
julia> a = 1
1
julia> M.@m1
ERROR: UndefVarError: a not defined
julia> @m2
2
julia> let a = 20
@m2
end
2
esc
阻止了这种情况的发生,但是默认使用它会违背语言设计。对此的一个很好的辩护是防止一个人在宏中使用和引入名称,这使得他们很难跟踪人类读者。