指南

π的 Metaprogramming 位和 b​​obs

目標:

  • 通過在適當的環境中引入概念的最小目標功能/有用/非抽象示例(例如 @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

巢狀 Exprs:

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多線 Exprs

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 在功能上是相同的,但提供額外的除錯資訊。

(*) 提示 :使用 letx 保留在塊內

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)

消除 Julia 超程式設計中的各種引用機制的歧義

$:( …) 以某種方式相互反轉?

:(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

因為那將首先看到 pmacro 範圍,它會找到一個本地 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,而 xy 沒有。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 上,甚至是它們自己。

人們已經多次提到過 gensyms 很快你就會被預設的黑暗面所誘惑,以便在這裡和那裡點綴幾個 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.002/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 到 evalstring(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 在 @Ms 範圍內是未定義的,因此它使用全域性變數…我實際上忘記了在我的愚蠢示例中轉義 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 阻止了這種情況的發生,但是預設使用它會違背語言設計。對此的一個很好的辯護是防止一個人在巨集中使用和引入名稱,這使得他們很難跟蹤人類讀者。