指南

π的 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 阻止了这种情况的发生,但是默认使用它会违背语言设计。对此的一个很好的辩护是防止一个人在宏中使用和引入名称,这使得他们很难跟踪人类读者。