首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Kernel.SpecialForms

特殊表单是Elixir的基本构建块,因此开发人员不能重写它。

我们在这个模块中定义它们。有些形式是词汇(如alias/2case/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宏:

代码语言:javascript
复制
defmodule User do
  defstruct name: "john", age: 27
end

现在可以创建如下结构:

代码语言:javascript
复制
%User{}

在一个结构体下方,只是一个带有:__struct__指向User模块的键的映射:

代码语言:javascript
复制
%User{} == %{__struct__: User, name: "john", age: 27}

结构也验证给定的键是定义的结构的一部分。下面的例子将失败,因为没有键:full_nameUser结构:

代码语言:javascript
复制
%User{full_name: "john doe"}

还可以使用特定于结构的更新操作:

代码语言:javascript
复制
%User{user | age: 28}

上面的语法将保证给定的键在编译时有效,并且它将确保运行时给定的参数是一个结构,否则BadStructError将失败。

虽然结构是map,但默认情况下,结构不会实现任何为map实现的协议。查看Kernel.defprotocol/2更多关于结构如何与多态调度协议一起使用的信息。另请参阅Kernel.struct/2以及Kernel.struct!/2有关如何动态创建和更新结构的示例。

%{} (macro)

创建一个map。

查看Map模块以获取更多关于地图的信息,它们的语法以及访问和操作它们的方法。

AST表示

无论=>是使用关键字语法还是使用关键字语法,为了简单起见,映射中的键值对总是在内部表示为两元素元组的列表:

代码语言:javascript
复制
iex> quote do
...>   %{"a" => :b, c: :d}
...> end
{:%{}, [], [{"a", :b}, {:c, :d}]}

&(expr) (macro)

捕获或创建匿名函数。

俘获

捕获运算符最常用于从模块中捕获具有给定名称和模块的函数:

代码语言:javascript
复制
iex> fun = &Kernel.is_atom/1
iex> fun.(:atom)
true
iex> fun.("string")
false

在上面的例子中,我们捕获了Kernel.is_atom/1然后调用它。

捕获操作符还可以通过省略模块名来捕获本地函数(包括私有函数)和导入函数:

代码语言:javascript
复制
&local_function/1

匿名函数

捕获操作符也可用于部分应用函数,其中&1&2等等,可以用作值占位符。例如:

代码语言:javascript
复制
iex> double = &(&1 * 2)
iex> double.(2)
4

换句话说,&(&1 * 2)相当于fn x -> x * 2 end。使用本地函数的另一个示例:

代码语言:javascript
复制
iex> fun = &is_atom(&1)
iex> fun.(:atom)
true

&运算符可以用更复杂的表达式中使用:

代码语言:javascript
复制
iex> fun = &(&1 + &2 + &3)
iex> fun.(1, 2, 3)
6

以及列表和元组:

代码语言:javascript
复制
iex> fun = &{&1, &2}
iex> fun.(1, 2)
{1, 2}

iex> fun = &[&1 | &2]
iex> fun.(1, 2)
[1 | 2]

创建匿名函数的唯一限制是至少有一个占位符必须存在,即它必须至少包含&1,并且不支持该块表达式:

代码语言:javascript
复制
# No placeholder, fails to compile.
&(:foo)

# Block expression, fails to compile.
&(&1; &2)

left . right (macro)

定义远程调用、对匿名函数的调用或别名。

Elixir中的dot(.)可用于远程调用:

代码语言:javascript
复制
iex> String.downcase("FOO")
"foo"

在上面的这个例子中,我们使用了. 在String模块中调用downcase,传递“FOO”作为参数。

点也可用于调用匿名函数:

代码语言:javascript
复制
iex> (fn(n) -> n end).(7)
7

在这种情况下,左侧有一个功能。

我们还可以使用点创建别名:

代码语言:javascript
复制
iex> Hello.World
Hello.World

这一次,我们加入了两个别名,定义了最终的别名。Hello.World...

句法

右面.可以是以大写开头的单词,表示别名、以小写或下划线开头的单词、任何有效的语言运算符或以单引号或双引号包装的任何名称。这些都是有效的例子:

代码语言:javascript
复制
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"将被视为远程呼叫而不是别名。这种选择是在每次使用单引号或双引号时完成的,无论引用内容如何,​​我们都有远程调用。这个决定也反映在下面讨论的引用表达式中。

当点用来调用一个匿名函数时,只有一个操作数,但仍然使用后缀表示法编写:

代码语言:javascript
复制
iex> negate = fn(n) -> -n end
iex> negate.(7)
-7

引用的表达

.被使用时,引用的表达式可能有两种截然不同的形式。当右侧以小写字母(或下划线)开头时:

代码语言:javascript
复制
iex> quote do
...>   String.downcase("FOO")
...> end
{{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], ["FOO"]}

请注意,我们有一个内部元组,其中包含:.代表点的原子作为第一个元素:

代码语言:javascript
复制
{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}

该元组遵循Elixir中通用的引用表达式结构,名称作为第一个参数,一些关键字列表作为元数据作为第二个参数,而参数列表作为第三个参数。在这种情况下,参数是别名String和原子:downcase。远程调用中的第二个参数始终是一个原子,而不管在调用中使用的文字是什么:

代码语言:javascript
复制
iex> quote do
...>   String."downcase"("FOO")
...> end
{{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], ["FOO"]}

包含的元组:.被包装在另一个元组中,该元组实际表示函数调用,并且具有"FOO"作为参数。

在调用匿名函数的情况下,具有点特殊形式的内部元组只有一个参数,这反映了操作符是一元的事实:

代码语言:javascript
复制
iex> quote do
...>   negate.(0)
...> end
{{:., [], [{:negate, [], __MODULE__}]}, [], [0]}

当右侧是别名(即以大写字母开头)时,我们得到:

代码语言:javascript
复制
iex> quote do
...>   Hello.World
...> end
{:__aliases__, [alias: false], [:Hello, :World]}

我们在__aliases__/1特殊格式文档中详细介绍别名。

不引用

我们还可以使用unquote在带引号的表达式中生成远程调用:

代码语言:javascript
复制
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

代码语言:javascript
复制
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的两个不同的场合使用。它用于类型描述来指定变量,函数或类型本身的类型:

代码语言:javascript
复制
@type number :: integer | float
@spec add(number, number) :: number

它也可以用于位串以指定给定位段的类型:

代码语言:javascript
复制
<<int::integer-little, rest::bits>> = bits

阅读Typespec页面上的文档,<<>>/1并分别获取关于类型规范和位串的更多信息。

<<args>> (macro)

定义一个新的位串。

实例

代码语言:javascript
复制
iex> <<1, 2, 3>>
<<1, 2, 3>>

类型

位串由多个段组成,每个段都有一个类型。在位字符串中使用了9种类型:

  • integer
  • float
  • bitsbitstring的别名)
  • bitstring
  • binary
  • bytesbinary的别名)
  • utf8
  • utf16
  • utf32

当没有指定类型时,默认值是integer

代码语言:javascript
复制
iex> <<1, 2, 3>>
<<1, 2, 3>>

Elixir还默认接受该段为文字字符串或文字charlist,默认情况下该段扩展为整数:

代码语言:javascript
复制
iex> <<0, "foo">>
<<0, 102, 111, 111>>

变量或任何其他类型都需要显式标记:

代码语言:javascript
复制
iex> rest = "oo"
iex> <<102, rest>>
** (ArgumentError) argument error

我们可以通过显式地将其标记为binary*

代码语言:javascript
复制
iex> rest = "oo"
iex> <<102, rest::binary>>
"foo"

这些utf8utf16utf32类型是用于Unicode码点的。它们也可以应用于字符串和查询表:

代码语言:javascript
复制
iex> <<"foo"::utf16>>
<<0, 102, 0, 111, 0, 111>>
iex> <<"foo"::utf32>>
<<0, 0, 0, 102, 0, 0, 0, 111, 0, 0, 0, 111>>

备选方案

可以通过使用-作为隔膜。顺序是任意的,因此以下所有内容都是等价的:

代码语言:javascript
复制
<<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>>

单位和大小

匹配的长度等于unitsize长度重复段的数量)乘以(位数)倍unit

类型

默认单位

整数

1位

浮动

1位

二进制

8位

类型的大小稍微有点细微差别。整数的默认大小是8。

对于浮点数,它是64.对于浮点数,size * unit必须为32或64,分别对应于IEEE 754 binary32和binary64。

对于二进制文件,缺省值是二进制文件的大小。只有匹配中的最后一个二进制文件才能使用默认大小。其他所有的必须显式指定它们的大小,即使匹配是明确的。例如:

代码语言:javascript
复制
iex> <<name::binary-size(5), " the ", species::binary>> = <<"Frank the Walrus">>
"Frank the Walrus"
iex> {name, species}
{"Frank", "Walrus"}

未能指定非最后一次的大小会导致编译失败:

代码语言:javascript
复制
<<name::binary, " the ", species::binary>> = <<"Frank the Walrus">>
** (CompileError): a binary field without size is only allowed at the end of a binary pattern

快捷语法

在传递整数值时,还可以使用语法快捷方式指定大小和单位:

代码语言:javascript
复制
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

标志

整数可以是,signedunsigned,默认为unsigned

代码语言:javascript
复制
iex> <<int::integer>> = <<-100>>
<<156>>
iex> int
156
iex> <<int::integer-signed>> = <<-100>>
<<156>>
iex> int
-100

signedunsigned仅用于匹配二进制文件(见下文),仅用于整数。

代码语言:javascript
复制
iex> <<-100::signed, _rest::binary>> = <<-100, "foo">>
<<156, 102, 111, 111>>

字节序

Elixir对字节序有三个选项:biglittle,和native。默认值是big

代码语言:javascript
复制
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的一个强大特性,它有助于从二进制文件中提取信息以及模式匹配。

二进制匹配本身可以用于从二进制文件中提取信息:

代码语言:javascript
复制
iex> <<"Hello, ", place::binary>> = "Hello, World"
"Hello, World"
iex> place
"World"

或者作为模式匹配函数定义的一部分:

代码语言:javascript
复制
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编译器选项:

代码语言:javascript
复制
ERL_COMPILER_OPTIONS=bin_opt_info mix compile

要了解更多关于特定优化和性能考虑事项,请查看Erlang的效率指南处理二进制文件

left = right (macro)

将右边的值与左边的模式相匹配。

^var (macro)

在匹配子句中访问已绑定的变量。也被称为pin操作符。

实例

Elixir允许变量通过静态单一赋值来反弹:

代码语言:javascript
复制
iex> x = 1
iex> x = x + 1
iex> x
2

但是,在某些情况下,与现有值匹配而不是重新绑定是有用的。这可以通过^特殊形式,俗称针操作符:

代码语言:javascript
复制
iex> x = 1
iex> ^x = List.first([1])
iex> ^x = List.first([2])
** (MatchError) no match of right hand side value: 2

请注意,^x始终是指x匹配前的值。以下示例将匹配:

代码语言:javascript
复制
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)

内部特殊形式,以保存别名信息。

它通常被编译成一个原子:

代码语言:javascript
复制
iex> quote do
...>   Foo.Bar
...> end
{:__aliases__, [alias: false], [:Foo, :Bar]}

Elixir代表Foo.Bar作为__aliases__这样的呼叫可以由操作员清楚地识别:.。例如:

代码语言:javascript
复制
iex> quote do
...>   Foo.bar
...> end
{{:., [], [{:__aliases__, [alias: false], [:Foo]}, :bar]}, [], []}

每当表达式迭代器看到一个:。 作为元组键,可以确定它代表一个调用,并且列表中的第二个参数是一个原子。

另一方面,别名具有一些属性:

  1. 别名的头元素可以是在编译​​时必须扩展到原子的任何术语。
  1. 别名的尾部元素保证始终是原子。

3. 当别名的头元素是原子时:Elixir,不会发生扩展。

链接到这个宏

__block __(args)查看源代码(宏)

块表达式的内部特殊形式。

无论何时我们在Elixir中有一段表达式,这都是特殊的形式。这种特殊的形式是私人的,不应该直接调用:

代码语言:javascript
复制
iex> quote do
...>   1
...>   2
...>   3
...> end
{:__block__, [], [1, 2, 3]}

链接到这个宏

别名(模块,opts)查看源代码(宏)

alias/2 用于设置别名,通常用于模块名称。

例子

alias/2 可以用来为任何模块设置别名:

代码语言:javascript
复制
defmodule Math do
  alias MyKeyword, as: Keyword
end

在上面的例子中,我们设置了MyKeyword别名as Keyword。所以现在,任何引用Keyword将被自动替换为MyKeyword

如果想访问原件Keyword,可以通过访问Elixir

代码语言:javascript
复制
Keyword.values   #=> uses MyKeyword.values
Elixir.Keyword.values #=> uses Keyword.values

请注意,alias没有该as:选项的调用会自动根据模块的最后一部分设置别名。例如:

代码语言:javascript
复制
alias Foo.Bar.Baz

是相同的:

代码语言:javascript
复制
alias Foo.Bar.Baz, as: Baz

我们也可以在一行中混用多个模块:

代码语言:javascript
复制
alias Foo.{Bar, Baz, Biz}

是相同的:

代码语言:javascript
复制
alias Foo.Bar
alias Foo.Baz
alias Foo.Biz

词汇范围

import/2require/2并被alias/2称为指令,都有词汇范围。这意味着您可以在特定函数内设置别名,并且不会影响整个范围。

警告

如果您为模块别名,而您没有使用别名,则Elixir将发出警告,暗示该别名未被使用。

如果别名是由宏自动生成的,Elixir不会发出任何警告,因为别名没有明确定义。

通过将:warn选项明确设置为true或,可以更改两种警告行为false

case(condition, clauses)View Source(macro)

根据给定的子句匹配给定的表达式。

示例

代码语言:javascript
复制
case thing do
  {:selector, i, value} when is_integer(i) ->
    value
  value ->
    value
end

在上面的例子中,我们匹配thing每个子句“head”并执行与匹配的第一个子句对应的子句“body”。

如果没有子句匹配,则会引发错误。出于这个原因,可能需要添加_总是匹配的最终catch-all子句(如)。

代码语言:javascript
复制
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”中的变量不会泄漏到外部上下文中:

代码语言:javascript
复制
case data do
  {:ok, value} -> value
  :error -> nil
end

value #=> unbound variable value

但是,可以从外部上下文访问子句“body”中显式绑定的变量:

代码语言:javascript
复制
value = 7

case lucky? do
  false -> value = 13
  true  -> true
end

value #=> 7 or 13

在上面的例子中,值将取决于713取决于值lucky?。如果在大小写value之前没有先前的值,那么没有显式绑定值的子句将绑定变量nil

如果您想对现有变量进行模式匹配,则需要使用该^/1运算符:

代码语言:javascript
复制
x = 1

case 10 do
  ^x -> "Won't match"
  _  -> "Will match"
end
#=> "Will match"

链接到这个宏

cond(clauses)查看源代码(宏)

评估与评估为真值的第一个子句对应的表达式。

代码语言:javascript
复制
cond do
  hd([1, 2, 3]) ->
    "1 is considered as true"
end
#=> "1 is considered as true"

如果所有条件评估为nil或,则会引发错误false。出于这个原因,可能有必要添加最终总是truthy条件(任何非false和非nil),这将永远匹配。

例子

代码语言:javascript
复制
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

代码语言:javascript
复制
iex> add = fn a, b -> a + b end
iex> add.(1, 2)
3

Link to this macro

for(args)

理解允许您从可枚举或比特串快速构建数据结构。

我们从一个例子开始:

代码语言:javascript
复制
iex> for n <- [1, 2, 3, 4], do: n * 2
[2, 4, 6, 8]

理解接受许多生成器和过滤器。可枚举的生成器使用以下定义<-

代码语言:javascript
复制
# 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]

还可以给出过滤器:

代码语言:javascript
复制
# 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]

注释生成器也可以用于过滤,因为它可以删除任何与左侧的模式不匹配的值<-

代码语言:javascript
复制
iex> users = [user: "john", admin: "meg", guest: "barbara"]
iex> for {type, name} when type != :guest <- users do
...>   String.upcase(name)
...> end
["JOHN", "MEG"]

比特串生成器也被支持,并且在需要组织比特串流时非常有用:

代码语言:javascript
复制
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选择删除字符串中的所有空格:

代码语言:javascript
复制
iex> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>
"helloworld"

IO模块提供了流,这两个都是,Enumerable并且 Collectable这里是一个使用理解的upcase回声服务器:

代码语言:javascript
复制
for line <- IO.stream(:stdio, :line), into: IO.stream(:stdio, :line) do
  String.upcase(line)
end

import(module, opts)

View Source(macro)

从其他模块导入函数和宏。

import/2 允许从其他模块轻松访问函数或宏而不使用限定名称。

例子

如果您正在使用来自给定模块的多个功能,则可以导入这些功能并将它们作为本地功能引用,例如:

代码语言:javascript
复制
iex> import List
iex> flatten([1, [2], 3])
[1, 2, 3]

选择

默认情况下,Elixir从给定模块中导入函数和宏,除了以下划线开头的函数和宏(通常是回调函数):

代码语言:javascript
复制
import List

开发人员可以通过唯一选项来筛选仅导入宏或函数:

代码语言:javascript
复制
import List, only: :functions
import List, only: :macros

或者,Elixir允许开发人员将成对的名称/元数据传递给:only或者:except作为对要导入(或不导入)的内容的细粒度控制:

代码语言:javascript
复制
import List, only: [flatten: 1]
import String, except: [split: 2]

请注意,调用except先前声明的import/2 过滤器只会过滤以前导入的元素。例如:

代码语言:javascript
复制
import List, only: [flatten: 1, keyfind: 4]
import List, except: [flatten: 1]

在上述两个导入d调用之后,只会List.keyfind/4导入。

下划线函数

默认情况下,_不会导入以开头的函数。如果你真的想导入一个函数,_你必须明确地将它包含在 :only选择器中。

代码语言:javascript
复制
import File.Stream, only: [__build__: 3]

词汇范围

重要的是要注意这import/2是词法。这意味着您可以在特定函数内导入特定的宏:

代码语言:javascript
复制
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

模糊的函数/宏名称

如果两个模块AB被导入并它们都包含一个foo用的元数函数1,误差只射出如果一个不明确的呼叫到foo/1实际上是由; 也就是说,错误是懒散地发出的,而不是急切地发出。

quote(opts, block)View Source(macro)

获取任何表达式的表示。

示例

代码语言:javascript
复制
iex> quote do
...>   sum(1, 2, 3)
...> end
{:sum, [], [1, 2, 3]}

说明

任何Elixir代码都可以使用Elixir数据结构来表示。Elixir宏的构建块是一个包含三个元素的元组,例如:

代码语言:javascript
复制
{:sum, [], [1, 2, 3]}

上面的元组表示一个函数调用来sum传递1,2和3作为参数。元组元素是:

  1. 元组的第一个元素始终是同一表示中的原子或另一个元组。

2. 元组的第二个元素表示元数据。

3. 元组的第三个元素是函数调用的参数。第三个参数可能是一个原子,它通常是一个变量(或一个本地调用)。

选项

  • ::unquote - 如果为false,则禁用取消引用。 当你在另一个报价单内有一个报价并想要控制报价能够引用时很有用。
  • ::location- 当设置为:keep,保持当前行和文件不被引用。 阅读下面的Stacktrace信息部分以获取更多信息。
  • :generated - 将给定的块标记为已生成,因此不会发出警告。目前它只适用于特殊形式(例如,您可以注释case但不是if)。
  • :context - 设置分辨率上下文。
  • :bind_quoted-将绑定传递到宏。无论何时给出绑定,unquote/1自动禁用。

引用文字

除了上面描述的元组之外,Elixir还有一些文字在引用时会自行返回。他们是:

代码语言:javascript
复制
:sum         #=> Atoms
1            #=> Integers
2.0          #=> Floats
[1, 2]       #=> Lists
"strings"    #=> Strings
{key, value} #=> Tuples with two elements

引用和宏

quote/2通常与代码生成的宏一起使用。作为一个练习,让我们定义一个宏,它自己乘以一个数(平方)。请注意,没有理由定义诸如宏(实际上它会被视为不好的做法),但它足够简单,它使我们能够专注于引用和宏的重要方面:

代码语言:javascript
复制
defmodule Math do
  defmacro squared(x) do
    quote do
      unquote(x) * unquote(x)
    end
  end
end

我们可以调用它为:

代码语言:javascript
复制
import Math
IO.puts "Got #{squared(5)}"

起初,这个例子中没有任何东西显示它是一个宏。但是,正在发生的事情是,在汇编时,squared(5) 变成了现实5 * 5。该参数5在生成的代码中是重复的,我们可以在实践中看到这种行为,尽管因为我们的宏实际上有一个错误:

代码语言:javascript
复制
import Math
my_number = fn ->
  IO.puts "Returning 5"
  5
end
IO.puts "Got #{squared(my_number.())}"

上面的例子将打印:

代码语言:javascript
复制
Returning 5
Returning 5
Got 25

请注意“返回5”是如何打印两次的,而不是一次。这是因为宏接收到一个表达式而不是一个值(这是我们在常规函数中所期望的)。这意味着:

代码语言:javascript
复制
squared(my_number.())

其实扩展到:

代码语言:javascript
复制
my_number.() * my_number.()

它调用了两次函数,解释了为什么我们获得了两次打印值!在大多数情况下,这实际上是意料之外的行为,这就是为什么在涉及宏时需要牢记的第一件事情就是不要多次引用相同的值

让我们来修复我们的宏:

代码语言:javascript
复制
defmodule Math do
  defmacro squared(x) do
    quote do
      x = unquote(x)
      x * x
    end
  end
end

现在square(my_number.())像以前一样调用将只打印一次值。

事实上,这种模式非常常见,大多数情况下您都希望使用该bind_quoted选项quote/2

代码语言:javascript
复制
defmodule Math do
  defmacro squared(x) do
    quote bind_quoted: [x: x] do
      x * x
    end
  end
end

:bind_quoted将转换为与上例相同的代码。 :bind_quoted可以在很多情况下使用,并被视为良好实践,不仅因为它帮助我们避免陷入常见错误,而且还因为它允许我们利用宏提供的其他工具,如下面某些部分讨论的未引用片段。

在我们完成这个简短介绍之前,你会注意到,尽管我们x在报价中定义了一个变量:

代码语言:javascript
复制
quote do
  x = unquote(x)
  x * x
end

当我们调用:

代码语言:javascript
复制
import Math
squared(5)
x #=> ** (CompileError) undefined variable x or undefined function x/0

我们可以看到x没有泄漏到用户上下文中。发生这种情况是因为Elixir宏是干净的,我们将在下一节详细讨论这个话题。

Hygiene in variables

考虑下面的例子:

代码语言:javascript
复制
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!宏的帮助下做到这一点:

代码语言:javascript
复制
defmodule NoHygiene do
  defmacro interference do
    quote do
      var!(a) = 1
    end
  end
end

require NoHygiene

a = 10
NoHygiene.interference
a #=> 1

请注意,除非你明确地给它一个上下文,否则你甚至不能访问在同一个模块中定义的变量:

代码语言:javascript
复制
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

为此,您可以显式传递当前模块范围作为参数:

代码语言:javascript
复制
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

引用中的别名默认是卫生的。考虑下面的例子:

代码语言:javascript
复制
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

同样,即使我们在调用宏之前定义了一个具有相同名称的别名,它也不会影响宏的结果:

代码语言:javascript
复制
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!宏:

代码语言:javascript
复制
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的进口卫生。考虑下面的代码:

代码语言:javascript
复制
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返回的。实际上,即使 从另一个模块导入了一个具有相同名称和元素的函数,它也不会影响函数结果:3Kernel.length/1return_length/0

代码语言:javascript
复制
def return_length do
  import String, only: [length: 1]
  get_length
end

调用这个新的return_length/0仍然会返回3结果。

Elixir非常聪明,可以将分辨率推迟到最新的时刻。所以,如果您length([1, 2, 3])在内部报价中打电话,但没有length/1可用的功能,则会在呼叫者中进行扩展:

代码语言:javascript
复制
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信息

当通过宏定义函数时,开发人员可以选择是否从调用者或报价中报告运行时错误。我们来看一个例子:

代码语言:javascript
复制
# 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报价/取消报价机制提供了称为非报价分段的功能。取消引用片段提供了一种简便的方法来即时生成功能。考虑这个例子:

代码语言:javascript
复制
kv = [foo: 1, bar: 2]
Enum.each kv, fn {k, v} ->
  def unquote(k)(), do: unquote(v)
end

在上面的例子中,我们已经生成了这些功能foo/0并且是 bar/0动态的。现在,设想一下,我们想将这个功能转换成一个宏:

代码语言:javascript
复制
defmacro defkv(kv) do
  Enum.map kv, fn {k, v} ->
    quote do
      def unquote(k)(), do: unquote(v)
    end
  end
end

我们可以调用这个宏:

代码语言:javascript
复制
defkv [foo: 1, bar: 2]

但是,我们无法如下调用它:

代码语言:javascript
复制
kv = [foo: 1, bar: 2]
defkv kv

这是因为宏在编译时希望其参数成为关键字列表。由于在上面的例子中我们传递了变量的表示,所以kv我们的代码失败了。

这实际上是开发宏时常见的错误。我们正在宏观中呈现出特定的形状。我们可以通过在引用表达式中取消引用变量来解决它:

代码语言:javascript
复制
defmacro defkv(kv) do
  quote do
    Enum.each unquote(kv), fn {k, v} ->
      def unquote(k)(), do: unquote(v)
    end
  end
end

如果您尝试运行我们的新的宏,你会发现它甚至不会编译,抱怨变量kv 不存在。这是因为含糊不清:unquote(k) 既可以像以前一样是一个不引人注目的片段,也可以是一个普通的非引用片段unquote(kv)

解决这个问题的一个办法是在宏中禁用不引用,但是,这样做会使表示无法注入 kv到树中。那是什么时候该:bind_quoted 选项来拯救(再次!)。通过使用:bind_quoted,我们可以在仍然将所需变量注入树中的同时自动禁用不加注释:

代码语言:javascript
复制
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)

检查是否有消息与当前进程邮箱中的给定子句匹配。

如果没有这样的消息,则当前进程挂起,直到消息到达或等待给定的超时值。

例子

代码语言:javascript
复制
receive do
  {:selector, i, value} when is_integer(i) ->
    value
  value when is_atom(value) ->
    value
  _ ->
    IO.puts :stderr, "Unexpected message received"
end

如果after在给定的超时时间之后没有收到消息,则可以给出可选的子句,以毫秒为单位指定:

代码语言:javascript
复制
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_2950xFFFFFFFF以十六进制表示法)的正整数- 应该可以将超时值表示为无符号的32位整数。

变量处理

receive/1特殊形式处理变量完全一样的case/2特殊的宏。有关更多信息,请查看文档case/2

require(module, opts) (macro)

需要一个模块才能使用它的宏。

实例

模块中的公共函数是全局可用的,但是为了使用宏,您需要通过要求在其中定义模块来选择。

假设您if/2在模块中创建了自己的实现MyMacros。如果你想调用它,你需要首先明确地要求MyMacros

代码语言:javascript
复制
defmodule Math do
  require MyMacros
  MyMacros.if do_something, it_works
end

试图调用未加载的宏将引发错误。

别名捷径

require/2as:作为一个选项接受,所以它会自动设置一个别名。请检查alias/2更多信息。

super(args) (macro)

覆盖它时调用覆盖函数Kernel.defoverridable/1

查看Kernel.defoverridable/1了解更多信息和文档。

try(args) (macro)

评估给定的表达式并处理任何错误,退出或抛出可能发生的错误。

实例

代码语言:javascript
复制
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子句可用于基于表达式的结果控制流。catchrescueelse子句基于模式匹配工作(类似于case特殊形式)。

请注意,内部调用try/1不是尾递归的,因为VM需要保持堆栈跟踪,以防出现异常。

rescue 子句

除了依赖模式匹配之外,rescue子句提供了一些便利,例如允许用名称拯救异常的异常。以下所有格式都是rescue条款中的有效模式:

代码语言:javascript
复制
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的错误:

代码语言:javascript
复制
try do
  :erlang.error(:badarg)
rescue
  ArgumentError -> :ok
end

最常见的Erlang错误将被转化为他们的Elixir对手部分。那些不会被转化为ErlangError

代码语言:javascript
复制
try do
  :erlang.error(:unknown)
rescue
  ErlangError -> :ok
end

事实上,ErlangError可以用来拯救任何不是适当的Elixir错误。例如,:badarg在转换之前,它可以用来拯救更早的错误:

代码语言:javascript
复制
try do
  :erlang.error(:badarg)
rescue
  ErlangError -> :ok
end

捕捉抛出和退出

catch子句可用于捕获抛出的值和退出。

代码语言:javascript
复制
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错误:

代码语言:javascript
复制
try do
  :erlang.error(:badarg)
catch
  :error, :badarg ->
    :ok
end

请注意,有可能匹配捕获值以及这种值的种类

代码语言:javascript
复制
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表,端口,套接字等)都会链接到或监视拥有的进程,并在进程退出时自动清理自己。

代码语言:javascript
复制
File.write!("tmp/story.txt", "Hello, World")
try do
  do_something_with("tmp/story.txt")
after
  File.rm("tmp/story.txt")
end

else 子句

else 子句允许尝试表达式的结果在以下模式上匹配:

代码语言:javascript
复制
x = 2
try do
  1 / x
rescue
  ArithmeticError ->
    :infinity
else
  y when y < 1 and y > -1 ->
    :small
  _ ->
    :large
end

如果else子句不存在,也不引发异常,表达式的结果将被返回:

代码语言:javascript
复制
x = 1
^x =
  try do
    1 / x
  rescue
    ArithmeticError ->
      :infinity
  end

但是,如果存在else子句但表达式的结果与任何模式都不匹配,则会引发异常。这个例外不会被一个catch或者rescue同一个人发现try

代码语言:javascript
复制
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

代码语言:javascript
复制
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子句。同样的道理也适用于rescuecatch子句。

只有尝试表达式的结果才会下降到else子句。如果try最后在rescuecatch子句,它们的结果不会下降到else*

代码语言:javascript
复制
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无法从外部访问。例如:

代码语言:javascript
复制
try do
  x = 1
  do_something_that_may_fail(same_arg)
  :ok
catch
  _, _ -> :failed
end

x #=> unbound variable "x"

在上面的例子中,x由于它是在try子句中定义的,因此无法访问。解决此问题的常见做法是返回内部定义的变量try

代码语言:javascript
复制
x =
  try do
    x = 1
    do_something_that_may_fail(same_arg)
    x
  catch
    _, _ -> :failed
  end

unquote(expr) (macro)

从宏中取消引用给定表达式。

实例

想象一下你有一个变量的情况value,你想把它注入一些引用。第一次尝试将是:

代码语言:javascript
复制
value = 13
quote do
  sum(1, value, 3)
end

然后返回:

代码语言:javascript
复制
{:sum, [], [1, {:value, [], quoted}, 3]}

这不是预期的结果。为此,我们使用以下引号:

代码语言:javascript
复制
iex> value = 13
iex> quote do
...>   sum(1, unquote(value), 3)
...> end
{:sum, [], [1, 13, 3]}

unquote_splicing(expr) (macro)

没有引用扩展其参数的给定列表。类似于unquote/1

实例

代码语言:javascript
复制
iex> values = [2, 3, 4]
iex> quote do
...>   sum(1, unquote_splicing(values), 5)
...> end
{:sum, [], [1, 2, 3, 4, 5]}

with(args) (macro)

用于组合匹配子句。

让我们从一个例子开始:

代码语言:javascript
复制
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块将被执行,并返回其结果。否则,链将被中止,并返回不匹配的值:

代码语言:javascript
复制
iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...>      {:ok, height} <- Map.fetch(opts, :height),
...>      do: {:ok, width * height}
:error

守卫也可以用于模式:

代码语言:javascript
复制
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不会泄漏; 条款之间也可以插入“裸露的表达”:

代码语言:javascript
复制
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在匹配失败的情况下返回的内容:

代码语言:javascript
复制
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/2or Kernel.tuple_size/1)中使用。

AST表示

只有两项元组在Elixir中被视为文字,并在引用时返回自己。因此,所有其他元组都在AST中表示为对:{}特殊表单的调用。

代码语言:javascript
复制
iex> quote do
...>   {1, 2}
...> end
{1, 2}

iex> quote do
...>   {1, 2, 3}
...> end
{:{}, [], [1, 2, 3]}

扫码关注腾讯云开发者

领取腾讯云代金券