元编程
Julia最重要的Lisp遗产是它对元编程的支持。 像Lisp一样,Julia将自己的代码作为语言本身的数据结构。 由于代码由可以在语言中创建和操作的对象表示,因此程序具有转换和生成自己的代码的能力。 这允许您生成复杂的代码,而无需额外的汇编步骤,以及使用真正的Lisp风格的宏,在https://en.wikipedia.org/wiki/Abstract_syntax_tree [抽象语法树]。 相比之下,预处理类似于c和C中使用的"宏"系统++,在分析或解释实际发生之前执行与文本及其替换的操作。 由于Julia中的所有数据类型和代码都由Julia数据结构表示,因此可以使用强大的功能。 https://en.wikipedia.org/wiki/Reflection_%28computer_programming%29 [reflections]允许您研究程序的内部结构及其类型,就像任何其他数据一样。
元编程是一个强大的工具,但它引入了额外的困难,使其难以理解代码。 例如,理解定义作用域的规则可能出乎意料地困难。 通常,只有在其他方法不适用的情况下,才应使用元编程,例如 高阶函数和https://en.wikipedia.org/wiki/Closure_ (computer_programming)[短路]。 'eval’和定义新宏通常是最后的手段。 它几乎从来没有建议使用’元。解析`或将任意字符串转换为Julia代码。 要操作Julia代码,请直接使用’Expr’数据结构,这样您就不必了解Julia语法分析的具体细节。 在元编程应用程序的最佳示例中,大多数功能通常作为辅助运行时函数实现,只需创建最少量的代码。 |
节目介绍
每个Julia程序都以字符串的形式开始其生命:
julia> prog = "1 + 1"
"1 + 1"
接下来会发生什么?
下一步是https://en.wikipedia.org/wiki/Parsing#Computer_languages [convert]将每个字符串转换为一个称为表达式的对象,由Julia类型表示 'Expr':
julia> ex1 = Meta.parse(prog)
:(1 + 1)
julia> typeof(ex1)
Expr
"Expr"对象包含两个部分:
-
'Symbol',它定义了表达式的类型。 符号是标识符。 https://en.wikipedia.org/wiki/String_interning [保存的字符串](更多关于这个下面)。
julia> ex1.head
:call
-
参数是可以是字符、其他表达式或文字值的表达式。:
julia> ex1.args
3-element Vector{Any}:
:+
1
1
表达式也可以直接在https://en.wikipedia.org/wiki/Polish_notation [前缀符号]:
julia> ex2 = Expr(:call, :+, 1, 1)
:(1 + 1)
上面通过分析和直接构造构造的两个表达式是等价的:
julia> ex1 == ex2
true
这里的关键点是Julia的代码在内部表示为可从语言本身访问的数据结构。
功能 `dump'表示带有缩进和注释的`Expr’对象:
julia> dump(ex2)
Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol +
2: Int64 1
3: Int64 1
'Expr’对象也可以嵌套:
julia> ex3 = Meta.parse("(4 + 4) / 2")
:((4 + 4) / 2)
查看表达式的另一种方法是使用’Meta.show_sexpr',显示https://en.wikipedia.org/wiki/S-expression 一个给定的"Expr"的[s-表达式],其形式对Lisp用户来说可能非常熟悉。 下面是一个示例,说明嵌套`Expr`上的显示:
julia> Meta.show_sexpr(ex3)
(:call, :/, (:call, :+, 4, 4), 2)
符号
':'符号在Julia中有两个句法函数。 第一个表单创建 '符号',https://en.wikipedia.org/wiki/String_interning [保存的字符串]用作基于有效名称的表达式的一个构建块:
julia> s = :foo
:foo
julia> typeof(s)
Symbol
设计师 'Symbol'接受任意数量的参数,并通过连接字符串的表示形式来创建一个新的符号:
julia> :foo === Symbol("foo")
true
julia> Symbol("1foo") # `:1foo` не сработает, так как `1foo` не является допустимым именем
Symbol("1foo")
julia> Symbol("func",10)
:func10
julia> Symbol(:var,'_',"sym")
:var_sym
在表达式的上下文中,符号用于指示对变量的访问;当计算表达式时,符号被与相应符号相关联的值替换 地区。
有时在':'参数周围需要额外的括号,以避免分析过程中的歧义。:
julia> :(:)
:(:)
julia> :(::)
:(::)
表达式和计算
用引号括起来
符号`:`的第二个语法功能是创建表达式对象,而不显式使用构造函数 'Expr'。 这就是所谓的引用。 字符`:`后跟一对围绕单个Julia代码表达式的括号,根据包含的代码生成一个’Expr`对象。 下面是一个用于引用算术表达式的短格式的示例:
julia> ex = :(a+b*c+1)
:(a + b * c + 1)
julia> typeof(ex)
Expr
请注意,可以使用以下方法构造等价表达式 '元。parse'或直接形式’Expr`:
julia> :(a + b*c + 1) ==
Meta.parse("a + b*c + 1") ==
Expr(:call, :+, :a, Expr(:call, :*, :b, :c), 1)
true
分析器提供的表达式通常只有符号、其他表达式和文字值作为参数,而Julia代码构造的表达式可以有任意的运行时变量,而没有文字形式作为参数。 在这个特定的例子中,'+'和’a’是字符,'*(b,c)`是子表达式,`1`是带符号的文字64位整数。
对于几个表达式,还有第二种语法形式的引用:包含在`quote中的代码块。.. 结束'。
julia> ex = quote
x = 1
y = 2
x + y
end
quote
#= none:2 =#
x = 1
#= none:3 =#
y = 2
#= none:4 =#
x + y
end
julia> typeof(ex)
Expr
插值法
直接构造对象 'Expr'对于值参数是有效的,但是与Julia的"正常"语法相比,使用`Expr`构造函数可能很乏味。 或者,Julia允许将文本或表达式的_interpolation_转换为带引号的表达式。 插值由前缀'$'表示。
在这个例子中,变量`a`的值被插值。:
julia> a = 1;
朱莉娅>ex=:($a+b)
:(1 + b)
不支持将插值到非引号表达式中,这将导致编译时错误。:
julia> $a + b
ERROR: syntax: "$" expression outside quote
在此示例中,元组`(1,2,3)'作为条件检查中的表达式进行插值。:
julia> ex = :(a in $:((1,2,3)) )
:(a in (1, 2, 3))
分割插值
请注意,$'插值语法只允许将一个表达式插入到封闭表达式中。 在某些情况下,你有一个表达式数组,你需要它们都成为周围表达式的参数。 这可以使用语法'+$(xs…)+
. 例如,下面的代码创建一个函数调用,其中以编程方式确定参数的数量:
julia> args = [:x, :y, :z];
julia> :(f(1, $(args...)))
:(f(1, x, y, z))
嵌入式引文
自然,引文表达式可以包含其他引文表达式。 了解插值在这些情况下的工作原理可能有些困难。 考虑这个例子:
julia> x = :(1 + 2);
julia> e = quote quote $x end end
quote
#= none:1 =#
$(Expr(:quote, quote
#= none:1 =#
$(Expr(:$, :x))
end))
end
请注意,结果包含'$x`,这意味着`x’尚未计算。 换句话说,表达式'$'"属于"内部引用表达式,因此只有在内部引用表达式为以下内容时才计算其参数:
julia> eval(e)
quote
#= none:1 =#
1 + 2
end
但是,外部表达式’quote’可以在内部引号中插入`$`内的值。 这是用几个'$`完成的:
julia> e = quote quote $$x end end
quote
#= none:1 =#
$(Expr(:quote, quote
#= none:1 =#
$(Expr(:$, :(1 + 2)))
end))
end
请注意,结果现在显示`(1+2)而不是`x`字符。 计算此表达式会产生插值的"3
:
julia> eval(e)
quote
#= none:1 =#
3
end
这种行为背后的直觉是,x`为每个'$'计算一次:一个
$'的工作原理类似于’eval(:x),为`x`赋值,而两个
$执行相当于`eval(eval(:x))
。
[医]报价单
Ast中引文('quote')的通常表示形式是 Expr'
标题为:quote':
julia> dump(Meta.parse(":(1+2)"))
Expr
head: Symbol quote
args: Array{Any}((1,))
1: Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol +
2: Int64 1
3: Int64 2
正如我们所看到的,这样的表达式支持用`$进行插值。 然而,在某些情况下,需要在不执行插值的情况下引用_代码。 这种类型的引用还没有语法,但它有一个内部表示为QuoteNode对象
:
julia> eval(Meta.quot(Expr(:$, :(1+2))))
3
julia> eval(QuoteNode(Expr(:$, :(1+2))))
:($(Expr(:$, :(1 + 2))))
分析器为简单的带引号的元素(如字符)输出`QuoteNode:
julia> dump(Meta.parse(":x"))
QuoteNode
value: Symbol x
"QuoteNode"也可用于某些难以实现的元编程任务。
计算表达式
如果存在表达式对象,则可以使用以下方法使Julia环境在全局作用域中计算(执行)它 'eval':
julia> ex1 = :(1 + 2)
:(1 + 2)
julia> eval(ex1)
3
julia> ex = :(a + b)
:(a + b)
julia> eval(ex)
ERROR: UndefVarError: `b` not defined in `Main`
[...]
julia> a = 1; b = 2;
julia> eval(ex)
3
julia> ex = :(x = 1)
:(x = 1)
julia> x
ERROR: UndefVarError: `x` not defined in `Main`
julia> eval(ex)
1
julia> x
1
这里,表达式对象的求值导致向全局变量`x’赋值。
由于表达式只是`Expr`对象,因此可以通过编程方式构造它们,然后可以计算是否可以动态创建任意代码,然后可以使用 'eval'。 下面是一个简单的例子。
julia> a = 1;
julia> ex = Expr(:call, :+, a, :b)
:(1 + b)
julia> a = 0; b = 2;
julia> eval(ex)
3
值’a`用于构造表达式`ex`,它将函数`+`应用于值1和变量`b'。 注意`a`和`b`之间的重要区别。:
-
变量’a’的值在构造表达式期间用作表达式中的直接值。 因此,在计算表达式时,
a`的值不再重要:表达式中的值已经是`1
,无论`a`的值是什么。 -
反过来,在表达式的构造中使用了_character_`:b`,因此此时变量`b`的值是微不足道的,
:b`只是一个符号,变量`b’甚至不需要定义。 然而,在计算表达式期间,通过搜索变量`b`的值来解析符号
:b`的值。
'Expr’表达式中的函数
如上所述,Julia的一个非常有用的特性是能够在Julia语言本身中生成和操作Julia代码。 我们已经看到了一个返回对象的函数的例子。 `Expr':函数 '元。parse',它接受Julia的代码并返回相应的’Expr'。 该函数还可以将一个或多个`Expr`对象作为参数并返回另一个’Expr'。 这是一个简单的激励例子。
julia> function math_expr(op, op1, op2)
expr = Expr(:call, op, op1, op2)
return expr
end
math_expr (generic function with 1 method)
julia> ex = math_expr(:+, 1, Expr(:call, :*, 4, 5))
:(1 + 4 * 5)
julia> eval(ex)
21
让我们再举一个例子。 这是一个函数,它将任何数字参数加倍,但不影响表达式。:
julia> 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 (generic function with 1 method)
julia> make_expr2(:+, 1, 2)
:(2 + 4)
julia>ex=make_expr2(:+,1,Expr(:call,:*,5,8))
:(2 + 5 * 8)
朱莉娅>eval(ex)
42
宏
宏提供了一种将生成的代码包含在最终程序体中的机制。 宏将参数元组映射到返回的表达式,生成的表达式直接编译,不需要调用。 `eval'执行期间。 宏参数可以是表达式、字面值和符号。
基本知识
这是一个非常简单的宏:
julia> macro sayhello()
return :( println("Hello, world!") )
end
@sayhello (macro with 1 method)
Julia语法中的宏有一个专用字符:'@'(commercial at),后跟块中声明的'宏名称。.. 结束'。 在此示例中,编译器将用"@sayhello"替换所有实例:
:( println("Hello, world!") )
当`@sayhello’输入到REPL中时,表达式立即执行,因此我们只看到计算的结果。:
julia> @sayhello()
Hello, world!
现在让我们来看一个稍微复杂一点的宏。:
julia> macro sayhello(name)
return :( println("Hello, ", $name) )
end
@sayhello (macro with 1 method)
这个宏需要一个参数:'name'。 如果出现'@sayhello',则会扩展带引号的表达式以插入最终表达式中参数的值。:
julia> @sayhello("human")
Hello, human
我们可以使用函数查看返回的带引号的表达式 'macroexpand'(*重要提示:*它是调试宏的非常有用的工具):
julia> ex = macroexpand(Main, :(@sayhello("human")) )
:(Main.println("Hello, ", "human"))
julia> typeof(ex)
Expr
您可以看到文字'"人类"'被插值到表达式中。
还有一个宏 '@macroexpand',这可能比’macroexpand’函数更方便一点:
julia> @macroexpand @sayhello "human"
:(println("Hello, ", "human"))
等一下:为什么宏?
我们已经看到了函数'f(::Expr。..)->Expr`在上一节中。 事实上 'macroexpand'也是这样的函数。 那么宏为什么存在呢?
宏是必要的,因为它们是在代码被分析时执行的,因此,宏允许程序员在程序作为一个整体执行之前创建并包括用户代码的片段。 为了说明差异,请考虑以下示例。
julia> macro twostep(arg)
println("I execute at parse time. The argument is: ", arg)
return :(println("I execute at runtime. The argument is: ", $arg))
end
@twostep (macro with 1 method)
julia> ex = macroexpand(Main, :(@twostep :(1, 2, 3)) );
I execute at parse time. The argument is: :((1, 2, 3))
第一个挑战 println'
在调用时执行 'macroexpand'。 结果表达式只包含第二个’println:
julia> typeof(ex)
Expr
julia> ex
:(println("I execute at runtime. The argument is: ", $(Expr(:copyast, :($(QuoteNode(:((1, 2, 3)))))))))
朱莉娅>eval(ex)
我在运行时执行。 论点是:(1,2,3)
调用宏
使用以下常规语法调用宏:
@name expr1 expr2 ...
@name(expr1, expr2, ...)
请注意宏名称前的特殊"@",第一种形式的参数表达式之间没有逗号,第二种形式的"@name"后面没有空格。 这两种风格不应该混合使用。 例如,以下语法与上面的示例不同;它传递元组'(expr1,expr2,...)'作为宏的单个参数:
@name (expr1, expr2, ...)
为数组字面量(或包含)调用宏的另一种方法是在不使用括号的情况下将一个宏与另一个宏进行对比。 在这种情况下,数组将是传递给宏的唯一表达式。 以下语法是等价的(与`@name[a b]*v’不同):
@name[a b] * v
@name([a b]) * v
需要强调的是,宏将其参数接收为表达式、文字或符号。 检查宏参数的一种方法是调用函数 `show'在宏的主体中:
julia> macro showarg(x)
show(x)
# …оставшаяся часть макроса, возвращается выражение
end
@showarg (macro with 1 method)
julia> @showarg(a)
:a
julia> @showarg(1+1)
:(1 + 1)
julia> @showarg(println("Yo!"))
:(println("Yo!"))
julia> @showarg(1) # Числовой литерал
1
julia> @showarg("Yo!") # Строковой литерал
"Yo!"
julia>@showarg("哟! $("hello")")#插值字符串是表达式,而不是字符串
:("Yo! $("hello")")
除了指定的参数列表之外,名为'__source__`和'__module__'的其他参数将传递给每个宏。
__Source__'参数提供有关宏调用中
@符号分析器位置的信息(以`LineNumberNode`对象的形式)。 这允许宏包括更好的错误诊断信息,并且通常使用,例如,通过日志记录,字符串分析宏和文档,以及用于实现宏。 '@__线__
, '@__FILE__和 '@__DIR__。
位置信息可以通过参考`__source__来访问。行’和'__source__。档案`:
julia> macro __LOCATION__(); return QuoteNode(__source__); end
@__LOCATION__ (macro with 1 method)
julia> dump(
@__LOCATION__(
))
LineNumberNode
line: Int64 2
file: Symbol none
`__Module__'参数提供有关宏调用扩展的上下文的信息(以`Module`对象的形式)。 这允许宏搜索上下文信息,例如现有关系,或者将值作为附加参数插入到在当前模块中执行自我检查的运行时函数调用中。
创建扩展宏
这是宏的简化定义 `@assert'朱莉娅:
julia> macro assert(ex)
return :( $ex ? nothing : throw(AssertionError($(string(ex)))) )
end
@assert (macro with 1 method)
此宏可按如下方式使用:
julia> @assert 1 == 1.0
julia> @assert 1 == 0
ERROR: AssertionError: 1 == 0
在分析过程中,宏将扩展到其返回的结果,而不是编写的语法。 这相当于以下条目:
1 == 1.0 ? nothing : throw(AssertionError("1 == 1.0"))
1 == 0 ? nothing : throw(AssertionError("1 == 0"))
也就是说,在第一次调用中,表达式`:(1==1.0)'被编织到测试条件的槽中,而’string的值(:(1 == 1.0))` 被编织到批准消息的槽中。 以这种方式构造的整个表达式被放置在调用宏`@assert`的语法树中。 然后,在执行过程中,如果测试表达式的值在计算时为true,则返回 'nothing',而如果值为false,则返回一个错误,指示正在验证的表达式的值为false。 请注意,不可能将其写为函数,因为只有_condition_的值可用,并且无法在错误消息中显示计算它的表达式。
Julia Base中`@assert’的实际定义更加复杂。 它允许用户指定自己的错误消息选项,而不仅仅是在屏幕上显示失败的表达式。 就像在参数数量可变的函数中一样(具有可变参数数量的函数(Vararg)),这由最后一个参数后面的省略号表示:
julia> macro assert(ex, msgs...)
msg_body = isempty(msgs) ? ex : msgs[1]
msg = string(msg_body)
return :($ex ? nothing : throw(AssertionError($msg)))
end
@assert (macro with 1 method)
现在'@assert’有两种操作模式,具体取决于收到的参数数量! 如果只有一个参数,则’msgs’捕获的表达式元组将为空,并且与上面更简单的定义的行为相同。 但是现在,如果用户指定了第二个参数,它将显示在消息正文中,而不是显示在失败的表达式中。 您可以使用适当命名的宏检查宏扩展的结果。 '@macroexpand':
julia> @macroexpand @assert a == b
:(if Main.a == Main.b
Main.nothing
else
Main.throw(Main.AssertionError("a == b"))
end)
julia> @macroexpand @assert a==b "a should equal b!"
:(if Main.a == Main.b
Main.nothing
else
Main.throw(Main.AssertionError("a should equal b!"))
end)
还有另一种情况是`@assert’宏处理:如果除了显示"a应该等于b"之外,我们还想显示它们的值呢? 您可以天真地尝试在用户消息中使用字符串插值,例如’@assert a==b"a( b)!"`,但这不会像上述宏中预期的那样工作。 你明白为什么吗? 请记住,在部分 字符串插值有人说,插值后的字符串在调用中被复盖 '字符串'。 比较一下:
julia> typeof(:("a should equal b"))
String
julia> typeof(:("a ($a) should equal b ($b)!"))
Expr
julia> dump(:("a ($a) should equal b ($b)!"))
Expr
head: Symbol string
args: Array{Any}((5,))
1: String "a ("
2: Symbol a
3: String ") should equal b ("
4: Symbol b
5: String ")!"
所以现在,宏不是得到一个简单的字符串`msg_body`,而是得到整个表达式,需要计算才能按预期显示数据。 这可以作为调用参数编织到返回的表达式中。 `string';有关完整的实现,请参阅链接https://github.com/JuliaLang/julia/blob/master/base/error.jl ['错误。jl']。
`@Assert’宏广泛使用交织的带引号的表达式,以简化宏主体内表达式的操作。
卫生学
在更复杂的宏中出现的问题是https://en.wikipedia.org/wiki/Hygienic_macro [卫生]。 简而言之,宏必须确保它们在返回表达式中表示的变量不会意外地与它们部署到的周围代码中的现有变量冲突。 相反,作为参数传递给宏的表达式通常需要在周围代码的上下文中执行计算,与现有变量交互并更改它们。 另一个问题来自这样一个事实,即宏可以在定义它的模块之外的模块中调用。 在这种情况下,我们需要确保所有全局变量都解析为正确的模块。 与文本宏扩展的语言(如C)相比,Julia已经有一个重要的优势,那就是只考虑返回的表达式是必要的。 所有其他变量(如上面`@assert`中的`msg')都遵循 作用域块的正常行为。
为了演示这些问题,考虑编写宏`@time`,它将表达式作为其参数,记录时间,计算表达式,再次记录时间,显示前后时间的差异,然后获取表达式的值作为最终值。 宏可能如下所示:
macro time(ex)
return quote
local t0 = time_ns()
local val = $ex
local t1 = time_ns()
println("elapsed time: ", (t1-t0)/1e9, " seconds")
val
end
end
在这里,我们希望`t0'’t1’和’val’是私有临时变量,并且还希望’time_ns’引用函数 `time_ns'在Julia Base中,而不是用户可能拥有的任何变量`time_ns'(同样适用于`println')。 想象一下,如果自定义表达式`ex`还包含对名为`t0`的变量的赋值或定义其自己的变量`time_ns',则可能发生的问题。 我们可能会得到错误或神秘不正确的行为。
Julia宏扩展器解决这些问题如下。 首先,宏结果中的变量被分类为局部或全局。 一个变量被认为是局部的,如果它有一个值分配给它(并且它没有被声明为全局),它被声明为局部的,或者它被用作函数参数的名称。 否则,它被认为是全球性的。 然后将局部变量重命名为唯一(使用函数 'gensym',生成新字符),并且全局变量在宏定义环境中解析。 因此,上面描述的两个问题都解决了;局部宏变量不会与任何用户变量冲突,并且`time_ns`和`println`将引用Julia Base定义。
然而,仍然存在一个问题。 请考虑此宏的以下用法:
module MyModule
import Base.@time
time_ns() = ... # вычисляет что-то
@time time_ns()
end
这里,自定义表达式’ex`是对`time_ns`的调用,但不是对宏使用的相同函数`time_ns’的调用。 它明确地指’MyModule。time_ns'。 因此,我们需要确保’ex’中的代码解析到宏环境中。 这是通过向表达式添加转义序列来完成的。 'esc':
macro time(ex)
...
local val = $(esc(ex))
...
end
以这种方式包装的表达式不受宏扩展器的影响,只需逐字插入到输出中。 因此,它将在宏调用环境中解决。
这种使用转义序列的机制,如有必要,可用于"违反"卫生规则,以便引入或管理用户变量。 例如,以下宏在调用环境中将`x`设置为0:
julia> macro zerox()
return esc(:(x = 0))
end
@zerox (macro with 1 method)
julia> function foo()
x = 1
@zerox
return x # равно нулю
end
foo (generic function with 1 method)
julia> foo()
0
这种类型的变量操作应该谨慎使用,但在某些情况下它是相当方便的。
正确制定卫生规则可能是一项艰巨的任务。 在使用宏之前,您可能需要考虑关闭函数是否足够。 另一个有用的策略是将尽可能多的工作推迟到完成。 例如,许多宏简单地将其参数包装在"QuoteNode"或其他类似的宏中。 'Expr'。 这方面的例子是`@task body`,它简单地返回`schedule(Task(()->$body)),以及
@eval expr`,它简单地返回`eval(QuoteNode(expr))`。
为了演示,我们可以重写上面的"@time"示例,如下所示:
macro time(expr)
return :(timeit(() -> $(esc(expr))))
end
function timeit(f)
t0 = time_ns()
val = f()
t1 = time_ns()
println("elapsed time: ", (t1-t0)/1e9, " seconds")
return val
end
但是,我们不这样做,这不是巧合:将`expr`包装在一个新的作用域块(匿名函数)中也会稍微改变表达式的含义(其中任何变量的作用域),同时我们希望`@time`在对包
宏和调度
宏以及Julia函数是通用的。 这意味着由于多次调度,它们也可以有多个方法定义。:
julia> macro m end
@m (macro with 0 methods)
julia> macro m(args...)
println("$(length(args)) arguments")
end
@m (macro with 1 method)
julia> macro m(x,y)
println("Two arguments")
end
@m (macro with 2 methods)
julia> @m "asd"
1 arguments
julia> @m 1 2
Two arguments
但是,应该记住,多个分派是基于传递给宏的AST类型,而不是基于ast在运行时计算的类型。:
julia> macro m(::Int)
println("An Integer")
end
@m (macro with 3 methods)
julia> @m 2
An Integer
julia> x = 2
2
julia> @m x
1 arguments
代码生成
当需要大量重复的模板代码时,通常以编程方式生成以避免冗余。 在大多数语言中,这需要额外的构建步骤和单独的程序来生成重复的代码。 在Julia中,表达式的插值和 'eval'允许您在程序执行的正常过程中执行此类代码生成。 例如,请考虑以下自定义类型
struct MyNumber
x::Float64
end
# вывод
我们要添加几种方法。 我们可以在下一个周期以编程方式做到这一点。:
for op = (:sin, :cos, :tan, :log, :exp)
eval(quote
Base.$op(a::MyNumber) = MyNumber($op(a.x))
end)
end
# вывод
现在我们可以将这些函数与我们的自定义类型一起使用。:
julia> x = MyNumber(π)
MyNumber(3.141592653589793)
julia> sin(x)
MyNumber(1.2246467991473532e-16)
julia> cos(x)
MyNumber(-1.0)
因此,朱莉娅作为自己的https://en.wikipedia.org/wiki/Preprocessor [预处理器]并允许从语言内部生成代码。 上面的代码可以使用前缀引用形式`:`写得更短一点:
for op = (:sin, :cos, :tan, :log, :exp)
eval(:(Base.$op(a::MyNumber) = MyNumber($op(a.x))))
end
但是,这种类型的代码从语言内部生成,使用模板'eval(quote(...))`,是相当普遍的,所以Julia有一个宏来缩短这个模板。:
for op = (:sin, :cos, :tan, :log, :exp)
@eval Base.$op(a::MyNumber) = MyNumber($op(a.x))
end
@eval begin
# несколько строк
end
非标准字符串文字
请记住,在部分 Strings有人说,以标识符作为前缀的字符串字面量被称为非标准字符串字面量,其语义将与没有前缀的字符串字面量不同。 例如:
令人惊讶的是,这些行为在Julia分析器或编译器中没有硬编码。 相反,它们使用由每个人都可以使用的通用机制提供的自定义行为:带有前缀的字符串文字被解析为具有特殊名称的宏调用。 例如,正则表达式宏只是以下代码:
macro r_str(p)
Regex(p)
end
就这样。 这个宏说字符串字面量的内容是'r"^\s*(?:#/$)"'它必须传递给宏'@r_str',并且此扩展的结果必须放在字符串字面量所在的语法树中。 换句话说,表达式'r"^\s*(?:#/$)"'相当于把下面的对象直接放到语法树中:
Regex("^\\s*(?:#|\$)")
字符串文字形式不仅更短,更方便,而且效率更高。:由于正则表达式是编译的,而`Regex’对象实际上是创建的,所以当代码被编译时,编译只发生一次,而不是每次执行代码时。 让我们考虑正则表达式是否发生在循环中。:
for line = lines
m = match(r"^\s*(?:#|$)", line)
if m === nothing
# не комментарий
else
# комментарий
end
end
由于正则表达式是'r"^\s*(?:#/$)"'编译并插入到语法树中,当分析此代码时,表达式只编译一次,而不是每次执行循环时。 要在没有宏的情况下完成此操作,我们必须编写此循环,如下所示:
re = Regex("^\\s*(?:#|\$)")
for line = lines
m = match(re, line)
if m === nothing
# не комментарий
else
# комментарий
end
end
而且,如果编译器无法确定正则表达式对象在所有循环中都没有变化,则可能无法进行某些优化,这仍然使得此版本比上述更方便的字面形式效率更低。 当然,在非字面形式更方便的情况下,如果你需要将一个变量插入一个正则表达式,你应该选择这个更详细的方法;如果正则表达式模板本身是动态的,并且可以随着循环的每次迭代而改变,你需要在每次迭代中构造一个新的正则表达式对象。 但是,在绝大多数用例中,正则表达式不是基于运行时数据构建的。 在大多数情况下,将正则表达式写成编译时值的能力是非常宝贵的。
使用用户定义的字符串文字的机制非常有效。 它不仅实现了非标准的Julia字面量,还实现了命令字面量的语法(echo’echo"Hello,person person"'$),为此使用了以下看起来无害的宏:
macro cmd(str)
:(cmd_gen($(shell_parse(str)[1])))
end
当然,很多复杂性在于这个宏定义中使用的函数,但它们只是完全用Julia编写的函数。 您可以阅读他们的源代码并确切地看到他们在做什么,他们所做的只是构造表达式对象以插入到程序的语法树中。
与字符串字面量一样,命令字面量也可以接受标识符作为前缀,以形成所谓的非标准命令字面量。 这些命令文本被解析为具有特殊名称的宏调用。 例如,custom custom’literal"$的语法被解析为'@custom_cmd"literal"`。 Julia语言本身缺少非标准命令文本,但包可以使用此语法。 除了不同的语法和后缀`_cmd`而不是`_str`之外,非标准命令字面量的行为与非标准字符串字面量完全相同。
如果两个模块提供具有相同名称的非标准字符串或命令文本,则可以使用模块名称限定字符串或命令文本。 例如,如果’Foo`和`Bar`都提供非标准字符串字面值`@x_str`,那么您可以编写’Foo。x"字面"'或’酒吧。x"字面"'来区分它们。
定义宏的另一种方法如下:
macro foo_str(str, flag)
# сделай что-нибудь
end
然后可以使用以下语法调用此宏:
foo"str"flag
上面提到的语法中的标志的类型将是一个字符串(String
),其中包含字符串文字后面的所有内容。
生成的函数
一个非常特殊的宏是 '@generated',它允许您定义所谓的_generated functions_。 它们能够根据参数的类型生成专门的代码,与使用多个调度相比,具有更大的灵活性和/或更少的代码。 虽然宏在调试期间使用表达式并且无法访问其输入数据的类型,但生成的函数在已知参数类型但尚未编译时会展开。
生成的函数的声明不是执行某种计算或操作,而是返回一个带引号的表达式,然后该表达式形成与参数类型相对应的方法的主体。 当一个生成的函数被调用时,它返回的表达式被编译然后执行。 为了使此过程高效,通常会缓存结果。 为了使其可推导,只有有限的语言子集适合使用。 因此,由于对允许的构造有很大的限制,生成的函数提供了一种灵活的方式来将工作从运行时转移到编译时。
如果我们谈论生成的函数,与通常的函数有五个主要区别:
-
函数声明用宏`@generated’标记。 这会向AST添加一些信息,让编译器知道它是一个生成的函数。
-
在生成函数的主体中,您只能访问参数类型,而不能访问它们的值。
-
而不是计算某些东西或执行某些操作,你返回一个带引号的表达式,当计算时,它会做你想要的。
-
生成的函数只允许调用在生成函数定义之前已经定义的函数。 (如果不这样做,可能会导致"MethodErrors"引用来自"世界时代"方法定义的未来层次结构的函数。)
-
生成的函数不得_更改或_观察任何非永久性全局状态(包括例如I/O操作、锁、非本地字典或用法 'hasmethod')。 这意味着它们只能读取全局常量,不能产生副作用。 换句话说,它们必须完全干净。 由于实现限制,这也意味着他们目前无法定义闭包或生成器。
说明这一点的最简单方法是用一个例子。 我们可以将生成的函数`foo`声明为
julia> @generated function foo(x)
Core.println(x)
return :(x * x)
end
foo (generic function with 1 method)
请注意,主体返回带引号的表达式,即`:(x*x)`,而不仅仅是值`x*x'。
从调用者的角度来看,这与常规函数相同;事实上,您不需要知道您是在调用常规函数还是生成函数。 让我们看看’foo’的行为。:
julia> x = foo(2); # примечание: выходные данные из выражения println() в теле
Int64
julia> x # теперь мы выводим на экран x
4
julia> y = foo("bar");
String
julia> y
"barbar"
因此,我们看到在生成函数的主体中,`x`是传递参数的类型,而生成函数返回的值是计算我们从定义返回的带引号的表达式的结果,现在值为`x'。
如果我们用我们已经使用的类型再次计算`foo`会发生什么?
julia> foo(4)
16
请注意 `Int64'不显示。 我们看到,对于一组特定的参数类型,生成函数的主体在这里只执行一次,并且结果被缓存。 之后,在此示例中,第一次调用时从生成的函数返回的表达式被重用为方法体。 但是,实际的缓存行为是实现定义的性能优化,因此过于依赖此行为是不可接受的。
生成的函数只能创建一次,并且可以更频繁地创建或根本不创建。 因此,您不应该编写具有副作用的生成函数,因为无法确定副作用发生的时间和频率。 (宏也是如此,就像宏一样,使用 `eval'在生成的函数是一个迹象,表明你做错了什么。)但是,与宏不同,运行时无法正确处理调用。 'eval',因此禁止其使用。
了解生成(@generated')函数如何与方法重写交互也很重要。 遵循正确生成的(
@generated')函数不应该观察任何可变状态或导致全局状态发生任何变化的原则,我们看到以下行为。 请注意,生成的函数不能调用在定义生成的函数本身之前未定义的任何方法。
最初’f(x)'有一个定义
julia> f(x) = "original definition";
使用`f(x)`定义其他操作:
julia> g(x) = f(x);
julia> @generated gen1(x) = f(x);
julia>@generated gen2(x)=:(f(x));
现在我们为`f(x)`添加一些新的定义:
julia> f(x::Int) = "definition for Int";
julia> f(x::Type{Int}) = "definition for Type{Int}";
我们比较这些结果之间的差异。:
julia> f(1)
"definition for Int"
julia> g(1)
"definition for Int"
julia> gen1(1)
"original definition"
julia> gen2(1)
"definition for Int"
生成函数的每个方法都有自己对某些函数的表示:
julia> @generated gen1(x::Real) = f(x);
julia> gen1(1)
"definition for Type{Int}"
上面生成的函数’foo’的例子没有做正常函数`foo(x)=x*x`不能做的事情(除了显示第一次调用的类型和更高的成本)。 但是,生成函数的全部功能是基于它根据传递给它的类型计算各种引用表达式的能力。:
julia> @generated function bar(x)
if x <: Integer
return :(x ^ 2)
else
return :(x)
end
end
bar (generic function with 1 method)
julia> bar(4)
16
julia> bar("baz")
"baz"
(当然,虽然这个牵强附会的例子更容易使用多个调度来实现。..)
滥用这会破坏运行时环境并导致未定义的行为。:
julia> @generated function baz(x)
if rand() < .9
return :(x^2)
else
return :("boo!")
end
end
baz (generic function with 1 method)
由于生成函数的主体不是确定性的,因此其行为和整个后续code_的行为是未定义的。
不要复制这些例子!
我们希望这些示例在说明生成的函数如何工作方面已经证明是有用的,无论是在它们的定义还是调用方面;但是,出于以下原因,不要复制它们:
-
'foo’函数有副作用(调用’Core。println'),并且没有确切地定义何时,多久或多少次这些副作用会发生。;
-
'bar’函数解决了一个使用多重调度更好地解决的问题-定义’bar(x)=x`和`bar(x::Integer)=x^2`将做同样的事情,但更容易,更快。
-
'baz’的功能是"病态的"。
请注意,在生成的函数中不应尝试的操作集是无限的,运行时当前只能检测到无效操作的子集。 还有许多其他操作,在没有通知的情况下,只会破坏运行时环境,通常以微妙的方式与不正确的定义没有明显的关联。 由于函数生成器是在输出期间启动的,因此它必须遵守此代码的所有限制。
以下是一些不应尝试的操作。
-
缓存自己的指针。
-
与`核心的内容或方法的交互。编译器’通过任何手段。
-
任何可变状态的观察。 **生成函数的输出可以随时执行,包括当您的代码试图观察或更改此状态时。
-
使用任何锁:您发送调用的C代码可能在内部使用锁(例如,调用`malloc`不是问题,尽管在大多数实现中内部需要锁),但在执行Julia代码时不要尝试持有或接收锁。
-
对在生成的函数主体之后定义的任何函数的调用。 此条件对于增量加载的预编译模块来说并不严格,以允许调用模块中的任何函数。
好。 现在您已经更好地了解了生成的函数是如何工作的,让我们使用它们来创建更高级(和可接受的)功能。..
一个高级示例
Julia基础库有一个内部函数’sub2ind',用于根据一组n个多线性索引计算n维数组中的线性索引-换句话说,计算索引`i`,该索引可用于使用`A[i]而不是
+A[x,y,z,..]+`. 可能的实现方式之一是以下。
julia> function sub2ind_loop(dims::NTuple{N}, I::Integer...) where N
ind = I[N] - 1
for i = N-1:-1:1
ind = I[i]-1 + dims[i]*ind
end
return ind + 1
end;
julia> sub2ind_loop((3, 5), 1, 2)
4
同样可以使用递归来完成。:
julia> sub2ind_rec(dims::Tuple{}) = 1;
julia> sub2ind_rec(dims::Tuple{}, i1::Integer, I::Integer...) =
i1 == 1 ? sub2ind_rec(dims, I...) : throw(BoundsError());
julia> sub2ind_rec(dims::Tuple{Integer, Vararg{Integer}}, i1::Integer) = i1;
julia> sub2ind_rec(dims::Tuple{Integer, Vararg{Integer}}, i1::Integer, I::Integer...) =
i1 + dims[1] * (sub2ind_rec(Base.tail(dims), I...) - 1);
julia> sub2ind_rec((3, 5), 1, 2)
4
这两个实现虽然彼此不同,但几乎做了相同的事情:在数组的维度上进行运行时循环,将每个维度中的偏移量收集到最终索引中。
但是,循环所需的所有信息都嵌入在参数的信息类型中。 这允许编译器将迭代移动到编译时间,并完全消除运行时周期。 我们可以使用生成的函数来实现相同的效果;在编译器方面,我们使用生成的函数来手动折叠循环。 身体变得几乎相同,但不是计算线性索引,而是创建一个计算索引的表达式。:
julia> @generated function sub2ind_gen(dims::NTuple{N}, I::Integer...) where N
ex = :(I[$N] - 1)
for i = (N - 1):-1:1
ex = :(I[$i] - 1 + dims[$i] * $ex)
end
return :($ex + 1)
end;
julia> sub2ind_gen((3, 5), 1, 2)
4
将生成什么代码?
找出一个简单的方法是将身体提取到另一个(常规)功能中。:
julia> function sub2ind_gen_impl(dims::Type{T}, I...) where T <: NTuple{N,Any} where N
length(I) == N || return :(error("partial indexing is unsupported"))
ex = :(I[$N] - 1)
for i = (N - 1):-1:1
ex = :(I[$i] - 1 + dims[$i] * $ex)
end
return :($ex + 1)
end;
julia> @generated function sub2ind_gen(dims::NTuple{N}, I::Integer...) where N
return sub2ind_gen_impl(dims, I...)
end;
julia> sub2ind_gen((3, 5), 1, 2)
4
现在我们可以执行’sub2ind_gen_impl’并检查它返回的表达式。:
julia> sub2ind_gen_impl(Tuple{Int,Int}, Int, Int)
:(((I[1] - 1) + dims[1] * (I[2] - 1)) + 1)
因此,将在这里使用的方法的主体根本不包括循环-只索引成两个元组,乘法和加法或减法。 所有的循环执行都在编译过程中进行,我们完全避免了执行过程中的循环操作。 因此,我们只为每个type_运行一次循环,在这种情况下为每个`N’运行一次(除了在函数生成多次的边缘情况下-请参阅下面的免责声明)。
附加生成的函数
生成的函数在运行时可以实现很高的效率,但在编译时它们的使用成本很高:对于特定参数类型的每个组合,都必须生成一个新的函数体。 Julia通常可以编译函数的"通用"版本,这些版本将与任何参数一起工作,但对于生成的函数来说这是不可能的。 这意味着对于密集使用生成的函数的程序来说,静态编译可能是不可能的。
为了解决这个问题,该语言提供了一种语法,用于编写生成函数的通用,不可生成的替代实现。 如果您将其应用于上面带有’sub2ind`的示例,它将如下所示:
julia> function sub2ind_gen_impl(dims::Type{T}, I...) where T <: NTuple{N,Any} where N
ex = :(I[$N] - 1)
for i = (N - 1):-1:1
ex = :(I[$i] - 1 + dims[$i] * $ex)
end
return :($ex + 1)
end;
julia> function sub2ind_gen_fallback(dims::NTuple{N}, I) where N
ind = I[N] - 1
for i = (N - 1):-1:1
ind = I[i] - 1 + dims[i]*ind
end
return ind + 1
end;
julia> function sub2ind_gen(dims::NTuple{N}, I::Integer...) where N
length(I) == N || error("partial indexing is unsupported")
if @generated
return sub2ind_gen_impl(dims, I...)
else
return sub2ind_gen_fallback(dims, I)
end
end;
julia> sub2ind_gen((3, 5), 1, 2)
4
在内部,此代码创建函数的两个实现:生成的一个,它使用`if@generated`中的第一个块,以及常规的一个,它使用`else`块。 在’if@generated’块的’then’部分中,代码具有与其他生成函数相同的语义:参数名称引用类型,代码必须返回表达式。 可能有几个’if@generated’块,在这种情况下,生成的实现使用所有`then`块,而替代实现使用所有`else`块。
请注意,我们在函数的顶部添加了错误检查。 此代码对两个版本都是通用的,并且是两个版本中的运行时代码(它将被引用并作为生成版本的表达式返回)。 这意味着在代码生成期间局部变量的值和类型不可用-代码生成的代码只能看到参数的类型。
在这种风格中,定义代码生成函数主要是一个可选的优化。 如果方便的话,编译器会使用它,但否则它可以使用正常的实现。 这种风格是首选,因为它允许编译器做出额外的决定并以更多方式编译程序,并且正常代码比使用代码生成的代码更具可读性。 但是,将使用哪个实现取决于编译器实现的细节,因此两个实现的行为相同非常重要。