元编程
Lisp在Julia语言中最强大的遗产是它的元编程支持。 像Lisp一样,Julia将自己的代码表示为语言本身的数据结构。 由于代码由可以在语言内部创建和操作的对象表示,因此程序可以转换和生成自己的代码。 这允许在没有额外构建步骤的情况下生成复杂的代码,并且还允许在https://en.wikipedia.org/wiki/Abstract_syntax_tree[抽象语法树]。 相比之下,预处理器"宏"系统,如c和C++,在发生任何实际解析或解释之前执行文本操作和替换。 因为Julia中所有的数据类型和代码都是用Julia数据结构表示的,强大https://en.wikipedia.org/wiki/Reflection_%28computer_programming%29[反射]功能可用于探索程序的内部及其类型,就像任何其他数据一样。
|
警告元编程是一个强大的工具,但它引入了复杂性,使代码更难理解。 例如,要使范围规则正确,可能会令人惊讶地困难。 元编程通常只能在其他方法时使用,例如 高阶函数和https://en.wikipedia.org/wiki/Closure_(computer_programming)[closures]无法应用。
元编程的最佳用途通常在运行时帮助程序函数中实现其大部分功能,努力最大限度地减少它们生成的代码量。 |
程序表示
每一个茱莉亚程序都以字符串的形式开始生活:
julia> prog = "1 + 1"
"1 + 1"
*接下来会发生什么?*
下一步是https://en.wikipedia.org/wiki/Parsing#Computer_languages[parse]将每个字符串转换为一个称为表达式的对象,由Julia类型表示 Expr:
julia> ex1 = Meta.parse(prog)
:(1 + 1)
julia> typeof(ex1)
Expr
Expr 对象包含两个部分:
*一个 符号识别表达的种类。 符号是一个https://en.wikipedia.org/wiki/String_interning[interned string]标识符(下面更多讨论)。
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代码在内部表示为可从语言本身访问的数据结构。*
该 垃圾场功能提供缩进和注释显示 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)
查看表达式的另一种方法是使用 元。显示_sexpr,其中显示https://en.wikipedia.org/wiki/S-expression[s-表达式]给定的形式 Expr,这对于Lisp的用户来说可能看起来非常熟悉。 下面是一个示例,说明嵌套上的显示 Expr:
julia> Meta.show_sexpr(ex3)
(:call, :/, (:call, :+, 4, 4), 2)
符号
该 : character在Julia中有两个句法目的。 第一种形式创建一个 符号,一个https://en.wikipedia.org/wiki/String_interning[interned string]用作表达式的一个构建块,来自有效的标识符:
julia> s = :foo
:foo
朱莉娅>类型(s)
符号
该 符号构造函数接受任意数量的参数,并通过将它们的字符串表示连接在一起来创建一个新的符号:
julia> :foo === Symbol("foo")
true
julia> Symbol("1foo") # `:1foo` would not work, as `1foo` is not a valid identifier
Symbol("1foo")
julia> Symbol("func",10)
:func10
julia> Symbol(:var,'_',"sym")
:var_sym
在表达式的上下文中,符号用于指示对变量的访问;当计算表达式时,符号被替换为在适当的符号中绑定到该符号的值 范围。
有时在参数周围额外的括号 : 需要避免解析中的歧义:
julia> :(:)
:(:)
julia> :(::)
:(::)
表达式和评估
引用
的第二句法目的 : 字符是在不使用显式的情况下创建表达式对象 Expr构造函数。 这被称为_quoting_。 该 : 字符,后跟在Julia代码的单个语句周围的成对括号,产生一个 Expr 基于所附代码的对象。 下面是用于引用算术表达式的简短形式的示例:
julia> ex = :(a+b*c+1)
:(a + b * c + 1)
julia> typeof(ex)
Expr
请注意,等价表达式可以使用 元。解析,解析或直接 Expr 表格:
julia> :(a + b*c + 1) ==
Meta.parse("a + b*c + 1") ==
Expr(:call, :+, :a, Expr(:call, :*, :b, :c), 1)
true
解析器提供的表达式通常只有符号、其他表达式和文字值作为其args,而Julia代码构造的表达式可以具有任意的运行时值,而没有文字形式作为args。 在这个具体的例子中, + 和 a 是符号, *(b,c) 是子表达式,并且 1 是一个字面的64位有符号整数。
对于多个表达式,还有第二种语法形式的引用:包含在 报价。.. 结束.
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具有值参数的对象是强大的,但是 Expr 与"普通"Julia语法相比,构造函数可能很乏味。 作为替代方案,Julia允许将文本或表达式的_interpolation_转换为带引号的表达式。 插值由前缀表示 $.
在这个例子中,变量的值 a 被插值:
julia> a = 1;
julia> 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))
嵌套报价
自然,quote表达式可以包含其他quote表达式。 了解插值在这些情况下的工作原理可能有点棘手。 考虑这个例子:
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
然而,外 报价 表达式能够在 $ 在内心报价中。 这是用多个 $s:
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值,而二 $s做相当于 eval(eval(:x)).
报价/报价
A的通常表示 报价 AST中的形式是 Expr带头 :报价:
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
正如我们所看到的,这样的表达式支持插值与 $. 但是,在某些情况下,有必要引用执行插值的代码_without_。 这种引用还没有语法,但在内部表示为类型的对象 报价/报价:
julia> eval(Meta.quot(Expr(:$, :(1+2))))
3
julia> eval(QuoteNode(Expr(:$, :(1+2))))
:($(Expr(:$, :(1 + 2))))
解析器产生 报价/报价s用于简单的引用项目,如符号:
julia> dump(Meta.parse(":x"))
QuoteNode
value: Symbol x
[医]报价单 也可用于某些高级元编程任务。
求值表达式
给定一个表达式对象,可以使Julia在全局范围内使用 埃瓦尔:
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 对象可以通过编程方式构造,然后进行评估,可以动态生成任意代码,然后使用 埃瓦尔. 下面是一个简单的例子:
julia> a = 1;
julia> ex = Expr(:call, :+, a, :b)
:(1 + b)
julia> a = 0; b = 2;
julia> eval(ex)
3
的价值 a 用于构建表达式 前 适用于 + 函数值1和变量 b. 注意方式之间的重要区别 a 和 b 被使用:
*变量的值_ a 在表达式构建时用作表达式中的立即值。 因此,值 a 当表达式被计算不再重要时:表达式中的值已经 1,独立于 a 也许是。
*另一方面,symbol :b 在表达式构造中使用,因此变量的值 b 那时是无关紧要的 — :b 只是一个符号和变量 b 甚至不需要定义。 但是,在表达式求值时,符号的值 :b 通过查找变量的值来解决 b.
上的功能 Expr[医]附言
如上所述,Julia的一个非常有用的特性是能够在Julia本身内生成和操作Julia代码。 我们已经看到一个函数返回的例子 Expr目标: 元。解析,解析函数,它接受一串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)
朱莉娅>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)
julia> eval(ex)
42
宏
宏提供了一种机制,将生成的代码包含在程序的最终主体中。 宏将参数元组映射到返回的_expression_,生成的表达式直接编译,而不需要运行时 埃瓦尔呼叫。 宏参数可以包括表达式、文字值和符号。
基本知识
这是一个非常简单的宏:
julia> macro sayhello()
return :( println("Hello, world!") )
end
@sayhello (macro with 1 method)
宏在Julia的语法中有一个专用字符: @ (at-sign),后跟在a中声明的唯一名称 宏名称。.. 结束 块。 在此示例中,编译器将替换 @sayhello 与:
:( println("Hello, world!") )
何时 @sayhello 在REPL中输入,表达式立即执行,因此我们只看到计算结果:
julia> @sayhello()
Hello, world!
现在,考虑一个稍微复杂一点的宏:
julia> macro sayhello(name)
return :( println("Hello, ", $name) )
end
@sayhello (macro with 1 method)
这个宏需要一个参数: 姓名. 何时 @sayhello 遇到时,带引号的表达式为_expanded_,以将参数的值插值到最终表达式中:
julia> @sayhello("human")
Hello, human
我们可以使用函数查看引用的返回表达式 宏扩展(*重要提示:*这是一个非常有用的工具,用于调试宏):
julia> ex = macroexpand(Main, :(@sayhello("human")) )
:(Main.println("Hello, ", "human"))
julia> typeof(ex)
Expr
我们可以看到 "人类" 字面量已插值到表达式中。
还有一个宏 @macroexpand那也许比……方便一点。 宏扩展 功能:
julia> @macroexpand @sayhello "human"
:(println("Hello, ", "human"))
保持:为什么宏?
我们已经看到了一个功能 f(::Expr...)->Expr 在上一节中。 事实上, 宏扩展也是这样的功能。 那么,宏为什么存在呢?
宏是必要的,因为它们在代码解析时执行,因此,宏允许程序员生成并包含自定义代码的片段_before_完整程序运行。 为了说明差异,请考虑以下示例:
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))
julia> typeof(ex)
Expr
julia> ex
:(println("I execute at runtime. The argument is: ", $(Expr(:copyast, :($(QuoteNode(:((1, 2, 3)))))))))
julia> eval(ex)
I execute at runtime. The argument is: (1, 2, 3)
宏调用
使用以下常规语法调用宏:
@name expr1 expr2 ...
@name(expr1, expr2, ...)
注意区分 @ 在宏名称之前,第一种形式的参数表达式之间缺少逗号,以及后面缺少空格 @姓名 在第二种形式。 这两种风格不应该混合使用。 例如,以下语法与上面的示例不同;它传递元组 (expr1,expr2,。..) 作为宏的一个参数:
@name (expr1, expr2, ...)
在数组字面量(或理解)上调用宏的另一种方法是将两者并置而不使用括号。 在这种情况下,数组将是馈送到宏的唯一表达式。 以下语法是等价的(并且不同于 @姓名[a b]*v):
@name[a b] * v
@name([a b]) * v
需要强调的是,宏将其参数接收为表达式、文字或符号。 探索宏参数的一种方法是调用 展览宏体内的功能:
julia> macro showarg(x)
show(x)
# ... remainder of macro, returning an expression
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) # Numeric literal
1
julia>@showarg("哟!")#字符串文字
"哟!"
julia>@showarg("哟! $("hello")")#带有插值的字符串是Expr而不是字符串
:("Yo! $("hello")")
除了给定的参数列表之外,每个宏都传递了额外的参数。 __来源__ 和 __模块__.
论点 __来源__ 提供资料(以 N.线,线 对象)关于解析器的位置 @ 从宏调用中签名。 这允许宏包含更好的错误诊断信息,并且通常用于日志记录、字符串解析器宏和文档,例如,以及实现 @__线__, @__文件__,和 @__DIR__宏。
位置信息可以通过引用访问 __来源__。行 和 __来源__。档案:
julia> macro __LOCATION__(); return QuoteNode(__source__); end
@__LOCATION__ (macro with 1 method)
julia> dump(
@__LOCATION__(
))
LineNumberNode
line: Int64 2
file: Symbol none
论点 __模块__ 提供资料(以 模块 对象)关于宏调用的扩展上下文。 这允许宏查找上下文信息(如现有绑定),或者将值作为额外参数插入到在当前模块中进行自反射的运行时函数调用中。
构建高级宏
这是Julia的简化定义 @断言宏:
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) 被拼接到测试条件槽中,而值 字符串(:(1==1.0)) 被拼接到断言消息槽中。 这样构造的整个表达式被放置到语法树中,其中 @断言 发生宏调用。 然后在执行时,如果测试表达式计算为true,则 什么都没有 返回,而如果测试为false,则会引发一个错误,指示断言的表达式为false。 请注意,不可能将其写为函数,因为只有条件的_value_可用,并且不可能在错误消息中显示计算它的表达式。
的实际定义 @断言 在朱莉娅基地更复杂。 它允许用户可选地指定自己的错误消息,而不仅仅是打印失败的表达式。 就像在参数数量可变的函数中一样(Varargs函数),这是在最后一个参数后面用省略号指定的:
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)
现在 @断言 有两种操作模式,取决于它接收的参数数量! 如果只有一个参数,则由 味精 将为空,它的行为与上面更简单的定义相同。 但是现在,如果用户指定了第二个参数,它将打印在消息正文中,而不是失败的表达式中。 您可以使用恰当的命名来检查宏扩展的结果 @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)
还有另一种情况是实际的 @断言 宏句柄:如果除了打印"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,宏正在接收一个完整的表达式,该表达式需要进行计算才能按预期显示。 这可以直接拼接到返回的表达式中,作为 字符串呼叫;见https://github.com/JuliaLang/julia/blob/master/base/error.jl[脧锚脧赂`错误。jl`]进行完整的实施。
该 @断言 宏很好地利用拼接成带引号的表达式来简化宏体内部表达式的操作。
卫生学
在更复杂的宏中出现的一个问题是https://en.wikipedia.org/wiki/Hygienic_macro[卫生].简而言之,宏必须确保它们在其返回的表达式中引入的变量不会意外地与它们扩展到的周围代码中的现有变量发生冲突。 相反,作为参数传递到宏中的表达式通常是_expected_在周围代码的上下文中进行计算,与现有变量交互并修改。 另一个问题是,宏可能在与定义宏不同的模块中被调用。 在这种情况下,我们需要确保所有全局变量都解析为正确的模块。 与具有文本宏扩展(如C)的语言相比,Julia已经具有主要优势,因为它只需要考虑返回的表达式。 所有其他变量(如 味精 在 @断言 上)按照 正常范围块行为。
为了证明这些问题,让我们考虑写一个 @时间 以表达式为参数的宏,记录时间,计算表达式,再次记录时间,打印前后时间之间的差异,然后将表达式的值作为其最终值。 宏可能如下所示:
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,而 瓦尔 作为私有临时变量,我们希望 时间_ns 参考 时间_ns功能在朱莉娅基地,而不是任何 时间_ns 用户可能拥有的变量(同样适用于 打印,打印). 想象一下,如果用户表达式可能出现的问题 前 还包含对变量的赋值 t0,或定义了自己的 时间_ns 变量。 我们可能会得到错误,或神秘不正确的行为。
Julia的宏扩展器通过以下方式解决了这些问题。 首先,宏结果中的变量被分类为局部或全局。 如果变量被分配给(而不是声明为全局)、声明为局部或用作函数参数名,则将其视为局部变量。 否则,它被认为是全球性的。 然后将局部变量重命名为唯一(使用 [医根西姆]函数,它生成新的符号),并且全局变量在宏定义环境内解析。 因此,上述两个问题都得到了处理;宏的局部变量不会与任何用户变量发生冲突,并且 时间_ns 和 打印,打印 将参考Julia Base定义。
然而,一个问题仍然存在。 请考虑以下使用此宏:
module MyModule
import Base.@time
time_ns() = ... # compute something
@time time_ns()
end
这里是用户表达式 前 是一个电话 时间_ns,但不一样 时间_ns 宏使用的功能。 它明确地指 MyModule的。时间_ns. 因此,我们必须安排代码在 前 要在宏调用环境中进行解析。 这是通过"转义"表达式来完成的 电调:
macro time(ex)
...
local val = $(esc(ex))
...
end
以这种方式包装的表达式由宏扩展器单独保留,并简单地逐字粘贴到输出中。 因此,它将在宏调用环境中解决。
这种转义机制可用于在必要时"违反"卫生,以便引入或操纵用户变量。 例如,以下宏集 x 在呼叫环境中为零:
julia> macro zerox()
return esc(:(x = 0))
end
@zerox (macro with 1 method)
julia> function foo()
x = 1
@zerox
return x # is zero
end
foo (generic function with 1 method)
julia> foo()
0
这种对变量的操作应该明智地使用,但偶尔会非常方便。
正确的卫生规则可能是一个巨大的挑战。 在使用宏之前,您可能需要考虑函数闭包是否足够。 另一个有用的策略是将尽可能多的工作推迟到运行时。 例如,许多宏简单地将其参数包装在 [医]报价单 或其他类似 Expr. 这方面的一些例子包括 @任务体 它只是返回 时间表(任务(()->$body)),而 @eval expr,它简单地返回 eval(QuoteNode(expr)).
为了证明,我们可以重写 @时间 上面的例子如下:
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 在一个新的作用域块(匿名函数)也稍微改变了表达式的含义(其中任何变量的作用域),而我们希望 @时间 在对包装代码的影响最小的情况下使用。
宏和调度
宏,就像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中,表达式插值和 埃瓦尔允许这样的代码生成在程序执行的正常过程中进行。 例如,请考虑以下自定义类型
struct MyNumber
x::Float64
end
# output
为此,我们想添加一些方法。 我们可以在以下循环中以编程方式执行此操作:
for op = (:sin, :cos, :tan, :log, :exp)
eval(quote
Base.$op(a::MyNumber) = MyNumber($op(a.x))
end)
end
# output
我们现在可以将这些函数与我们的自定义类型一起使用:
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(...)) 模式,是足够常见的,朱莉娅带有一个宏来缩写这个模式:
for op = (:sin, :cos, :tan, :log, :exp)
@eval Base.$op(a::MyNumber) = MyNumber($op(a.x))
end
@eval begin
# multiple lines
end
非标准字符串文字
召回自 Strings以标识符为前缀的字符串文字称为非标准字符串文字,并且可以具有与未前缀的字符串文字不同的语义。 例如:
* r"^\s*(?:#|$)" 产生一个 正则表达式对象而不是字符串
* b"数据\xff\u2200" 是一个 字节数组字面量为 [68,65,84,65,255,226,136,128].
也许令人惊讶的是,这些行为并没有硬编码到Julia解析器或编译器中。 相反,它们是由任何人都可以使用的通用机制提供的自定义行为:前缀字符串文字被解析为对特殊命名的宏的调用。 例如,正则表达式宏如下所示:
macro r_str(p)
Regex(p)
end
仅此而已。 此宏表示字符串字面量的字面内容 r"^\s*(?:#|$)" 应传递给 @r_str 宏和扩展的结果应该放在出现字符串字面量的语法树中。 换句话说,表达 r"^\s*(?:#|$)" 等同于将以下对象直接放入语法树中:
Regex("^\\s*(?:#|\$)")
字符串文字形式不仅更短、更方便,而且效率更高:由于正则表达式是编译的, 正则表达式 对象实际上是创建的_当代码被编译时,编译只发生一次,而不是每次执行代码时。 考虑正则表达式是否发生在循环中:
for line = lines
m = match(r"^\s*(?:#|$)", line)
if m === nothing
# non-comment
else
# comment
end
end
由于正则表达式 r"^\s*(?:#|$)" 被编译并插入到语法树中当解析此代码时,表达式只编译一次,而不是每次执行循环时。 为了在没有宏的情况下完成此操作,必须编写这样的循环:
re = Regex("^\\s*(?:#|\$)")
for line = lines
m = match(re, line)
if m === nothing
# non-comment
else
# comment
end
end
此外,如果编译器无法确定regex对象在所有循环中都是常量,则可能无法进行某些优化,这使得此版本的效率仍然低于上面更方便的字面形式。 当然,在某些情况下,非文字形式更方便:如果需要将变量插入正则表达式,则必须采用这种更详细的方法;如果正则表达式模式本身是动态的,可能会在每次循环迭代中发生变化,则必须在每次迭代中构造一个新的正则表达式对象。 然而,在绝大多数用例中,正则表达式不是基于运行时数据构建的。 在大多数情况下,将正则表达式写成编译时值的能力是非常宝贵的。
用户定义的字符串文字的机制是深刻的,深刻的强大。 不仅是Julia的非标准字面量使用它实现,而且命令字面量语法(回声"你好,我的人")也使用以下看起来无害的宏来实现:
macro cmd(str)
:(cmd_gen($(shell_parse(str)[1])))
end
当然,在这个宏定义中使用的函数中隐藏了大量的复杂性,但它们只是函数,完全用Julia编写。 您可以阅读它们的源代码并准确地查看它们所做的事情-并且它们所做的只是构造要插入到程序语法树中的表达式对象。
与字符串字面量一样,命令字面量也可以以标识符作为前缀,以形成所谓的非标准命令字面量。 这些命令文本被解析为对特殊命名的宏的调用。 例如,语法 海关规定字面意思`` 被解析为 @custom_cmd"文字". Julia本身不包含任何非标准命令文本,但包可以使用此语法。 除了不同的语法和 _cmd 后缀而不是 _str 后缀,非标准命令字面量的行为与非标准字符串字面量完全相同。
如果两个模块提供具有相同名称的非标准字符串或命令文本,则可以使用模块名称限定字符串或命令文本。 例如,如果两者都有 [医]脚 和 酒吧 提供非标准字符串字面量 @x_str,那么一个可以写 Foo。x"字面" 或 酒吧。x"字面" 在两者之间消除歧义。
定义宏的另一种方法是这样的:
macro foo_str(str, flag)
# do stuff
end
然后可以使用以下语法调用此宏:
foo"str"flag
在上面提到的语法标志的类型将是一个 字符串 带有字符串文字后面的任何跟踪的内容。
生成的函数
一个非常特殊的宏是 @生成,它允许您定义所谓的_generated functions_。 它们能够根据其参数的类型生成专门的代码,具有比使用多个分派可以实现的更大的灵活性和/或更少的代码。 虽然宏在解析时使用表达式,并且无法访问其输入的类型,但生成的函数在已知参数类型但尚未编译该函数时会被扩展。
生成的函数声明不是执行某些计算或操作,而是返回一个带引号的表达式,然后该表达式形成与参数类型相对应的方法的主体。 当一个生成的函数被调用时,它返回的表达式被编译然后运行。 为了提高效率,通常会缓存结果。 为了使这个推断,只有有限的语言子集是可用的。 因此,生成的函数提供了一种灵活的方式来将工作从运行时移到编译时,而牺牲了对允许的构造的更大限制。
在定义生成函数时,与普通函数有五个主要区别:
-
您可以使用
@生成宏。 这会向AST添加一些信息,让编译器知道这是一个生成的函数。 -
在生成的函数体中,您只能访问参数的_types_-而不是它们的值。
-
而不是计算某些东西或执行某些操作,你返回一个_quoted expression_,当评估时,它会做你想要的。
-
生成的函数只允许调用已定义_before_生成函数的定义的函数。 (不遵循这可能会导致获得
方法器指的是未来世界时代的功能。) -
生成的函数不得_mutate_或_observe_任何非常量全局状态(包括例如IO、锁、非本地字典或使用
哈斯米索德). 这意味着它们只能读取全局常量,并且不能有任何副作用。 换句话说,它们必须是完全纯净的。 由于实现限制,这也意味着他们目前无法定义闭包或生成器。
用一个例子来说明这一点是最容易的。 我们可以声明一个生成的函数 [医]脚 作为
julia> @generated function foo(x)
Core.println(x)
return :(x * x)
end
foo (generic function with 1 method)
注意,body返回一个带引号的表达式,即 :(x*x),而不仅仅是 x*x.
从调用者的角度来看,这与常规函数相同;事实上,您不必知道您是在调用常规函数还是生成函数。 让我们看看如何 [医]脚 行为举止:
julia> x = foo(2); # note: output is from println() statement in the body
Int64
julia> x # now we print x
4
julia> y = foo("bar");
String
julia> y
"barbar"
所以,我们看到在生成函数的主体中, x 是传递参数的_type_,以及生成的函数返回的值,是计算我们从定义返回的带引号的表达式的结果,现在使用_value_ x.
如果我们评估会发生什么 [医]脚 再次与我们已经使用的类型?
julia> foo(4)
16
请注意,没有打印输出 Int64. 我们可以看到,对于特定的参数类型集,生成函数的主体在这里只执行了一次,结果被缓存了。 之后,对于此示例,在第一次调用时从生成的函数返回的表达式被重新用作方法主体。 但是,实际的缓存行为是实现定义的性能优化,因此过于紧密地依赖于此行为是无效的。
生成函数的次数_might_只能生成一次,但它_might_也会更频繁,或者看起来根本不会发生。 因此,您应该_never_编写一个带有副作用的生成函数-副作用发生的时间和频率未定义。 (宏也是如此-就像宏一样,使用 埃瓦尔在一个生成的函数是一个迹象,表明你正在做一些错误的方式。)但是,与宏不同,运行时系统无法正确处理对 埃瓦尔,所以是不允许的。
看看如何做也很重要 @生成 函数与方法重新定义交互。 遵循正确的原则 @生成 函数不能观察到任何可变状态或导致全局状态的任何突变,我们看到以下行为。 观察生成的函数_cannot_调用生成函数本身的_definition_之前未定义的任何方法。
最初 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(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)
由于生成的函数的主体是非确定性的,因此它的行为_以及所有后续代码的行为都是未定义的。
不要复制这些例子!
这些示例希望有助于说明生成的函数在定义端和调用站点的工作方式;但是,由于以下原因,don’t copy them:
* [医]脚 函数有副作用(调用 核心。打印,打印),并且未定义这些副作用发生的时间,频率或次数
* 酒吧 函数解决了一个使用多个调度定义更好地解决的问题 条(x)=x 和 bar(x::整数)=x^2 会做同样的事情,但它既简单又快。
* 巴兹 功能是病态的
请注意,在生成的函数中不应尝试的操作集是无界的,运行时系统当前只能检测到无效操作的子集。 还有许多其他操作会在没有通知的情况下简单地破坏运行时系统,通常以微妙的方式没有明显地连接到糟糕的定义。 由于函数生成器是在推理期间运行的,因此它必须尊重该代码的所有限制。
一些不应该尝试的操作 include:
-
本机指针的缓存。
-
与
核心。编译器以任何方式。 -
观察任何可变状态。 **生成函数的推理可以在_any_time运行,包括在您的代码试图观察或改变此状态时。
-
采取任何锁:你调用的C代码可能在内部使用锁,(例如,调用它没有问题
马洛克,即使大多数实现在内部需要锁),但在执行Julia代码时不要尝试持有或获取任何。 -
调用在生成函数主体之后定义的任何函数。 对于增量加载的预编译模块,此条件放宽,以允许调用模块中的任何函数。
好吧,现在我们已经更好地理解了生成的函数是如何工作的,让我们用它们来构建一些更高级(和有效)的功能。..
一个高级示例
朱莉娅的基础库有一个内部 子2ind 函数计算一个线性索引到一个n维数组,基于一组n个多线性索引-换句话说,计算索引 i 它可以用来索引到数组中 A 使用 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
这两个实现虽然不同,但基本上做了相同的事情:在数组的维度上循环运行时,将每个维度中的偏移量收集到最终索引中。
但是,循环所需的所有信息都嵌入在参数的类型信息中。 这允许编译器将迭代移动到编译时间,并完全消除运行时循环。 我们可以利用生成的函数来达到类似的效果;用编译器的说法,我们使用生成的函数来手动展开循环。 身体变得几乎相同,但不是计算线性索引,而是建立一个计算索引的_expression_:
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
我们现在可以执行 子2ind_gen_impl 并检查它返回的表达式:
julia> sub2ind_gen_impl(Tuple{Int,Int}, Int, Int)
:(((I[1] - 1) + dims[1] * (I[2] - 1)) + 1)
所以,这里将使用的方法体根本不包括循环-只是索引到两个元组,乘法和加法/减法。 所有的循环都是在编译时执行的,我们完全避免在执行过程中循环。 因此,我们只循环_once每个type_,在这种情况下,每个 N (除了在函数生成不止一次的边缘情况下-见上面的免责声明)。
可选生成的函数
生成的函数在运行时可以实现很高的效率,但也有编译时的开销:必须为每个具体参数类型的组合生成一个新的函数体。 通常,Julia能够编译适用于任何参数的函数的"泛型"版本,但对于生成的函数,这是不可能的。 这意味着大量使用生成函数的程序可能无法静态编译。
为了解决这个问题,该语言提供了用于编写生成函数的正常、非生成的替代实现的语法。 应用于 子2ind 上面的例子,它看起来像这样:
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>函数sub2ind_gen_fallback(dims::NTuple{N},I)其中N
ind=I[N]-1
对于i=(N-1):-1:1
ind=I[i]-1+dims[i]*ind
结束
返回ind+1
结束;
julia>函数sub2ind_gen(dims::NTuple{N},I::整数。..)其中N
长度(I)==n||错误("不支持部分索引")
如果@生成
返回sub2ind_gen_impl(dims,I。..)
其他
返回sub2ind_gen_fallback(dims,I)
结束
结束;
朱莉娅>sub2ind_gen((3, 5), 1, 2)
4
在内部,这段代码创建了函数的两个实现:一个生成的实现,其中第一个块在 如果@生成 被使用,和一个正常的,其中 其他 块被使用。 在 然后 的一部分 如果@生成 块,代码与其他生成的函数具有相同的语义:参数名称引用类型,代码应该返回一个表达式。 多个 如果@生成 块可能会发生,在这种情况下,生成的实现使用所有的 然后 块和备用实现使用所有的 其他 街区。
请注意,我们在函数的顶部添加了一个错误检查。 此代码对两个版本都是通用的,并且在两个版本中都是运行时代码(它将被引用并作为生成版本的表达式返回)。 这意味着局部变量的值和类型在代码生成时不可用—代码生成代码只能看到参数的类型。
在这种定义风格中,代码生成功能本质上是一种可选的优化。 如果方便的话,编译器会使用它,但否则可能会选择使用正常的实现。 这种风格是优选的,因为它允许编译器做出更多的决定并以更多的方式编译程序,并且由于正常代码比代码生成代码更具可读性。 但是,使用哪个实现取决于编译器实现细节,因此两个实现的行为相同至关重要。