查看源代码 Kernel.SpecialForms (Elixir v1.16.2)
特殊形式是 Elixir 的基本构建块,因此无法被开发者覆盖。
The Kernel.SpecialForms
模块仅包含可以在 Elixir 代码中的任何地方调用的宏,无需使用 Kernel.SpecialForms.
前缀。这是因为它们都已自动导入,与 Kernel
模块中的函数和宏一样。
这些构建块在此模块中定义。其中一些特殊形式是词法形式(如 alias/2
和 case/2
)。宏 {}/1
和 <<>>/1
也是用于分别定义元组和二进制数据结构的特殊形式。
此模块还记录了返回有关 Elixir 编译环境信息的宏,例如(__ENV__/0
、__MODULE__/0
、__DIR__/0
、__STACKTRACE__/0
和 __CALLER__/0
)。
此外,它还记录了两种特殊形式,__block__/1
和 __aliases__/1
,它们并非开发者直接调用的,但它们出现在引用的内容中,因为它们是 Elixir 结构中的必需部分。
摘要
函数
匹配或构建结构。
创建映射。
捕获运算符。捕获或创建匿名函数。
点运算符。定义远程调用、对匿名函数的调用或别名。
用于保存别名信息的内部特殊形式。
用于块表达式的内部特殊形式。
将当前调用环境返回为 Macro.Env
结构。
将当前文件的目录的绝对路径返回为二进制。
将当前模块名称返回为原子,否则返回 nil
。
返回当前处理的异常的堆栈跟踪。
类型运算符。由类型和位串用于指定类型。
定义新的位串。
匹配运算符。将右侧的值与左侧的模式进行匹配。
alias/2
用于设置别名,通常与模块名称一起使用。
将给定的表达式与给定的子句进行匹配。
评估第一个计算结果为真值的子句对应的表达式。
定义匿名函数。
推导允许您快速从可枚举对象或位串中构建数据结构。
从其他模块导入函数和宏。
获取任何表达式的表示形式。
检查当前进程邮箱中是否有与给定子句匹配的消息。
需要一个模块才能使用其宏。
在使用 Kernel.defoverridable/1
覆盖函数时,调用覆盖的函数。
评估给定的表达式,并处理可能发生的任何错误、退出或抛出。
在引用的表达式内取消引用给定的表达式。
取消引用给定的列表,扩展其参数。
组合匹配子句。
固定运算符。访问匹配子句中已绑定的变量。
创建元组。
函数
匹配或构建结构。
结构是一个标记的映射,允许开发人员为键提供默认值,用于多态分派和编译时断言的标记。
结构通常使用 Kernel.defstruct/1
宏定义
defmodule User do
defstruct name: "john", age: 27
end
现在可以像下面这样创建结构
%User{}
结构的本质是一个映射,带有指向 User
模块的 :__struct__
键
%User{} == %{__struct__: User, name: "john", age: 27}
在构建结构时,可以提供结构字段
%User{age: 31}
#=> %{__struct__: User, name: "john", age: 31}
也可以通过模式匹配来提取值
%User{age: age} = user
还提供了一种针对结构的更新操作
%User{user | age: 28}
结构的优点是它们会验证给定的键是否属于定义的结构。以下示例将失败,因为 User
结构中没有 :full_name
键
%User{full_name: "john doe"}
上面的语法将保证给定的键在编译时有效,并在运行时保证给定的参数是一个结构,否则将使用 BadStructError
失败。
虽然结构是映射,但默认情况下,结构不会实现为映射实现的任何协议。有关如何在结构中使用协议进行多态分派的更多信息,请查看 Kernel.defprotocol/2
。另请参阅 Kernel.struct/2
和 Kernel.struct!/2
,了解有关如何动态创建和更新结构的示例。
对结构名称进行模式匹配
除了允许对结构字段进行模式匹配,例如
%User{age: age} = user
结构还允许对结构名称进行模式匹配
%struct_name{} = user
struct_name #=> User
当您想要检查某个东西是否是结构,但对它的名称不感兴趣时,也可以将结构名称分配给 _
%_{} = user
创建映射。
有关映射、其语法以及访问和操作映射的方法的更多信息,请参阅 Map
模块。
AST 表示
无论使用 =>
还是关键字语法,映射中的键值对在内部始终表示为由两个元素组成的元组列表,以方便起见
iex> quote do
...> %{"a" => :b, c: :d}
...> end
{:%{}, [], [{"a", :b}, {:c, :d}]}
捕获运算符。捕获或创建匿名函数。
捕获
捕获运算符最常用于从模块中捕获具有给定名称和元数的函数
iex> fun = &Kernel.is_atom/1
iex> fun.(:atom)
true
iex> fun.("string")
false
在上面的示例中,我们捕获了 Kernel.is_atom/1
作为匿名函数,然后调用它。
捕获运算符还可以用于捕获本地函数(包括私有函数)和导入的函数,方法是省略模块名称
&local_function/1
另请参阅 Function.capture/3
。
匿名函数
捕获运算符还可以用于部分应用函数,其中 &1
、&2
等可以用作值占位符。例如
iex> double = &(&1 * 2)
iex> double.(2)
4
换句话说,&(&1 * 2)
等效于 fn x -> x * 2 end
。
我们可以使用占位符部分应用远程函数
iex> take_five = &Enum.take(&1, 5)
iex> take_five.(1..10)
[1, 2, 3, 4, 5]
使用导入或本地函数时的另一个示例
iex> first_elem = &elem(&1, 0)
iex> first_elem.({0, 1})
0
可以使用更复杂的表达式使用 &
运算符
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, 3])
[1, 2, 3]
创建匿名函数时的唯一限制是必须存在至少一个占位符(即必须至少包含 &1
),并且不支持块表达式
# No placeholder, fails to compile.
&(:foo)
# Block expression, fails to compile.
&(&1; &2)
点运算符。定义远程调用、对匿名函数的调用或别名。
Elixir 中的点 (.
) 可用于远程调用
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."+"(1, 2)
3
用单引号或双引号括起来的函数名始终是远程调用。因此,Kernel."Foo"
将尝试调用函数 "Foo",而不是返回别名 Kernel.Foo
。这是有意为之,因为模块名称比函数名称更严格。
当点用于调用匿名函数时,只有一个操作数,但它仍然使用后缀表示法编写
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
...> 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]}
用于保存别名信息的内部特殊形式。
它通常编译成一个原子。
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]}, [], []}
每当表达式迭代器看到 :.
作为元组键时,它都可以确定它代表一个调用,并且列表中的第二个参数是一个原子。
另一方面,别名具有一些属性。
别名的头部元素可以是任何在编译时必须扩展为原子的项。
别名的尾部元素保证始终是原子。
当别名的头部元素是原子
:Elixir
时,不会发生扩展。
用于块表达式的内部特殊形式。
这是在 Elixir 中具有表达式块时使用的特殊形式。这种特殊形式是私有的,不应该直接调用。
iex> quote do
...> 1
...> 2
...> 3
...> end
{:__block__, [], [1, 2, 3]}
将当前调用环境返回为 Macro.Env
结构。
在环境中,您可以访问文件名、行号、设置别名、函数等。
将当前文件的目录的绝对路径返回为二进制。
虽然可以使用 Path.dirname(__ENV__.file)
访问目录,但此宏是一个方便的快捷方式。
将当前环境信息返回为 Macro.Env
结构。
在环境中,您可以访问当前文件名、行号、设置别名、当前函数等。
将当前模块名称返回为原子,否则返回 nil
。
虽然可以在 __ENV__/0
中访问模块,但此宏是一个方便的快捷方式。
返回当前处理的异常的堆栈跟踪。
它只在 try/1
表达式的 catch
和 rescue
子句中可用。
要检索当前进程的堆栈跟踪,请使用 Process.info(self(), :current_stacktrace)
。
类型运算符。由类型和位串用于指定类型。
此运算符在 Elixir 中的两个不同场合使用。它在类型规范中用于指定变量、函数或类型本身的类型。
@type number :: integer | float
@spec add(number, number) :: number
它也可以在位字符串中使用,以指定给定位段的类型。
<<int::integer-little, rest::bits>> = bits
定义新的位串。
例子
iex> <<1, 2, 3>>
<<1, 2, 3>>
类型
位字符串由多个段组成,每个段都有一个类型。位字符串中使用 9 种类型。
整数
浮点数
bits
(bitstring
的别名)位字符串
二进制
bytes
(binary
的别名)utf8
utf16
utf32
当没有指定类型时,默认值为 integer
。
iex> <<1, 2, 3>>
<<1, 2, 3>>
Elixir 默认情况下也接受段为扩展为整数的字面字符串。
iex> <<0, "foo">>
<<0, 102, 111, 111>>
您可以使用 utf8
(默认)、utf16
和 utf32
中的一种来控制字符串的编码方式。
iex> <<"foo"::utf16>>
<<0, 102, 0, 111, 0, 111>>
等效于写入
iex> <<?f::utf16, ?o::utf16, ?o::utf16>>
<<0, 102, 0, 111, 0, 111>>
在运行时,二进制需要显式标记为 binary
。
iex> rest = "oo"
iex> <<102, rest::binary>>
"foo"
否则,在构造二进制时会得到 ArgumentError
。
rest = "oo"
<<102, rest>>
** (ArgumentError) argument error
选项
可以使用 -
作为分隔符给出许多选项。顺序是任意的,因此以下都是等效的。
<<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
必须为 16、32 或 64,分别对应于 IEEE 754 binary16、binary32 和 binary64。
对于二进制,默认值为二进制的大小。只有匹配中的最后一个二进制可以使用默认尺寸。所有其他二进制都必须显式指定其尺寸,即使匹配是明确的。例如
iex> <<name::binary-size(5), " the ", species::binary>> = <<"Frank the Walrus">>
"Frank the Walrus"
iex> {name, species}
{"Frank", "Walrus"}
尺寸可以是变量或任何有效的保护表达式。
iex> name_size = 5
iex> <<name::binary-size(^name_size), " the ", species::binary>> = <<"Frank the Walrus">>
iex> {name, species}
{"Frank", "Walrus"}
尺寸可以访问二进制本身中定义的先前变量。
iex> <<name_size::size(8), name::binary-size(name_size), " the ", species::binary>> = <<5, "Frank the Walrus">>
iex> {name, species}
{"Frank", "Walrus"}
但是,它不能访问在二进制/位字符串之外的匹配中定义的变量。
{name_size, <<name::binary-size(name_size), _rest::binary>>} = {5, <<"Frank the Walrus">>}
** (CompileError): undefined variable "name_size" in bitstring segment
未为非最后一个二进制指定尺寸会导致编译失败。
<<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
此语法反映了有效尺寸是通过将尺寸乘以单位得到的这一事实。
修饰符
某些类型具有相关的修饰符,以消除字节表示中的歧义。
修饰符 | 相关类型 |
---|---|
带符号 | 整数 |
unsigned (默认) | 整数 |
小端 | integer 、float 、utf16 、utf32 |
big (默认) | integer 、float 、utf16 、utf32 |
本机 | integer 、float 、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 ImageType do
@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 效率指南的“构建和匹配二进制”一章。
匹配运算符。将右侧的值与左侧的模式进行匹配。
alias/2
用于设置别名,通常与模块名称一起使用。
例子
alias/2
可用于为任何模块设置别名。
defmodule Math do
alias MyKeyword, as: Keyword
end
在上面的示例中,我们已将 MyKeyword
设置为别名为 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 File.read(file) do
{:ok, contents} when is_binary(contents) ->
String.split(contents, "\n")
{:error, _reason} ->
Logger.warning "could not find #{file}, assuming empty..."
[]
end
在上面的示例中,我们将 File.read/1
的结果与每个子句的“头部”进行匹配,并执行与第一个匹配的子句对应的子句的“主体”。
如果没有任何子句匹配,就会抛出错误。因此,可能需要添加一个最终的万能子句(例如 _
),它将始终匹配。
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)"
变量处理
请注意,在子句中绑定的变量不会泄漏到外部上下文。
case data do
{:ok, value} -> value
:error -> nil
end
value
#=> unbound variable value
外部上下文中的变量也不能被覆盖。
value = 7
case lucky? do
false -> value = 13
true -> true
end
value
#=> 7
在上面的例子中,value
将始终为 7
,无论 lucky?
的值如何。子句中绑定的变量 value
和外部上下文绑定变量 value
是两个完全独立的变量。
如果要根据现有变量进行模式匹配,需要使用 ^/1
运算符。
x = 1
case 10 do
^x -> "Won't match"
_ -> "Will match"
end
#=> "Will match"
使用守卫匹配多个值
虽然在一个子句中无法匹配多个模式,但可以使用守卫匹配多个值。
case data do
value when value in [:one, :two] ->
"#{value} has been matched"
:three ->
"three has been matched"
end
评估第一个计算结果为真值的子句对应的表达式。
cond do
hd([1, 2, 3]) ->
"1 is considered as true"
end
#=> "1 is considered as true"
如果所有条件都计算为 nil
或 false
,则会抛出错误。因此,可能需要添加一个最终始终为真的条件(任何非 false
和非 nil
的条件),它将始终匹配。
例子
cond do
1 + 1 == 1 ->
"This will never match"
2 * 2 != 4 ->
"Nor this"
true ->
"This will"
end
#=> "This will"
定义匿名函数。
更多信息请参考 Function
。
例子
iex> add = fn a, b -> a + b end
iex> add.(1, 2)
3
匿名函数也可以有多个子句。所有子句都应该期望相同数量的参数。
iex> negate = fn
...> true -> false
...> false -> true
...> end
iex> negate.(false)
true
推导允许您快速从可枚举对象或位串中构建数据结构。
让我们从一个例子开始。
iex> for n <- [1, 2, 3, 4], do: n * 2
[2, 4, 6, 8]
一个推导式可以接受多个生成器和过滤器。for
使用 <-
运算符从右边的可枚举对象中提取值,并将它们与左边的模式进行匹配。我们称它们为生成器。
# 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]
过滤器必须计算为真值(除 nil
和 false
之外的所有值)。如果过滤器为假,则丢弃当前值。
生成器也可以用于过滤,因为它会删除所有不匹配 <-
左边模式的值。
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}]
推导式中的变量赋值,无论是在生成器、过滤器还是代码块中,都不会反映到推导式之外。
过滤器中的变量赋值仍然必须返回一个真值,否则值将被丢弃。让我们来看一个例子。假设你有一个关键字列表,其中键是一个编程语言,而值是它的直接父语言。然后,让我们尝试计算每种语言的祖父母。你可以尝试以下操作
iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
iex> for {language, parent} <- languages, grandparent = languages[parent], do: {language, grandparent}
[elixir: :prolog]
由于 Erlang 和 Prolog 的祖父母为 nil,因此这些值被过滤掉了。如果你不想要这种行为,一个简单的选项是将过滤器移动到 do 代码块中
iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
iex> for {language, parent} <- languages do
...> grandparent = languages[parent]
...> {language, grandparent}
...> end
[elixir: :prolog, erlang: nil, prolog: nil]
但是,这种选项并不总是可用,因为你可能还有其他过滤器。另一种选择是将过滤器转换为生成器,方法是在 =
右侧用列表将其括起来
iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
iex> for {language, parent} <- languages, grandparent <- [languages[parent]], do: {language, grandparent}
[elixir: :prolog, erlang: nil, prolog: nil]
:into
和 :uniq
选项
在上面的例子中,推导式返回的结果始终是一个列表。返回的结果可以通过传递一个 :into
选项来配置,该选项接受任何结构,只要它实现了 Collectable
协议。
例如,我们可以使用 :into
选项与位串生成器一起使用,以轻松地从字符串中删除所有空格。
iex> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>
"helloworld"
IO
模块提供流,它们既是 Enumerable
又是 Collectable
,这是一个使用推导式的全大写回声服务器。
for line <- IO.stream(), into: IO.stream() do
String.upcase(line)
end
类似地,uniq: true
也可以传递给推导式,以确保仅当结果之前没有返回时,才将其添加到集合中。例如
iex> for x <- [1, 1, 2, 3], uniq: true, do: x * 2
[2, 4, 6]
iex> for <<x <- "abcabc">>, uniq: true, into: "", do: <<x - 32>>
"ABC"
:reduce
选项
虽然 :into
选项允许我们将推导式的行为定制为给定的数据类型,例如将所有值放入一个映射或一个二进制文件中,但这并不总是足够的。
例如,假设你有一个包含字母的二进制文件,你想要计算每个小写字母出现的次数,忽略所有大写字母。例如,对于字符串 "AbCabCABc"
,我们想要返回映射 %{"a" => 1, "b" => 2, "c" => 1}
。
如果要使用 :into
,我们需要一种数据类型来计算其包含的每个元素的频率。虽然 Elixir 中没有这样的数据类型,但你可以自己实现一个。
一个更简单的选择是使用推导式对字母进行映射和过滤,然后调用 Enum.reduce/3
来构建一个映射,例如
iex> letters = for <<x <- "AbCabCABc">>, x in ?a..?z, do: <<x>>
iex> Enum.reduce(letters, %{}, fn x, acc -> Map.update(acc, x, 1, & &1 + 1) end)
%{"a" => 1, "b" => 2, "c" => 1}
虽然上面的方法很简单,但它有一个缺点,即至少要遍历数据两次。如果你期望输入很长的字符串,这可能非常昂贵。
幸运的是,推导式也支持 :reduce
选项,这将使我们能够将上面的两个步骤融合到一个步骤中。
iex> for <<x <- "AbCabCABc">>, x in ?a..?z, reduce: %{} do
...> acc -> Map.update(acc, <<x>>, 1, & &1 + 1)
...> end
%{"a" => 1, "b" => 2, "c" => 1}
当给出 :reduce
键时,它的值用作初始累加器,并且 do
代码块必须更改为使用 ->
子句,其中 ->
的左侧接收前一次迭代的累加值,而右侧的表达式必须返回新的累加值。一旦没有更多元素,就会返回最终的累加值。如果根本没有元素,则返回初始累加值。
从其他模块导入函数和宏。
import/2
允许一个人轻松地访问其他模块中的函数或宏,而无需使用限定名称。
例子
如果你使用来自给定模块的几个函数,你可以导入这些函数并将它们引用为本地函数,例如
iex> import List
iex> flatten([1, [2], 3])
[1, 2, 3]
选择器
默认情况下,Elixir 从给定模块中导入函数和宏,但以下划线开头的函数除外(这些函数通常是回调函数)。
import List
开发人员可以通过 :only
选项过滤,仅导入函数、宏或 sigil(可以是函数或宏)。
import List, only: :functions
import List, only: :macros
import Kernel, only: :sigils
或者,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]
在上面的两个导入调用之后,只有 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
被导入,并且它们都包含一个元数为 1
的 foo
函数,则只有在实际上对 foo/1
进行不明确的调用时才会发出错误;也就是说,错误是延迟发出的,而不是立即发出的。
获取任何表达式的表示形式。
例子
iex> quote do
...> sum(1, 2, 3)
...> end
{:sum, [], [1, 2, 3]}
Elixir 的 AST(抽象语法树)
任何 Elixir 代码都可以使用 Elixir 数据结构表示。Elixir 宏的构建块是一个包含三个元素的元组,例如
{:sum, [], [1, 2, 3]}
上面的元组表示对 sum
函数的调用,将 1、2 和 3 作为参数传递。元组元素是
元组的第一个元素始终是原子或同一个表示形式中的另一个元组。
元组的第二个元素表示 元数据。
元组的第三个元素是函数调用的参数。第三个参数可以是原子,它通常是一个变量(或一个本地调用)。
除了上面描述的元组之外,Elixir 还有一些文字也是其 AST 的一部分。这些文字在引用时返回自身。它们是
:sum #=> Atoms
1 #=> Integers
2.0 #=> Floats
[1, 2] #=> Lists
"strings" #=> Strings
{key, value} #=> Tuples with two elements
任何其他值,例如地图或四元素元组,必须在被引入 AST 之前进行转义 (Macro.escape/1
)。
选项
:bind_quoted
- 传递绑定到宏。只要给出绑定,unquote/1
就会自动禁用。:context
- 设置解析上下文。:generated
- 将给定的代码块标记为生成的,因此不会发出警告。它也有助于避免 dialyzer 在宏生成未使用的子句时报告错误。:file
- 设置引用的表达式为给定的文件。:line
- 设置引用的表达式为给定的行。:location
- 当设置为:keep
时,保留当前行和文件,来自引用。有关更多信息,请阅读下面的“堆栈跟踪信息”部分。:unquote
- 当false
时,禁用反引用。这意味着任何unquote
调用都将保留在 AST 中,而不是被unquote
参数替换。例如iex> quote do ...> unquote("hello") ...> end "hello" iex> quote unquote: false do ...> unquote("hello") ...> end {:unquote, [], ["hello"]}
引用和宏
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
现在像以前一样调用 squared(my_number.())
将只打印一次值。
实际上,这种模式非常常见,以至于大多数情况下你会想要使用 quote/2
的 bind_quoted
选项。
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"
我们可以看到 x
没有泄漏到用户上下文。这是因为 Elixir 宏是卫生的,我们将在接下来的部分中详细讨论这个主题。
变量中的卫生
考虑以下示例
defmodule Hygiene do
defmacro no_interference do
quote do
a = 1
end
end
end
require Hygiene
a = 10
Hygiene.no_interference()
a
#=> 10
在上面的示例中,即使宏似乎将其设置为 1,a
返回 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
require Hygiene
Hygiene.write()
Hygiene.read()
** (CompileError) undefined variable "a" (context Hygiene)
为此,你可以显式地将当前模块范围作为参数传递。
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
require Hygiene
ContextHygiene.write()
ContextHygiene.read()
#=> 1
变量的上下文由元组的第三个元素标识。默认上下文是 nil
,quote
为其中的所有变量分配另一个上下文。
quote(do: var)
#=> {:var, [], Elixir}
对于宏返回的变量,元数据中可能还有一个 :counter
键,用于进一步细化其上下文并保证宏调用之间的隔离,如前面的示例所示。
别名中的卫生
引用中的别名默认情况下是卫生的。考虑以下示例
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
导入中的卫生
与别名类似,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
请注意,即使没有导入 Kernel.length/1
函数,Hygiene.return_length/0
仍然返回 3
。事实上,即使 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
堆栈跟踪信息
在通过宏定义函数时,开发人员可以选择运行时错误是报告来自调用者还是从引用内部报告。让我们看一个例子
# 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
仅影响引用内部的定义。
location: :keep
和反引用如果函数定义也反引用了某些宏参数,请不要使用
location: :keep
。如果你这样做,Elixir 将存储当前位置的文件定义,但反引用的参数可能包含宏调用者的行信息,从而导致错误的堆栈跟踪。
绑定和反引用片段
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 do
{:selector, number, name} when is_integer(number) ->
name
name when is_atom(name) ->
name
_ ->
IO.puts(:stderr, "Unexpected message received")
end
如果在给定的超时时间段内(以毫秒为单位)没有收到消息,则可以提供一个可选的 after
子句。
receive do
{:selector, number, name} when is_integer(number) ->
name
name when is_atom(name) ->
name
_ ->
IO.puts(:stderr, "Unexpected message received")
after
5000 ->
IO.puts(:stderr, "No message in 5 seconds")
end
即使没有匹配子句,也可以指定 after
子句。传递给 after
的超时值可以是任何评估为以下允许值的表达式。
:infinity
- 进程应该无限期地等待匹配消息,这与不使用 after 子句相同。0
- 如果邮箱中没有匹配消息,则超时将立即发生。小于或等于
4_294_967_295
(十六进制表示法为0xFFFFFFFF
)的正整数 - 应该能够将超时值表示为无符号 32 位整数。
变量处理
receive/1
特殊形式处理变量的方式与 case/2
特殊宏完全相同。有关更多信息,请查看 case/2
的文档。
需要一个模块才能使用其宏。
示例
模块中的公共函数是全局可用的,但为了使用宏,你需要通过要求定义它们的模块来选择加入。
假设你在模块 MyMacros
中创建了自己的 if/2
实现。如果你想调用它,你需要先显式地要求 MyMacros
defmodule Math do
require MyMacros
MyMacros.if do_something, it_works
end
尝试调用未加载的宏将引发错误。
别名快捷方式
在使用 Kernel.defoverridable/1
覆盖函数时,调用覆盖的函数。
查看 Kernel.defoverridable/1
以获取更多信息和文档。
评估给定的表达式,并处理可能发生的任何错误、退出或抛出。
示例
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 succeeded")
end
rescue
子句用于处理异常,而 catch
子句可用于捕获抛出的值和退出。 else
子句可用于根据表达式的结果控制流程。 catch
、rescue
和 else
子句根据模式匹配工作(类似于 case
特殊形式)。
try/1
中的调用不是尾递归,因为 VM 需要在发生异常时保留堆栈跟踪。要检索堆栈跟踪,请在 rescue
或 catch
子句中访问 __STACKTRACE__/0
。
rescue
子句
除了依赖于模式匹配,rescue
子句还提供了一些关于异常的便利,这些便利允许用户通过其名称来拯救异常。以下所有格式都是 rescue
子句中的有效模式
# Rescue a single exception without binding the exception
# to a variable
try do
UndefinedModule.undefined_function
rescue
UndefinedFunctionError -> nil
end
# Rescue any of the given exception without binding
try do
UndefinedModule.undefined_function
rescue
[UndefinedFunctionError, ArgumentError] -> nil
end
# Rescue and bind the exception to the variable "x"
try do
UndefinedModule.undefined_function
rescue
x in [UndefinedFunctionError] -> nil
end
# Rescue all kinds of exceptions and bind the rescued exception
# to the variable "x"
try do
UndefinedModule.undefined_function
rescue
x -> nil
end
Erlang 错误
在拯救时,Erlang 错误会被转换为 Elixir 错误。
try do
:erlang.error(:badarg)
rescue
ArgumentError -> :ok
end
#=> :ok
最常见的 Erlang 错误将被转换为其 Elixir 对应物。那些没有被转换为的将被转换为更通用的 ErlangError
try do
:erlang.error(:unknown)
rescue
ErlangError -> :ok
end
#=> :ok
事实上,ErlangError
可用于拯救任何不是 Elixir 正确错误的错误。例如,它可以用来拯救之前的 :badarg
错误,在转换之前。
try do
:erlang.error(:badarg)
rescue
ErlangError -> :ok
end
#=> :ok
catch
子句
catch
子句可用于捕获抛出的值、退出和错误。
捕获抛出的值
catch
可用于捕获由 Kernel.throw/1
抛出的值。
try do
throw(:some_value)
catch
thrown_value ->
IO.puts("A value was thrown: #{inspect(thrown_value)}")
end
捕获任何类型的值
catch
子句还支持捕获退出和错误。为此,它允许匹配捕获值的 _kind_ 以及值本身。
try do
exit(:shutdown)
catch
:exit, value ->
IO.puts("Exited with value #{inspect(value)}")
end
try do
exit(:shutdown)
catch
kind, value when kind in [:exit, :throw] ->
IO.puts("Caught exit or throw with value #{inspect(value)}")
end
catch
子句还支持 :error
与 :exit
和 :throw
一样,就像在 Erlang 中一样,尽管通常避免使用它,而是使用 raise
/rescue
控制机制。这样做的一个原因是,当捕获 :error
时,该错误不会自动转换为 Elixir 错误。
try do
:erlang.error(:badarg)
catch
:error, :badarg -> :ok
end
#=> :ok
after
子句
after
子句允许您定义清理逻辑,该逻辑将在传递给 try/1
的代码块成功时以及引发错误时被调用。请注意,当进程收到导致其突然退出的退出信号时,该进程将照常退出,因此 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
虽然 after
子句无论是否发生错误都会被调用,但它们不会修改返回值。以下所有示例都返回 :return_me
try do
:return_me
after
IO.puts("I will be printed")
:not_returned
end
try do
raise "boom"
rescue
_ -> :return_me
after
IO.puts("I will be printed")
:not_returned
end
else
子句
else
子句允许将传递给 try/1
的主体的结果进行模式匹配。
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
子句存在但表达式的结果与任何模式都不匹配时,将引发异常。此异常不会被同一个 try
中的 catch
或 rescue
捕获。
x = 1
try do
try do
1 / x
rescue
# The TryClauseError cannot 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
在引用的表达式内取消引用给定的表达式。
此函数期望一个有效的 Elixir AST(也称为带引号表达式)作为参数。如果您想 unquote
任何值,例如映射或四元组,则应在取消引用之前调用 Macro.escape/1
。
示例
想象一下,您有一个带引号表达式,您想将其注入到某个引号中。第一次尝试将是
value =
quote do
13
end
quote do
sum(1, value, 3)
end
:sum
函数调用的参数不是预期结果。
{:sum, [], [1, {:value, [], Elixir}, 3]}
为此,我们使用 unquote
iex> value =
...> quote do
...> 13
...> end
iex> quote do
...> sum(1, unquote(value), 3)
...> end
{:sum, [], [1, 13, 3]}
如果您想取消引用不是带引号表达式的值,例如映射,则需要在取消引用之前调用 Macro.escape/1
。
iex> value = %{foo: :bar}
iex> quote do
...> process_map(unquote(Macro.escape(value)))
...> end
{:process_map, [], [{:%{}, [], [foo: :bar]}]}
如果您忘记转义它,Elixir 在编译代码时将引发错误。
取消引用给定的列表,扩展其参数。
类似于 unquote/1
。
示例
iex> values = [2, 3, 4]
iex> quote do
...> sum(1, unquote_splicing(values), 5)
...> end
{:sum, [], [1, 2, 3, 4, 5]}
组合匹配子句。
理解 with 的一种方法是展示它改进了哪些代码模式。假设您有一个映射,其中字段 width
和 height
是可选的,并且您想计算其面积,如 {:ok, area}
或返回 :error
。我们可以将此函数实现为
def area(opts) do
case Map.fetch(opts, :width) do
{:ok, width} ->
case Map.fetch(opts, :height) do
{:ok, height} -> {:ok, width * height}
:error -> :error
end
:error ->
:error
end
end
当以 area(%{width: 10, height: 15})
的形式调用时,它应该返回 {:ok, 150}
。如果任何字段丢失,它将返回 :error
。
虽然上面的代码可以工作,但它非常冗长。使用 with
,我们可以将其重写为
def area(opts) do
with {:ok, width} <- Map.fetch(opts, :width),
{:ok, height} <- Map.fetch(opts, :height) do
{:ok, width * height}
end
end
我们使用 with
以及 PATTERN <- EXPRESSION
运算符来匹配右侧表达式的模式,而不是定义嵌套的 case
带子句。将 <-
视为 =
的兄弟姐妹,除了 =
在不匹配的情况下会引发异常,而 <-
将简单地中止 with
链并返回不匹配的值。
让我们在 IEx 上试一试。
iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> end
{: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}
...> end
: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)}
...> end
{:ok, "admin"}
就像在 for/1
中一样,在 with/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}
...> end
{:ok, 300}
iex> width
nil
with
中任何表达式的行为与在 with
之外编写时相同。例如,=
将引发 MatchError
而不是返回不匹配的值。
with :foo = :bar, do: :ok
** (MatchError) no match of right hand side value: :bar
与 Elixir 中的任何其他函数或宏调用一样,在 do
-end
块之前的参数周围也可以使用显式括号。
iex> opts = %{width: 10, height: 15}
iex> with(
...> {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height)
...> ) do
...> {:ok, width * height}
...> end
{:ok, 150}
使用括号还是不使用括号是个人喜好问题。
Else 子句
可以给出 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}
...>
...> _other_error ->
...> :unexpected_error
...> end
{:error, :wrong_data}
else
块就像一个 case
子句:它可以有多个子句,第一个匹配的子句将被使用。在 with
中绑定的变量(例如此示例中的 width
)在 else
块中不可用。
如果使用了 else
块,并且没有匹配的子句,则会引发 WithClauseError
异常。
注意!
请记住,with
的一个潜在缺点是所有失败子句都被扁平化为一个 else
块。例如,考虑这段代码,它检查给定路径是否指向 Elixir 文件,以及它在创建备份副本之前是否已经存在。
with ".ex" <- Path.extname(path),
true <- File.exists?(path) do
backup_path = path <> ".backup"
File.cp!(path, backup_path)
{:ok, backup_path}
else
binary when is_binary(binary) ->
{:error, :invalid_extension}
false ->
{:error, :missing_file}
end
请注意,我们必须重建 Path.extname/1
和 File.exists?/1
的结果类型以构建错误消息。在这种情况下,最好重构代码,以便每个 <-
在错误情况下都返回所需的格式,例如
with :ok <- validate_extension(path),
:ok <- validate_exists(path) do
backup_path = path <> ".backup"
File.cp!(path, backup_path)
{:ok, backup_path}
end
defp validate_extension(path) do
if Path.extname(path) == ".ex", do: :ok, else: {:error, :invalid_extension}
end
defp validate_exists(path) do
if File.exists?(path), do: :ok, else: {:error, :missing_file}
end
请注意,当我们确保 with
中的每个 <-
返回标准化格式后,上面的代码组织得更好,更清晰。
固定运算符。访问匹配子句中已绑定的变量。
示例
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
创建元组。
有关元组数据类型以及用于操作元组的函数的更多信息,请参阅 Tuple
模块;一些用于处理元组的函数也可以在 Kernel
中找到(例如 Kernel.elem/2
或 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]}