指南
π的 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
阻止了這種情況的發生,但是預設使用它會違背語言設計。對此的一個很好的辯護是防止一個人在巨集中使用和引入名稱,這使得他們很難跟蹤人類讀者。