- 编程语言
- 编译器
- 标准库
- 语言运行时
- 并发编程
【免费下载链接】elixir
Simple from zero to scale
本文是 Elixir 官方参考文档《Patterns and guards》的深度技术指南,完整覆盖模式匹配(pattern matching)与守卫(guards)的语义规则、允许使用的构造、适用位置以及基于defguard/1的自定义扩展方法。读者阅读后将能准确掌握=匹配运算符、pin 运算符、各数据结构的匹配规则,理解守卫与普通表达式的本质区别(无副作用、可优化、异常即失败),并在实际项目中熟练运用match?/2、函数子句、case、with与自定义守卫。
Elixir 通过模式匹配让我们可以断言数据的形状或从中提取值,而守卫则进一步提供了更复杂(但有严格限制)的检查能力。本文全部内容以仓库中的官方参考文档 patterns-and-guards.md 为主体,并结合 Elixir 源码与测试用例进行印证。
模式匹配(Patterns)
Elixir 中的模式由变量、字面量以及数据结构特有的语法构成。最常用的模式匹配构造是匹配运算符=:
iex> x = 1 1 iex> 1 = x 1在上面的例子中,x一开始没有值,被赋值为1。随后我们将x的值与字面量1进行比较,因为两者都是1,匹配成功。
如果尝试将x与2匹配则会抛出异常:
iex> 2 = x ** (MatchError) no match of right hand side value: 1模式不是双向的。如果一个变量y从未被赋值(通常称为未绑定变量),写下1 = y会直接报错:
iex> 1 = y ** (CompileError) iex:2: undefined variable "y"换句话说,模式只允许出现在=的左侧;=的右侧遵循语言常规的求值语义。
变量
模式中的变量总是会被赋值(绑定):
iex> x = 1 1 iex> x = 2 2 iex> x 2也就是说,Elixir 支持重新绑定(rebinding)。如果不想让变量的值改变,可以使用 pin 运算符(^):
iex> x = 1 1 iex> ^x = 2 ** (MatchError) no match of right hand side value: 2这里^x表示匹配x的当前值(即1),而不是重新绑定,因此与2匹配失败。从源码来看,pin 运算符匹配的是值而非模式,其相等性判断基于===/2严格比较,相关语义在 kernel.ex 的 pin 运算符文档(约第 3576 行、3604 行)中有明确说明。
如果同一个变量在同一个模式中出现多次,那么所有出现处都必须绑定到相同的值:
iex> {x, x} = {1, 1} {1, 1} iex> {x, x} = {1, 2} ** (MatchError) no match of right hand side value: {1, 2}然而,一个变量不能在同一模式中通过自身来定义。这包括变量同时出现在嵌套匹配运算符两侧的情况:
{:ok, x} = {x, :ok} %{a: value} = %{b: value}这类模式会被拒绝为"循环变量定义"(cyclic variable definitions)。正确的改写方式是:先匹配一次,再使用守卫或单独的==比较来施加额外约束。
下划线变量(_)具有特殊含义:它永远不会被绑定到任何值。当你不在意模式中的某个值时它特别有用:
iex> {_, integer} = {:not_important, 1} {:not_important, 1} iex> integer 1 iex> _ ** (CompileError) iex:3: invalid use of _被 pin 的值代表该值本身,而不是一个(即使语法上相同的)模式。右侧的值需要与 pin 的值相等:
iex> x = %{} %{} iex> {:ok, %{}} = {:ok, %{a: 13}} {:ok, %{a: 13}} iex> {:ok, ^x} = {:ok, %{a: 13}} ** (MatchError) no match of right hand side value: {:ok, %{a: 13}}注意:%{}模式可以匹配任何映射(包括含a: 13的映射),但被 pin 的^x只匹配与x的值(空映射%{})严格相等的值。
字面量(数字与原子)
原子和数字(整数与浮点数)可以出现在模式中,并且总是按自身表示。例如,原子只有当与它自身相同才会匹配:
iex> :atom = :atom :atom iex> :atom = :another_atom ** (MatchError) no match of right hand side value: :another_atom数字遵循类似的规则。需要特别注意的是,模式中的数字执行严格比较,即整数不匹配浮点数:
iex> 1 = 1.0 ** (MatchError) no match of right hand side value: 1.0元组
元组可以使用花括号语法({})出现在模式中。模式中的元组只匹配相同大小的元组,且每个元素也必须各自匹配:
iex> {:ok, integer} = {:ok, 13} {:ok, 13} # 因大小不同而不匹配 iex> {:ok, integer} = {:ok, 11, 13} ** (MatchError) no match of right hand side value: {:ok, 11, 13} # 因第一个元素不匹配 iex> {:ok, binary} = {:error, :enoent} ** (MatchError) no match of right hand side value: {:error, :enoent}{:ok, result}这种"标签 + 值"的匹配模式是 Elixir 代码中最常见的惯用法之一,广泛用于函数返回值(如File.read/1)与with表达式的解构。
列表
列表可以使用方括号语法([])出现在模式中。模式中的列表只匹配相同大小的列表,且每个元素也必须各自匹配:
iex> [:ok, integer] = [:ok, 13] [:ok, 13] # 因大小不同而不匹配 iex> [:ok, integer] = [:ok, 11, 13] ** (MatchError) no match of right hand side value: [:ok, 11, 13] # 因第一个元素不匹配 iex> [:ok, binary] = [:error, :enoent] ** (MatchError) no match of right hand side value: [:error, :enoent]与元组不同,列表还允许使用[head | tail]记法匹配非空列表,这会将列表拆分为头部head与尾部tail:
iex> [head | tail] = [1, 2, 3] [1, 2, 3] iex> head 1 iex> tail [2, 3]多个元素可以作为| tail的前缀:
iex> [first, second | tail] = [1, 2, 3] [1, 2, 3] iex> tail [3]注意[head | tail]不匹配空列表:
iex> [head | tail] = [] ** (MatchError) no match of right hand side value: []由于字符列表(charlist)表示为整数列表,因此也可以使用列表拼接运算符++对字符列表执行前缀匹配:
iex> ~c"hello " ++ world = ~c"hello world" ~c"hello world" iex> world ~c"world"这等价于匹配[?h, ?e, ?l, ?l, ?o, ?\s | world]。后缀匹配(如hello ++ ~c" world")不是合法的模式。
映射
映射可以使用百分号加花括号语法(%{})出现在模式中。与列表和元组不同,映射执行子集匹配:只要映射模式中的所有键在右侧映射中都存在(且值匹配),就匹配成功。
下面是一个所有键都匹配的例子:
iex> %{name: name} = %{name: "meg"} %{name: "meg"} iex> name "meg"下面是只有部分键匹配的例子:
iex> %{name: name} = %{name: "meg", age: 23} %{age: 23, name: "meg"} iex> name "meg"如果模式中的某个键在映射中不存在,则匹配失败:
iex> %{name: name, age: age} = %{name: "meg"} ** (MatchError) no match of right hand side value: %{name: "meg"}注意,空映射%{}会匹配所有映射,这与元组和列表形成鲜明对比——空元组或空列表只能分别匹配空元组或空列表:
iex> %{} = %{name: "meg"} %{name: "meg"}最后需要说明的是:模式中的映射键必须始终是字面量,或是用 pin 运算符固定的已绑定变量。
结构体
结构体可以使用百分号、结构体模块名(或一个变量)加上花括号语法(%{})出现在模式中。
给定如下结构体:
defmodule User do defstruct [:name] end下面是所有键都匹配的例子:
iex> %User{name: name} = %User{name: "meg"} %User{name: "meg"} iex> name "meg"如果给出了未知键,编译器会直接报错:
iex> %User{type: type} = %User{name: "meg"} ** (CompileError) iex: unknown key :type for struct User当把模块名替换为变量时,可以提取结构体的名称:
iex> %struct_name{} = %User{name: "meg"} %User{name: "meg"} iex> struct_name User二进制
二进制可以使用双尖括号语法(<<>>)出现在模式中。模式中的二进制可以同时匹配多个段(segment),每个段可以有不同的类型、大小和单位:
iex> <<val::unit(8)-size(2)-integer>> = <<123, 56>> "{8" iex> val 31544其中unit(8)指定每个单元为 8 位,size(2)指定共 2 个单元,integer指定该段为整数类型,因此val被解析为123 * 256 + 56 = 31544。二进制的完整模式匹配定义可查阅<<>>/1运算符的文档。
另外请记住:Elixir 中的字符串是 UTF-8 编码的二进制。因此与字符列表类似,字符串也可以用二进制拼接运算符<>进行前缀匹配:
iex> "hello " <> world = "hello world" "hello world" iex> world "world"后缀匹配(如hello <> " world")不是合法的模式。
守卫(Guards)
守卫是一种用更复杂检查来增强模式匹配的方式。它们被允许出现在一组预定义的、允许模式匹配的构造中,例如函数定义、case子句等。
并非所有表达式都能出现在守卫子句中,只有少数几种允许。这是有意为之的设计:通过这种方式,Elixir(经由 Erlang)确保所有守卫都是可预测的(没有突变或其他副作用),并且可以被优化、高效执行。
允许的函数与操作符清单
内置守卫的完整清单定义在Kernel模块中(见 kernel.ex 的## Guards文档章节,约第 148 行起)。概览如下:
- 比较运算符(
==、!=、===、!==、<、<=、>、>=),以及max/2、min/2 - 严格布尔运算符
and/2、or/2、not/1。注意&&、||和!这组同族运算符不允许出现在守卫中,因为它们不是严格布尔运算——它们不要求参数必须是布尔值 - 一元算术运算符
+/1、-/1 - 二元算术运算符
+/2、-/2、*/2、//2 in/2与not in/2运算符(前提是右侧为列表或范围)- "类型检查"函数(
is_list/1、is_number/1等) - 作用于内置数据类型上的函数(
abs/1、hd/1、map_size/1等) map.field语法
此外,Bitwise模块(见 bitwise.ex)也包含一批可作守卫使用的 Erlang 位运算操作。
由上述守卫任意组合构造出的宏同样是合法的守卫——例如Integer.is_even/1。更多信息参见下文"自定义模式与守卫表达式"一节。
为什么需要守卫
来看一个在函数子句中使用守卫的例子:
def empty_map?(map) when map_size(map) == 0, do: true def empty_map?(map) when is_map(map), do: false守卫以when运算符开头,后跟一个守卫表达式。当且仅当守卫表达式求值为true时,该子句才会被执行。多个布尔条件可以通过and/2与or/2组合。
仅靠模式匹配是无法写出这个empty_map?/1函数的——因为在%{}上做模式匹配会匹配任何映射,而不只是空映射(正如上文映射一节所述,空映射模式匹配所有映射)。
非布尔结果的守卫(Non-passing guards)
函数子句当且仅当其守卫表达式求值为true时才会执行。如果返回任何其他值,该函数子句都会被跳过。特别地,守卫没有"truthy"(真值)或"falsy"(假值)的概念。
例如,想象一个检查列表头部是否不是nil的函数:
def not_nil_head?([head | _]) when head, do: true def not_nil_head?(_), do: false not_nil_head?(["some_value", "another_value"]) #=> false尽管列表头部不是nil,not_nil_head?/1的第一个子句仍然失败,因为表达式没有求值为true,而是"some_value",因此触发了第二个子句返回false。要让守卫正确工作,必须确保守卫求值为true,例如:
def not_nil_head?([head | _]) when head != nil, do: true def not_nil_head?(_), do: false not_nil_head?(["some_value", "another_value"]) #=> true守卫中的错误
在守卫中,当函数正常会抛出异常时,守卫会失败(即该子句不匹配)而不是抛出异常。
例如,tuple_size/1只对元组有效。如果对其它类型使用,会抛出参数错误:
iex> tuple_size("hello") ** (ArgumentError) argument error但在守卫中使用时,相应的子句会匹配失败而不是报错:
iex> case "hello" do ...> something when tuple_size(something) == 2 -> ...> :worked ...> _anything_else -> ...> :failed ...> end :failed在许多情况下我们可以利用这一特性。上面的代码用tuple_size/1同时完成了"检查值是元组"和"检查其大小"两件事(而不必写is_tuple(something) and tuple_size(something) == 2)。
不过,如果守卫有多个条件(例如同时检查元组或映射),最好先调用is_tuple/1这类类型检查函数,再调用tuple_size/1,否则一旦传入的不是元组,整个守卫都会失败。另一种方案是使用下文介绍的"同一子句中的多重守卫"。
同一子句中的多重守卫
Elixir 提供了一种简化守卫中or表达式链的额外方式:在同一个子句中编写"多重守卫"。以下代码:
def categorize_number(term) when is_integer(term) or is_float(term) or is_nil(term), do: :maybe_number def categorize_number(_other), do: :something_else可以改写为:
def categorize_number(term) when is_integer(term) when is_float(term) when is_nil(term) do :maybe_number end def categorize_number(_other) do :something_else end如果每个守卫表达式始终返回布尔值,这两种形式是等价的。但请回顾:如果守卫中的任何函数调用抛出了异常,整个守卫都会失败。为了说明这一点,下面的函数将无法检测空元组:
defmodule Check do # 如果传入元组,map_size/1 会抛异常,tuple_size/1 将不会被求值 def empty?(val) when map_size(val) == 0 or tuple_size(val) == 0, do: true def empty?(_val), do: false end Check.empty?(%{}) #=> true Check.empty?({}) #=> false # 期望是 true!这可以通过确保不抛异常来修正,要么使用类型检查(如is_map(val) and map_size(val) == 0),要么使用多重守卫——这样即使某个守卫因异常失败,下一个守卫仍会被求值:
defmodule Check do # 如果传入元组,map_size/1 会抛异常,第二个守卫仍会被求值 def empty?(val) when map_size(val) == 0 when tuple_size(val) == 0, do: true def empty?(_val), do: false end Check.empty?(%{}) #=> true Check.empty?({}) #=> true核心区别:or链中一旦某个守卫抛异常,整个守卫短路失败;而多重守卫(when依次排列)中每个守卫独立求值,前一个失败会继续尝试下一个。这是两种写法在语义上最重要的差异。
模式与守卫可以在哪里使用
在之前的例子中,我们分别用匹配运算符=和函数子句展示了模式与守卫。以下是 Elixir 内置的、支持模式与守卫的全部构造:
match?/2:match?({:ok, value} when value > 0, {:ok, 13})match?/2是Kernel中的宏(见 kernel.ex 第 3614 行)。从源码可以看出,它本质上把模式编译成一个case表达式:模式匹配成功返回true,否则返回false,不会抛出MatchError,非常适合用在断言与过滤场景。函数子句:
def type(term) when is_integer(term), do: :integer def type(term) when is_float(term), do: :floatcase表达式:case x do 1 -> :one 2 -> :two n when is_integer(n) and n > 2 -> :larger_than_two end匿名函数(
fn):larger_than_two? = fn n when is_integer(n) and n > 2 -> true n when is_integer(n) -> false endfor与with支持在<-左侧使用模式与守卫:for x when x >= 0 <- [1, -2, 3, -4], do: xwith还支持else关键字,其中支持模式匹配与守卫。try在catch和else上支持模式与守卫。receive支持用模式与守卫匹配接收到的消息。也可以用
defguard/1与defguardp/1定义自定义守卫,自定义守卫只能基于已有守卫来定义。
需要特别注意:匹配运算符=不支持守卫。以下写法是合法的模式匹配(没有守卫):
{:ok, binary} = File.read("some/file")但你不能在=左侧写when守卫。
自定义模式与守卫表达式
只有本页列出的构造才允许出现在模式与守卫中。然而,我们可以利用宏来编写自定义的模式/守卫表达式,以简化程序或使其更具领域特定性。归根结底,重要的是宏的输出必须归结为上述构造的组合。
例如,Elixir 的Record模块(见 record.ex)提供了一系列可在模式与守卫中使用的宏,允许元组在编译期拥有命名字段。
用defguard/1与defguardp/1定义自定义守卫
Elixir 为定义自己的守卫提供了defguard/1与defguardp/1便利设施。来看一个快速案例研究:我们希望检查一个参数是否为偶数或奇数整数。用模式匹配这是不可能的——因为整数有无限多个,我们不可能对每一个都写一个模式。因此必须使用守卫。这里只关注偶数检查,奇数几乎完全一样。
这样的守卫看起来像:
def my_function(number) when is_integer(number) and rem(number, 2) == 0 do # do stuff end每次需要这个检查时都重复书写会非常啰嗦。相反,你可以用defguard/1和defguardp/1创建守卫宏:
defmodule MyInteger do defguard is_even(term) when is_integer(term) and rem(term, 2) == 0 end然后这样使用:
import MyInteger, only: [is_even: 1] def my_function(number) when is_even(number) do # do stuff enddefguard/1会在编译期对守卫表达式进行校验,若使用了守卫子句不允许的表达式,将在编译期直接抛出异常(guard_test.exs中对此有专门的测试用例,见 guard_test.exs)。defguardp/1定义的是模块私有的守卫宏,规则类似defmacrop/2:必须在当前模块中使用之前定义。
标准库中的defguard实践
Elixir 标准库自身就大量使用defguard。例如Integer.is_odd/1与Integer.is_even/1在 integer.ex 中的实现为:
defguard is_odd(integer) when is_integer(integer) and (integer &&& 1) == 1 defguard is_even(integer) when is_integer(integer) and (integer &&& 1) == 0注意这里用位运算&&&(来自Bitwise模块)实现奇偶判断,而本文案例中用rem/2取余实现——两种方式都是合法的守卫表达式。这印证了defguard的确可以组合任意"已允许的守卫构造"。
defguard的底层实现位于 kernel.ex 第 5922 行起:defguard生成一个宏,该宏在守卫内外都能使用,并保持各自语义——在守卫内部调用时,相当于把when右侧的表达式注入守卫;在守卫外部调用时,保持普通函数调用语义(所有参数在调用前求值,未使用的参数则不求值)。Kernel模块文档还建议,定义自定义守卫时参考 naming-conventions.md 中关于布尔守卫命名(is_前缀)的约定。
虽然用宏也能创建自定义守卫,但推荐使用defguard/1与defguardp/1,因为它们会执行额外的编译期检查,确保自定义守卫合法且可被编译器优化。
小结
- 模式匹配是解构数据的基础工具:变量总是可重新绑定,
^pin 运算符固定已有值,_永不绑定;元组与列表要求大小完全一致,映射与结构体执行子集匹配(结构体还要求键必须合法),二进制与字符串支持多段匹配与<>前缀匹配。 - 守卫是对模式匹配的受控增强:只允许无副作用的纯函数集合(比较、严格布尔、算术、类型检查、
in、map.field等),守卫中的异常导致子句失败而非抛出,且不存在 truthy/falsy 语义——只有true才会放行。 - 适用位置覆盖
match?/2、函数子句、case、fn、for/with、try、receive,但=运算符不支持守卫。 - 扩展之道是
defguard/1与defguardp/1:它们把合法守卫表达式封装为宏,兼具编译期校验与守卫内外的双重可用性,标准库的Integer.is_even/1就是最佳范例。
如需进一步学习,可继续阅读仓库中的相关文档:operators.md(运算符优先级)、naming-conventions.md(命名约定)、typespecs.md(类型规格),以及 kernel.ex 中## Guards章节列出的全部内置守卫清单。
- 编程语言
- 编译器
- 标准库
- 语言运行时
- 并发编程
【免费下载链接】elixir
Simple from zero to scale
相关推荐
NodeGui 中 NavigationMode 枚举完整参考:成员取值、定义位置与键盘导航模式语义
NodeGui 中 NavigationMode 枚举完整参考:成员取值、定义位置与键盘导航模式语义 NavigationMode 是 NodeGui 在 Qt
桌面应用跨平台GetQzonehistory 完整指南:QQ 空间历史说说备份,一键导出到 Excel
GetQzonehistory 完整指南:QQ 空间历史说说备份,一键导出到 Excel QQ 空间里堆了好几年的说说,翻起来费劲,想备份又怕丢。GetQzon
网页爬虫数据分析laravel-permission 通配符权限(Wildcard Permissions)完整指南:语法、匹配原理与自定义扩展
laravel permission 通配符权限(Wildcard Permissions)完整指南:语法、匹配原理与自定义扩展 导读 本文聚焦 laravel
后端认证鉴权
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考