Kernel.SpecialForms
特殊表单是Elixir的基本构建块,因此开发人员不能重写它。
我们在这个模块中定义它们。有些形式是词汇(如alias/2
,case/2
等)。宏{}
和<<>>
也用于分别限定元组和二进制数据结构特殊形式。
该模块还记录了返回有关Elixir的编译环境信息的宏,如(__ENV__/0
,__MODULE__/0
,__DIR__/0
和__CALLER__/0
)。
最后,它还记录了两种特殊形式,__block__/1
与__aliases__/1
,这些不打算由开发商直接调用,但它们出现在引用的内容中,因为它们在Elixir的构造中是必不可少的。
函数
%
创建一个结构
%{}
创建map
&(expr)
捕获或创建一个匿名函数
left . right
定义远程调用、对匿名函数的调用或别名。
left :: right
由类型和位字符串使用以指定类型。
<<args>>
定义新的位串。
left = right
将右边的值与左边的模式相匹配。
^var
在匹配子句中访问已绑定的变量。也被称为pin操作符
__CALLER__
以Macro.Env
结构形式返回当前调用环境
__DIR__
以二进制形式返回当前文件目录的绝对路径。
_ENV__
以Macro.Env
结构形式返回当前环境信息
__MODULE__
将当前模块名称作为原子返回或以其他方式返回nil
__aliases__(args)
保存别名信息的内部特殊形式
__block__(args)
块表达式的内部特殊形式
alias(module, opts)
alias/2
用于设置别名,通常用于模块名称。
case(condition, clauses)
将给定表达式与给定子句匹配。
cond(clauses)
计算与第一个子句对应的表达式,该表达式的计算值为真实值。
fn [clauses] end
定义匿名函数
for(args)
理解允许您快速地从可枚举或位字符串构建数据结构。
import(module, opts)
从其他模块导入函数和宏
quote(opts, block)
获取任何表达式的表示形式。
receive(args)
检查当前进程邮箱中是否存在匹配给定子句的消息。
require(module, opts)
需要一个模块才能使用它的宏。
super(args)
覆盖它时调用覆盖函数 Kernel.defoverridable/1
try(args)
计算给定表达式并处理可能发生的任何错误、退出或抛出。
unquote(expr)
从宏中取消引用给定表达式
unquote_splicing(expr)
取消引用扩展其参数的给定列表。类似于unquote/1
with(args)
用于组合匹配子句
{args}
创建元组
% (macro)
创建一个结构。
struct是一个带标记的映射,允许开发人员为键、用于多态分派的标记和编译时断言提供默认值。
结构通常使用Kernel.defstruct/1
宏:
defmodule User do
defstruct name: "john", age: 27
end
现在可以创建如下结构:
%User{}
在一个结构体下方,只是一个带有:__struct__
指向User
模块的键的映射:
%User{} == %{__struct__: User, name: "john", age: 27}
结构也验证给定的键是定义的结构的一部分。下面的例子将失败,因为没有键:full_name
的User
结构:
%User{full_name: "john doe"}
还可以使用特定于结构的更新操作:
%User{user | age: 28}
上面的语法将保证给定的键在编译时有效,并且它将确保运行时给定的参数是一个结构,否则BadStructError
将失败。
虽然结构是map,但默认情况下,结构不会实现任何为map实现的协议。查看Kernel.defprotocol/2
更多关于结构如何与多态调度协议一起使用的信息。另请参阅Kernel.struct/2
以及Kernel.struct!/2
有关如何动态创建和更新结构的示例。
%{} (macro)
创建一个map。
查看Map
模块以获取更多关于地图的信息,它们的语法以及访问和操作它们的方法。
AST表示
无论=>
是使用关键字语法还是使用关键字语法,为了简单起见,映射中的键值对总是在内部表示为两元素元组的列表:
iex> quote do
...> %{"a" => :b, c: :d}
...> end
{:%{}, [], [{"a", :b}, {:c, :d}]}
&(expr) (macro)
捕获或创建匿名函数。
俘获
捕获运算符最常用于从模块中捕获具有给定名称和模块的函数:
iex> fun = &Kernel.is_atom/1
iex> fun.(:atom)
true
iex> fun.("string")
false
在上面的例子中,我们捕获了Kernel.is_atom/1
然后调用它。
捕获操作符还可以通过省略模块名来捕获本地函数(包括私有函数)和导入函数:
&local_function/1
匿名函数
捕获操作符也可用于部分应用函数,其中&1
,&2
等等,可以用作值占位符。例如:
iex> double = &(&1 * 2)
iex> double.(2)
4
换句话说,&(&1 * 2)
相当于fn x -> x * 2 end
。使用本地函数的另一个示例:
iex> fun = &is_atom(&1)
iex> fun.(:atom)
true
该&
运算符可以用更复杂的表达式中使用:
iex> fun = &(&1 + &2 + &3)
iex> fun.(1, 2, 3)
6
以及列表和元组:
iex> fun = &{&1, &2}
iex> fun.(1, 2)
{1, 2}
iex> fun = &[&1 | &2]
iex> fun.(1, 2)
[1 | 2]
创建匿名函数的唯一限制是至少有一个占位符必须存在,即它必须至少包含&1
,并且不支持该块表达式:
# No placeholder, fails to compile.
&(:foo)
# Block expression, fails to compile.
&(&1; &2)
left . right (macro)
定义远程调用、对匿名函数的调用或别名。
Elixir中的dot(.
)可用于远程调用:
iex> String.downcase("FOO")
"foo"
在上面的这个例子中,我们使用了. 在String模块中调用downcase,传递“FOO”作为参数。
点也可用于调用匿名函数:
iex> (fn(n) -> n end).(7)
7
在这种情况下,左侧有一个功能。
我们还可以使用点创建别名:
iex> Hello.World
Hello.World
这一次,我们加入了两个别名,定义了最终的别名。Hello.World
...
句法
右面.
可以是以大写开头的单词,表示别名、以小写或下划线开头的单词、任何有效的语言运算符或以单引号或双引号包装的任何名称。这些都是有效的例子:
iex> Kernel.Sample
Kernel.Sample
iex> Kernel.length([1, 2, 3])
3
iex> Kernel.+(1, 2)
3
iex> Kernel."length"([1, 2, 3])
3
iex> Kernel.'+'(1, 2)
3
请注意,Kernel."FUNCTION_NAME"
将被视为远程呼叫而不是别名。这种选择是在每次使用单引号或双引号时完成的,无论引用内容如何,我们都有远程调用。这个决定也反映在下面讨论的引用表达式中。
当点用来调用一个匿名函数时,只有一个操作数,但仍然使用后缀表示法编写:
iex> negate = fn(n) -> -n end
iex> negate.(7)
-7
引用的表达
当.
被使用时,引用的表达式可能有两种截然不同的形式。当右侧以小写字母(或下划线)开头时:
iex> quote do
...> String.downcase("FOO")
...> end
{{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], ["FOO"]}
请注意,我们有一个内部元组,其中包含:.
代表点的原子作为第一个元素:
{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}
该元组遵循Elixir中通用的引用表达式结构,名称作为第一个参数,一些关键字列表作为元数据作为第二个参数,而参数列表作为第三个参数。在这种情况下,参数是别名String
和原子:downcase
。远程调用中的第二个参数始终是一个原子,而不管在调用中使用的文字是什么:
iex> quote do
...> String."downcase"("FOO")
...> end
{{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], ["FOO"]}
包含的元组:.
被包装在另一个元组中,该元组实际表示函数调用,并且具有"FOO"
作为参数。
在调用匿名函数的情况下,具有点特殊形式的内部元组只有一个参数,这反映了操作符是一元的事实:
iex> quote do
...> negate.(0)
...> end
{{:., [], [{:negate, [], __MODULE__}]}, [], [0]}
当右侧是别名(即以大写字母开头)时,我们得到:
iex> quote do
...> Hello.World
...> end
{:__aliases__, [alias: false], [:Hello, :World]}
我们在__aliases__/1
特殊格式文档中详细介绍别名。
不引用
我们还可以使用unquote在带引号的表达式中生成远程调用:
iex> x = :downcase
iex> quote do
...> String.unquote(x)("FOO")
...> end
{{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], ["FOO"]}
类似于Kernel."FUNCTION_NAME"
,unquote(x)
将始终生成一个远程调用,独立于该值x
。要通过引用的表达式生成别名,需要依赖Module.concat/2
:
iex> x = Sample
iex> quote do
...> Module.concat(String, unquote(x))
...> end
{{:., [], [{:__aliases__, [alias: false], [:Module]}, :concat]}, [],
[{:__aliases__, [alias: false], [:String]}, Sample]}
left :: right (macro)
由类型和位串使用来指定类型。
这个操作符在Elixir的两个不同的场合使用。它用于类型描述来指定变量,函数或类型本身的类型:
@type number :: integer | float
@spec add(number, number) :: number
它也可以用于位串以指定给定位段的类型:
<<int::integer-little, rest::bits>> = bits
阅读Typespec
页面上的文档,<<>>/1
并分别获取关于类型规范和位串的更多信息。
<<args>> (macro)
定义一个新的位串。
实例
iex> <<1, 2, 3>>
<<1, 2, 3>>
类型
位串由多个段组成,每个段都有一个类型。在位字符串中使用了9种类型:
integer
float
bits
(bitstring
的别名)
bitstring
binary
bytes
(binary
的别名)
utf8
utf16
utf32
当没有指定类型时,默认值是integer
:
iex> <<1, 2, 3>>
<<1, 2, 3>>
Elixir还默认接受该段为文字字符串或文字charlist,默认情况下该段扩展为整数:
iex> <<0, "foo">>
<<0, 102, 111, 111>>
变量或任何其他类型都需要显式标记:
iex> rest = "oo"
iex> <<102, rest>>
** (ArgumentError) argument error
我们可以通过显式地将其标记为binary
*
iex> rest = "oo"
iex> <<102, rest::binary>>
"foo"
这些utf8
,utf16
和utf32
类型是用于Unicode码点的。它们也可以应用于字符串和查询表:
iex> <<"foo"::utf16>>
<<0, 102, 0, 111, 0, 111>>
iex> <<"foo"::utf32>>
<<0, 0, 0, 102, 0, 0, 0, 111, 0, 0, 0, 111>>
备选方案
可以通过使用-
作为隔膜。顺序是任意的,因此以下所有内容都是等价的:
<<102::integer-native, rest::binary>>
<<102::native-integer, rest::binary>>
<<102::unsigned-big-integer, rest::binary>>
<<102::unsigned-big-integer-size(8), rest::binary>>
<<102::unsigned-big-integer-8, rest::binary>>
<<102::8-integer-big-unsigned, rest::binary>>
<<102, rest::binary>>
单位和大小
匹配的长度等于unit
(size
长度重复段的数量)乘以(位数)倍unit
。
类型 | 默认单位 |
---|---|
整数 | 1位 |
浮动 | 1位 |
二进制 | 8位 |
类型的大小稍微有点细微差别。整数的默认大小是8。
对于浮点数,它是64.对于浮点数,size * unit
必须为32或64,分别对应于IEEE 754 binary32和binary64。
对于二进制文件,缺省值是二进制文件的大小。只有匹配中的最后一个二进制文件才能使用默认大小。其他所有的必须显式指定它们的大小,即使匹配是明确的。例如:
iex> <<name::binary-size(5), " the ", species::binary>> = <<"Frank the Walrus">>
"Frank the Walrus"
iex> {name, species}
{"Frank", "Walrus"}
未能指定非最后一次的大小会导致编译失败:
<<name::binary, " the ", species::binary>> = <<"Frank the Walrus">>
** (CompileError): a binary field without size is only allowed at the end of a binary pattern
快捷语法
在传递整数值时,还可以使用语法快捷方式指定大小和单位:
iex> x = 1
iex> <<x::8>> == <<x::size(8)>>
true
iex> <<x::8*4>> == <<x::size(8)-unit(4)>>
true
这个语法反映了一个事实,即有效的大小是通过单位乘以大小来确定的。
修饰符
有些类型有关联的修饰符来清除字节表示中的歧义。
模式 | 相关类型 |
---|---|
signed | integer |
unsigned (default) | integer |
little | integer, float, utf16, utf32 |
big (default) | integer, float, utf16, utf32 |
native | integer, utf16, utf32 |
标志
整数可以是,signed
或unsigned
,默认为unsigned
。
iex> <<int::integer>> = <<-100>>
<<156>>
iex> int
156
iex> <<int::integer-signed>> = <<-100>>
<<156>>
iex> int
-100
signed
与unsigned
仅用于匹配二进制文件(见下文),仅用于整数。
iex> <<-100::signed, _rest::binary>> = <<-100, "foo">>
<<156, 102, 111, 111>>
字节序
Elixir对字节序有三个选项:big
,little
,和native
。默认值是big
:
iex> <<number::little-integer-size(16)>> = <<0, 1>>
<<0, 1>>
iex> number
256
iex> <<number::big-integer-size(16)>> = <<0, 1>>
<<0, 1>>
iex> number
1
native
由VM在启动时确定并取决于主机操作系统。
二进制/位串匹配
二进制匹配是Elixir的一个强大特性,它有助于从二进制文件中提取信息以及模式匹配。
二进制匹配本身可以用于从二进制文件中提取信息:
iex> <<"Hello, ", place::binary>> = "Hello, World"
"Hello, World"
iex> place
"World"
或者作为模式匹配函数定义的一部分:
defmodule ImageTyper
@png_signature <<137::size(8), 80::size(8), 78::size(8), 71::size(8),
13::size(8), 10::size(8), 26::size(8), 10::size(8)>>
@jpg_signature <<255::size(8), 216::size(8)>>
def type(<<@png_signature, rest::binary>>), do: :png
def type(<<@jpg_signature, rest::binary>>), do: :jpg
def type(_), do :unknown
end
性能与优化
Erlang编译器可以对二进制创建和匹配提供许多优化。要查看优化输出,请设置bin_opt_info
编译器选项:
ERL_COMPILER_OPTIONS=bin_opt_info mix compile
要了解更多关于特定优化和性能考虑事项,请查看Erlang的效率指南处理二进制文件。
left = right (macro)
将右边的值与左边的模式相匹配。
^var (macro)
在匹配子句中访问已绑定的变量。也被称为pin操作符。
实例
Elixir允许变量通过静态单一赋值来反弹:
iex> x = 1
iex> x = x + 1
iex> x
2
但是,在某些情况下,与现有值匹配而不是重新绑定是有用的。这可以通过^
特殊形式,俗称针操作符:
iex> x = 1
iex> ^x = List.first([1])
iex> ^x = List.first([2])
** (MatchError) no match of right hand side value: 2
请注意,^x
始终是指x
匹配前的值。以下示例将匹配:
iex> x = 0
iex> {x, ^x} = {1, 0}
iex> x
1
_CALLER__ (macro)
将当前调用环境作为Macro.Env
结构。
在这个环境中,您可以访问文件名、行号、设置别名、函数和其他。
__DIR__ (macro)
以二进制形式返回当前文件目录的绝对路径。
虽然目录可以访问为Path.dirname(__ENV__.file)
,这个宏是一个方便的快捷方式。
__ENV__ (macro)
将当前环境信息作为Macro.Env
结构。
在该环境中,您可以访问当前文件名、行号、设置别名、当前函数和其他。
__MODULE__ (macro)
将当前模块名称作为原子返回,否则返回零。
虽然模块可以在中访问,但__ENV__/0
这个宏是一个方便的捷径。
_aliases__(args) (macro)
内部特殊形式,以保存别名信息。
它通常被编译成一个原子:
iex> quote do
...> Foo.Bar
...> end
{:__aliases__, [alias: false], [:Foo, :Bar]}
Elixir代表Foo.Bar
作为__aliases__
这样的呼叫可以由操作员清楚地识别:.
。例如:
iex> quote do
...> Foo.bar
...> end
{{:., [], [{:__aliases__, [alias: false], [:Foo]}, :bar]}, [], []}
每当表达式迭代器看到一个:。 作为元组键,可以确定它代表一个调用,并且列表中的第二个参数是一个原子。
另一方面,别名具有一些属性:
- 别名的头元素可以是在编译时必须扩展到原子的任何术语。
- 别名的尾部元素保证始终是原子。
3. 当别名的头元素是原子时:Elixir
,不会发生扩展。
__block __(args)查看源代码(宏)
块表达式的内部特殊形式。
无论何时我们在Elixir中有一段表达式,这都是特殊的形式。这种特殊的形式是私人的,不应该直接调用:
iex> quote do
...> 1
...> 2
...> 3
...> end
{:__block__, [], [1, 2, 3]}
别名(模块,opts)查看源代码(宏)
alias/2
用于设置别名,通常用于模块名称。
例子
alias/2
可以用来为任何模块设置别名:
defmodule Math do
alias MyKeyword, as: Keyword
end
在上面的例子中,我们设置了MyKeyword
别名as Keyword
。所以现在,任何引用Keyword
将被自动替换为MyKeyword
。
如果想访问原件Keyword
,可以通过访问Elixir
:
Keyword.values #=> uses MyKeyword.values
Elixir.Keyword.values #=> uses Keyword.values
请注意,alias
没有该as:
选项的调用会自动根据模块的最后一部分设置别名。例如:
alias Foo.Bar.Baz
是相同的:
alias Foo.Bar.Baz, as: Baz
我们也可以在一行中混用多个模块:
alias Foo.{Bar, Baz, Biz}
是相同的:
alias Foo.Bar
alias Foo.Baz
alias Foo.Biz
词汇范围
import/2
,require/2
并被alias/2
称为指令,都有词汇范围。这意味着您可以在特定函数内设置别名,并且不会影响整个范围。
警告
如果您为模块别名,而您没有使用别名,则Elixir将发出警告,暗示该别名未被使用。
如果别名是由宏自动生成的,Elixir不会发出任何警告,因为别名没有明确定义。
通过将:warn
选项明确设置为true
或,可以更改两种警告行为false
。
case(condition, clauses)View Source(macro)
根据给定的子句匹配给定的表达式。
示例
case thing do
{:selector, i, value} when is_integer(i) ->
value
value ->
value
end
在上面的例子中,我们匹配thing
每个子句“head”并执行与匹配的第一个子句对应的子句“body”。
如果没有子句匹配,则会引发错误。出于这个原因,可能需要添加_
总是匹配的最终catch-all子句(如)。
x = 10
case x do
0 ->
"This clause won't match"
_ ->
"This clause would match any value (x = #{x})"
end
#=> "This clause would match any value (x = 10)"
变量处理
请注意,绑定在子句“head”中的变量不会泄漏到外部上下文中:
case data do
{:ok, value} -> value
:error -> nil
end
value #=> unbound variable value
但是,可以从外部上下文访问子句“body”中显式绑定的变量:
value = 7
case lucky? do
false -> value = 13
true -> true
end
value #=> 7 or 13
在上面的例子中,值将取决于7
或13
取决于值lucky?
。如果在大小写value
之前没有先前的值,那么没有显式绑定值的子句将绑定变量nil
。
如果您想对现有变量进行模式匹配,则需要使用该^/1
运算符:
x = 1
case 10 do
^x -> "Won't match"
_ -> "Will match"
end
#=> "Will match"
cond(clauses)查看源代码(宏)
评估与评估为真值的第一个子句对应的表达式。
cond do
hd([1, 2, 3]) ->
"1 is considered as true"
end
#=> "1 is considered as true"
如果所有条件评估为nil
或,则会引发错误false
。出于这个原因,可能有必要添加最终总是truthy条件(任何非false
和非nil
),这将永远匹配。
例子
cond do
1 + 1 == 1 ->
"This will never match"
2 * 2 != 4 ->
"Nor this"
true ->
"This will"
end
#=> "This will"
fn [clauses] endView Source(macro)
定义一个匿名函数。
Examples
iex> add = fn a, b -> a + b end
iex> add.(1, 2)
3
for(args)
理解允许您从可枚举或比特串快速构建数据结构。
我们从一个例子开始:
iex> for n <- [1, 2, 3, 4], do: n * 2
[2, 4, 6, 8]
理解接受许多生成器和过滤器。可枚举的生成器使用以下定义<-
:
# A list generator:
iex> for n <- [1, 2, 3, 4], do: n * 2
[2, 4, 6, 8]
# A comprehension with two generators
iex> for x <- [1, 2], y <- [2, 3], do: x * y
[2, 3, 4, 6]
还可以给出过滤器:
# A comprehension with a generator and a filter
iex> for n <- [1, 2, 3, 4, 5, 6], rem(n, 2) == 0, do: n
[2, 4, 6]
注释生成器也可以用于过滤,因为它可以删除任何与左侧的模式不匹配的值<-
:
iex> users = [user: "john", admin: "meg", guest: "barbara"]
iex> for {type, name} when type != :guest <- users do
...> String.upcase(name)
...> end
["JOHN", "MEG"]
比特串生成器也被支持,并且在需要组织比特串流时非常有用:
iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
iex> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b}
[{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}]
理解内部的变量赋值,无论是在发生器,过滤器还是块内,都不会在理解之外反映出来。
Into
在上面的例子中,理解返回的结果总是一个列表。返回的结果可以通过传递一个:into
选项来配置, 只要它实现Collectable
协议,它就接受任何结构。
例如,我们可以使用bitstring生成器来:into
选择删除字符串中的所有空格:
iex> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>
"helloworld"
该IO
模块提供了流,这两个都是,Enumerable
并且 Collectable
这里是一个使用理解的upcase回声服务器:
for line <- IO.stream(:stdio, :line), into: IO.stream(:stdio, :line) do
String.upcase(line)
end
import(module, opts)
View Source(macro)
从其他模块导入函数和宏。
import/2
允许从其他模块轻松访问函数或宏而不使用限定名称。
例子
如果您正在使用来自给定模块的多个功能,则可以导入这些功能并将它们作为本地功能引用,例如:
iex> import List
iex> flatten([1, [2], 3])
[1, 2, 3]
选择
默认情况下,Elixir从给定模块中导入函数和宏,除了以下划线开头的函数和宏(通常是回调函数):
import List
开发人员可以通过唯一选项来筛选仅导入宏或函数:
import List, only: :functions
import List, only: :macros
或者,Elixir允许开发人员将成对的名称/元数据传递给:only
或者:except
作为对要导入(或不导入)的内容的细粒度控制:
import List, only: [flatten: 1]
import String, except: [split: 2]
请注意,调用except
先前声明的import/2
过滤器只会过滤以前导入的元素。例如:
import List, only: [flatten: 1, keyfind: 4]
import List, except: [flatten: 1]
在上述两个导入d调用之后,只会List.keyfind/4
导入。
下划线函数
默认情况下,_
不会导入以开头的函数。如果你真的想导入一个函数,_
你必须明确地将它包含在 :only
选择器中。
import File.Stream, only: [__build__: 3]
词汇范围
重要的是要注意这import/2
是词法。这意味着您可以在特定函数内导入特定的宏:
defmodule Math do
def some_function do
# 1) Disable "if/2" from Kernel
import Kernel, except: [if: 2]
# 2) Require the new "if/2" macro from MyMacros
import MyMacros
# 3) Use the new macro
if do_something, it_works
end
end
在上面的例子中,我们导入了宏MyMacros
,取代了if/2
我们自己在该特定函数中的原始实现。该模块中的所有其他功能仍然可以使用原来的功能。
警告
如果你导入一个模块,并且你没有使用这个模块中的任何导入的函数或宏,Elixir将发出警告,暗示导入没有被使用。
如果导入由宏自动生成,Elixir不会发出任何警告,因为导入没有明确定义。
通过将:warn
选项明确设置为true
或,可以更改两种警告行为false
。
模糊的函数/宏名称
如果两个模块A
和B
被导入并它们都包含一个foo
用的元数函数1
,误差只射出如果一个不明确的呼叫到foo/1
实际上是由; 也就是说,错误是懒散地发出的,而不是急切地发出。
quote(opts, block)View Source(macro)
获取任何表达式的表示。
示例
iex> quote do
...> sum(1, 2, 3)
...> end
{:sum, [], [1, 2, 3]}
说明
任何Elixir代码都可以使用Elixir数据结构来表示。Elixir宏的构建块是一个包含三个元素的元组,例如:
{:sum, [], [1, 2, 3]}
上面的元组表示一个函数调用来sum
传递1,2和3作为参数。元组元素是:
- 元组的第一个元素始终是同一表示中的原子或另一个元组。
2. 元组的第二个元素表示元数据。
3. 元组的第三个元素是函数调用的参数。第三个参数可能是一个原子,它通常是一个变量(或一个本地调用)。
选项
- ::unquote - 如果为false,则禁用取消引用。 当你在另一个报价单内有一个报价并想要控制报价能够引用时很有用。
- ::location- 当设置为:keep,保持当前行和文件不被引用。 阅读下面的Stacktrace信息部分以获取更多信息。
:generated
- 将给定的块标记为已生成,因此不会发出警告。目前它只适用于特殊形式(例如,您可以注释case
但不是if
)。
:context
- 设置分辨率上下文。
:bind_quoted
-将绑定传递到宏。无论何时给出绑定,unquote/1
自动禁用。
引用文字
除了上面描述的元组之外,Elixir还有一些文字在引用时会自行返回。他们是:
:sum #=> Atoms
1 #=> Integers
2.0 #=> Floats
[1, 2] #=> Lists
"strings" #=> Strings
{key, value} #=> Tuples with two elements
引用和宏
quote/2
通常与代码生成的宏一起使用。作为一个练习,让我们定义一个宏,它自己乘以一个数(平方)。请注意,没有理由定义诸如宏(实际上它会被视为不好的做法),但它足够简单,它使我们能够专注于引用和宏的重要方面:
defmodule Math do
defmacro squared(x) do
quote do
unquote(x) * unquote(x)
end
end
end
我们可以调用它为:
import Math
IO.puts "Got #{squared(5)}"
起初,这个例子中没有任何东西显示它是一个宏。但是,正在发生的事情是,在汇编时,squared(5)
变成了现实5 * 5
。该参数5
在生成的代码中是重复的,我们可以在实践中看到这种行为,尽管因为我们的宏实际上有一个错误:
import Math
my_number = fn ->
IO.puts "Returning 5"
5
end
IO.puts "Got #{squared(my_number.())}"
上面的例子将打印:
Returning 5
Returning 5
Got 25
请注意“返回5”是如何打印两次的,而不是一次。这是因为宏接收到一个表达式而不是一个值(这是我们在常规函数中所期望的)。这意味着:
squared(my_number.())
其实扩展到:
my_number.() * my_number.()
它调用了两次函数,解释了为什么我们获得了两次打印值!在大多数情况下,这实际上是意料之外的行为,这就是为什么在涉及宏时需要牢记的第一件事情就是不要多次引用相同的值。
让我们来修复我们的宏:
defmodule Math do
defmacro squared(x) do
quote do
x = unquote(x)
x * x
end
end
end
现在square(my_number.())
像以前一样调用将只打印一次值。
事实上,这种模式非常常见,大多数情况下您都希望使用该bind_quoted
选项quote/2
:
defmodule Math do
defmacro squared(x) do
quote bind_quoted: [x: x] do
x * x
end
end
end
:bind_quoted
将转换为与上例相同的代码。 :bind_quoted
可以在很多情况下使用,并被视为良好实践,不仅因为它帮助我们避免陷入常见错误,而且还因为它允许我们利用宏提供的其他工具,如下面某些部分讨论的未引用片段。
在我们完成这个简短介绍之前,你会注意到,尽管我们x
在报价中定义了一个变量:
quote do
x = unquote(x)
x * x
end
当我们调用:
import Math
squared(5)
x #=> ** (CompileError) undefined variable x or undefined function x/0
我们可以看到x
没有泄漏到用户上下文中。发生这种情况是因为Elixir宏是干净的,我们将在下一节详细讨论这个话题。
Hygiene in variables
考虑下面的例子:
defmodule Hygiene do
defmacro no_interference do
quote do
a = 1
end
end
end
require Hygiene
a = 10
Hygiene.no_interference
a #=> 10
在上面的示例中,a
即使宏显然将其设置为1 ,也会返回10,因为宏中定义的变量不会影响宏执行的上下文。如果要在调用者的上下文中设置或获取变量,可以在var!
宏的帮助下做到这一点:
defmodule NoHygiene do
defmacro interference do
quote do
var!(a) = 1
end
end
end
require NoHygiene
a = 10
NoHygiene.interference
a #=> 1
请注意,除非你明确地给它一个上下文,否则你甚至不能访问在同一个模块中定义的变量:
defmodule Hygiene do
defmacro write do
quote do
a = 1
end
end
defmacro read do
quote do
a
end
end
end
Hygiene.write
Hygiene.read
#=> ** (RuntimeError) undefined variable a or undefined function a/0
为此,您可以显式传递当前模块范围作为参数:
defmodule ContextHygiene do
defmacro write do
quote do
var!(a, ContextHygiene) = 1
end
end
defmacro read do
quote do
var!(a, ContextHygiene)
end
end
end
ContextHygiene.write
ContextHygiene.read
#=> 1
Hygiene in aliases
引用中的别名默认是卫生的。考虑下面的例子:
defmodule Hygiene do
alias Map, as: M
defmacro no_interference do
quote do
M.new
end
end
end
require Hygiene
Hygiene.no_interference #=> %{}
注意,尽管M
在上下文中别名不可用,但是上面的代码M
仍然可以扩展到Map
。
同样,即使我们在调用宏之前定义了一个具有相同名称的别名,它也不会影响宏的结果:
defmodule Hygiene do
alias Map, as: M
defmacro no_interference do
quote do
M.new
end
end
end
require Hygiene
alias SomethingElse, as: M
Hygiene.no_interference #=> %{}
在某些情况下,您想要访问调用程序中定义的别名或模块。为此,您可以使用alias!
宏:
defmodule Hygiene do
# This will expand to Elixir.Nested.hello
defmacro no_interference do
quote do
Nested.hello
end
end
# This will expand to Nested.hello for
# whatever is Nested in the caller
defmacro interference do
quote do
alias!(Nested).hello
end
end
end
defmodule Parent do
defmodule Nested do
def hello, do: "world"
end
require Hygiene
Hygiene.no_interference
#=> ** (UndefinedFunctionError) ...
Hygiene.interference
#=> "world"
end
Hygiene in imports
与别名类似,Elixir的进口卫生。考虑下面的代码:
defmodule Hygiene do
defmacrop get_length do
quote do
length([1, 2, 3])
end
end
def return_length do
import Kernel, except: [length: 1]
get_length
end
end
Hygiene.return_length #=> 3
注意即使 函数没有被导入也是如何Hygiene.return_length/0
返回的。实际上,即使 从另一个模块导入了一个具有相同名称和元素的函数,它也不会影响函数结果:3
Kernel.length/1
return_length/0
def return_length do
import String, only: [length: 1]
get_length
end
调用这个新的return_length/0
仍然会返回3
结果。
Elixir非常聪明,可以将分辨率推迟到最新的时刻。所以,如果您length([1, 2, 3])
在内部报价中打电话,但没有length/1
可用的功能,则会在呼叫者中进行扩展:
defmodule Lazy do
defmacrop get_length do
import Kernel, except: [length: 1]
quote do
length("hello")
end
end
def return_length do
import Kernel, except: [length: 1]
import String, only: [length: 1]
get_length
end
end
Lazy.return_length #=> 5
Stacktrace信息
当通过宏定义函数时,开发人员可以选择是否从调用者或报价中报告运行时错误。我们来看一个例子:
# adder.ex
defmodule Adder do
@doc "Defines a function that adds two numbers"
defmacro defadd do
quote location: :keep do
def add(a, b), do: a + b
end
end
end
# sample.ex
defmodule Sample do
import Adder
defadd
end
require Sample
Sample.add(:one, :two)
#=> ** (ArithmeticError) bad argument in arithmetic expression
#=> adder.ex:5: Sample.add/2
当使用location: :keep
和无效参数给定时 Sample.add/2
,堆栈跟踪信息将指向报价中的文件和行。如果没有location: :keep
,则将错误报告给defadd
被调用的地方。注意location: :keep
只影响报价中的定义。
绑定和取消引用片段
Elixir报价/取消报价机制提供了称为非报价分段的功能。取消引用片段提供了一种简便的方法来即时生成功能。考虑这个例子:
kv = [foo: 1, bar: 2]
Enum.each kv, fn {k, v} ->
def unquote(k)(), do: unquote(v)
end
在上面的例子中,我们已经生成了这些功能foo/0
并且是 bar/0
动态的。现在,设想一下,我们想将这个功能转换成一个宏:
defmacro defkv(kv) do
Enum.map kv, fn {k, v} ->
quote do
def unquote(k)(), do: unquote(v)
end
end
end
我们可以调用这个宏:
defkv [foo: 1, bar: 2]
但是,我们无法如下调用它:
kv = [foo: 1, bar: 2]
defkv kv
这是因为宏在编译时希望其参数成为关键字列表。由于在上面的例子中我们传递了变量的表示,所以kv
我们的代码失败了。
这实际上是开发宏时常见的错误。我们正在宏观中呈现出特定的形状。我们可以通过在引用表达式中取消引用变量来解决它:
defmacro defkv(kv) do
quote do
Enum.each unquote(kv), fn {k, v} ->
def unquote(k)(), do: unquote(v)
end
end
end
如果您尝试运行我们的新的宏,你会发现它甚至不会编译,抱怨变量k
和v
不存在。这是因为含糊不清:unquote(k)
既可以像以前一样是一个不引人注目的片段,也可以是一个普通的非引用片段unquote(kv)
。
解决这个问题的一个办法是在宏中禁用不引用,但是,这样做会使表示无法注入 kv
到树中。那是什么时候该:bind_quoted
选项来拯救(再次!)。通过使用:bind_quoted
,我们可以在仍然将所需变量注入树中的同时自动禁用不加注释:
defmacro defkv(kv) do
quote bind_quoted: [kv: kv] do
Enum.each kv, fn {k, v} ->
def unquote(k)(), do: unquote(v)
end
end
end
实际上,:bind_quoted
每当想要在报价中注入一个值时,建议使用该选项。
receive(args)View Source(macro)
检查是否有消息与当前进程邮箱中的给定子句匹配。
如果没有这样的消息,则当前进程挂起,直到消息到达或等待给定的超时值。
例子
receive do
{:selector, i, value} when is_integer(i) ->
value
value when is_atom(value) ->
value
_ ->
IO.puts :stderr, "Unexpected message received"
end
如果after
在给定的超时时间之后没有收到消息,则可以给出可选的子句,以毫秒为单位指定:
receive do
{:selector, i, value} when is_integer(i) ->
value
value when is_atom(value) ->
value
_ ->
IO.puts :stderr, "Unexpected message received"
after
5000 ->
IO.puts :stderr, "No message in 5 seconds"
end
该after
条款即使没有match子句指定。给定的超时值after
可以是任何表达式,以评估其中一个允许的值:
:infinity
-进程应无限期地等待匹配消息,这与不使用超时相同
0
- 如果邮箱中没有匹配的邮件,超时将立即发生
- 小于
4_294_967_295
(0xFFFFFFFF
以十六进制表示法)的正整数- 应该可以将超时值表示为无符号的32位整数。
变量处理
该receive/1
特殊形式处理变量完全一样的case/2
特殊的宏。有关更多信息,请查看文档case/2
。
require(module, opts) (macro)
需要一个模块才能使用它的宏。
实例
模块中的公共函数是全局可用的,但是为了使用宏,您需要通过要求在其中定义模块来选择。
假设您if/2
在模块中创建了自己的实现MyMacros
。如果你想调用它,你需要首先明确地要求MyMacros
:
defmodule Math do
require MyMacros
MyMacros.if do_something, it_works
end
试图调用未加载的宏将引发错误。
别名捷径
require/2
也as:
作为一个选项接受,所以它会自动设置一个别名。请检查alias/2
更多信息。
super(args) (macro)
覆盖它时调用覆盖函数Kernel.defoverridable/1
。
查看Kernel.defoverridable/1
了解更多信息和文档。
try(args) (macro)
评估给定的表达式并处理任何错误,退出或抛出可能发生的错误。
实例
try do
do_something_that_may_fail(some_arg)
rescue
ArgumentError ->
IO.puts "Invalid argument given"
catch
value ->
IO.puts "Caught #{inspect(value)}"
else
value ->
IO.puts "Success! The result was #{inspect(value)}"
after
IO.puts "This is printed regardless if it failed or succeed"
end
该rescue
子句用于处理异常,而catch
子句可用于捕获抛出的值和退出。该else
子句可用于基于表达式的结果控制流。catch
,rescue
和else
子句基于模式匹配工作(类似于case
特殊形式)。
请注意,内部调用try/1
不是尾递归的,因为VM需要保持堆栈跟踪,以防出现异常。
rescue
子句
除了依赖模式匹配之外,rescue
子句提供了一些便利,例如允许用名称拯救异常的异常。以下所有格式都是rescue
条款中的有效模式:
try do
UndefinedModule.undefined_function
rescue
UndefinedFunctionError -> nil
end
try do
UndefinedModule.undefined_function
rescue
[UndefinedFunctionError] -> nil
end
# rescue and bind to x
try do
UndefinedModule.undefined_function
rescue
x in [UndefinedFunctionError] -> nil
end
# rescue all and bind to x
try do
UndefinedModule.undefined_function
rescue
x -> nil
end
Erlang错误
Erlang错误在拯救时转化为Elixir的错误:
try do
:erlang.error(:badarg)
rescue
ArgumentError -> :ok
end
最常见的Erlang错误将被转化为他们的Elixir对手部分。那些不会被转化为ErlangError
:
try do
:erlang.error(:unknown)
rescue
ErlangError -> :ok
end
事实上,ErlangError
可以用来拯救任何不是适当的Elixir错误。例如,:badarg
在转换之前,它可以用来拯救更早的错误:
try do
:erlang.error(:badarg)
rescue
ErlangError -> :ok
end
捕捉抛出和退出
该catch
子句可用于捕获抛出的值和退出。
try do
exit(:shutdown)
catch
:exit, :shutdown ->
IO.puts "Exited with shutdown reason"
end
try do
throw(:sample)
catch
:throw, :sample ->
IO.puts ":sample was thrown"
end
该catch
条款还支持:error
一起:exit
和:throw
,在二郎,虽然它支持常用避免raise
/ rescue
控制机制。其中一个原因是,在捕获时:error
,错误不会自动转化为Elixir错误:
try do
:erlang.error(:badarg)
catch
:error, :badarg ->
:ok
end
请注意,有可能匹配捕获值以及这种值的种类:
try do
exit(:shutdown)
catch
kind, value when kind in [:exit, :throw] ->
IO.puts "Exited with or thrown value #{inspect(value)}"
end
after
子句
一个after
子句允许你定义清理逻辑,当代码的成功执行结束时以及发生错误时,这些清理逻辑都会被调用。请注意,进程将通常在收到退出信号时退出,导致其突然退出,因此after
不保证子句被执行。幸运的是,Elixir中的大多数资源(例如打开文件,ETS表,端口,套接字等)都会链接到或监视拥有的进程,并在进程退出时自动清理自己。
File.write!("tmp/story.txt", "Hello, World")
try do
do_something_with("tmp/story.txt")
after
File.rm("tmp/story.txt")
end
else
子句
else
子句允许尝试表达式的结果在以下模式上匹配:
x = 2
try do
1 / x
rescue
ArithmeticError ->
:infinity
else
y when y < 1 and y > -1 ->
:small
_ ->
:large
end
如果else
子句不存在,也不引发异常,表达式的结果将被返回:
x = 1
^x =
try do
1 / x
rescue
ArithmeticError ->
:infinity
end
但是,如果存在else
子句但表达式的结果与任何模式都不匹配,则会引发异常。这个例外不会被一个catch
或者rescue
同一个人发现try
:
x = 1
try do
try do
1 / x
rescue
# The TryClauseError can not be rescued here:
TryClauseError ->
:error_a
else
0 ->
:small
end
rescue
# The TryClauseError is rescued here:
TryClauseError ->
:error_b
end
同样,一个else
子句中的异常不会在同一个内部被捕获或获救try
:
try do
try do
nil
catch
# The exit(1) call below can not be caught here:
:exit, _ ->
:exit_a
else
_ ->
exit(1)
end
catch
# The exit is caught here:
:exit, _ ->
:exit_b
end
这意味着VM不再需要将堆栈跟踪保存在else
子句,所以当使用try
中的尾调用作为最后的调用。else
子句。同样的道理也适用于rescue
和catch
子句。
只有尝试表达式的结果才会下降到else
子句。如果try
最后在rescue
或catch
子句,它们的结果不会下降到else
*
try do
throw(:catch_this)
catch
:throw, :catch_this ->
:it_was_caught
else
# :it_was_caught will not fall down to this "else" clause.
other ->
{:else, other}
end
变量处理
因为里面的表达式try
可能由于异常而没有计算,在其中创建的任何变量try
无法从外部访问。例如:
try do
x = 1
do_something_that_may_fail(same_arg)
:ok
catch
_, _ -> :failed
end
x #=> unbound variable "x"
在上面的例子中,x
由于它是在try
子句中定义的,因此无法访问。解决此问题的常见做法是返回内部定义的变量try
:
x =
try do
x = 1
do_something_that_may_fail(same_arg)
x
catch
_, _ -> :failed
end
unquote(expr) (macro)
从宏中取消引用给定表达式。
实例
想象一下你有一个变量的情况value
,你想把它注入一些引用。第一次尝试将是:
value = 13
quote do
sum(1, value, 3)
end
然后返回:
{:sum, [], [1, {:value, [], quoted}, 3]}
这不是预期的结果。为此,我们使用以下引号:
iex> value = 13
iex> quote do
...> sum(1, unquote(value), 3)
...> end
{:sum, [], [1, 13, 3]}
unquote_splicing(expr) (macro)
没有引用扩展其参数的给定列表。类似于unquote/1
。
实例
iex> values = [2, 3, 4]
iex> quote do
...> sum(1, unquote_splicing(values), 5)
...> end
{:sum, [], [1, 2, 3, 4, 5]}
with(args) (macro)
用于组合匹配子句。
让我们从一个例子开始:
iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height),
...> do: {:ok, width * height}
{:ok, 150}
如果所有子句匹配,则do
块将被执行,并返回其结果。否则,链将被中止,并返回不匹配的值:
iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height),
...> do: {:ok, width * height}
:error
守卫也可以用于模式:
iex> users = %{"melany" => "guest", "bob" => :admin}
iex> with {:ok, role} when not is_binary(role) <- Map.fetch(users, "bob"),
...> do: {:ok, to_string(role)}
{:ok, "admin"}
和for/1
一样,里面的变量with/1
不会泄漏; 条款之间也可以插入“裸露的表达”:
iex> width = nil
iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> double_width = width * 2,
...> {:ok, height} <- Map.fetch(opts, :height),
...> do: {:ok, double_width * height}
{:ok, 300}
iex> width
nil
else
可以给出一个选项来修改with
在匹配失败的情况下返回的内容:
iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> else
...> :error ->
...> {:error, :wrong_data}
...> end
{:error, :wrong_data}
如果没有匹配else
条件,则引发WithClauseError
异常。
{args} (macro)
创建一个元组。
有关元组数据类型和操作元组的函数的更多信息可以在Tuple
模块中找到; 一些用于处理元组的函数也可以在Kernel
(如Kernel.elem/2
or Kernel.tuple_size/1
)中使用。
AST表示
只有两项元组在Elixir中被视为文字,并在引用时返回自己。因此,所有其他元组都在AST中表示为对:{}
特殊表单的调用。
iex> quote do
...> {1, 2}
...> end
{1, 2}
iex> quote do
...> {1, 2, 3}
...> end
{:{}, [], [1, 2, 3]}
本文档系腾讯云开发者社区成员共同维护,如有问题请联系 cloudcommunity@tencent.com