查看源代码 Enumerable 协议 (Elixir v1.16.2)
Enumerable 协议用于 Enum
和 Stream
模块。
当您在 Enum
模块中调用一个函数时,第一个参数通常是一个集合,它必须实现此协议。例如,表达式 Enum.map([1, 2, 3], &(&1 * 2))
调用 Enumerable.reduce/3
来执行构建映射列表的减少操作,通过对集合中的每个元素调用映射函数 &(&1 * 2)
,并使用累积列表消费元素。
在内部,Enum.map/2
的实现如下
def map(enumerable, fun) do
reducer = fn x, acc -> {:cont, [fun.(x) | acc]} end
Enumerable.reduce(enumerable, {:cont, []}, reducer) |> elem(1) |> :lists.reverse()
end
请注意,用户提供的函数被封装在一个 reducer/0
函数中。 reducer/0
函数必须在每一步之后返回一个带标签的元组,如 acc/0
类型中所述。最后, Enumerable.reduce/3
返回 result/0
。
此协议使用带标签的元组在减少器函数和实现该协议的数据类型之间交换信息。这允许有效地枚举资源,例如文件,同时还保证在枚举结束时资源将被关闭。此协议还允许暂停枚举,这在需要在多个枚举之间交织时很有用(如 zip/1
和 zip/2
函数)。
此协议要求实现四个函数,reduce/3
,count/1
,member?/2
和 slice/1
。协议的核心是 reduce/3
函数。所有其他函数都作为优化路径存在,用于可以比线性时间更好地实现某些属性的数据结构。
摘要
类型
每一步的累加器值。
一个部分应用的减少函数。
减少器函数。
减少操作的结果。
一个切片函数,它接收初始位置、切片中的元素数量和步长。
所有实现此协议的类型。
类型为 element
的元素的枚举。
接收一个枚举并返回一个列表。
类型
每一步的累加器值。
它必须是一个带标签的元组,带有以下标签之一
:cont
- 枚举应继续:halt
- 枚举应立即停止:suspend
- 枚举应立即暂停
根据累加器值, Enumerable.reduce/3
返回的结果将发生变化。有关更多信息,请查看 result/0
类型文档。
如果 reducer/0
函数返回一个 :suspend
累加器,则必须由调用者显式处理,并且永远不会泄漏。
一个部分应用的减少函数。
继续是枚举暂停时返回的结果闭包。当被调用时,它需要一个新的累加器,并返回结果。
只要减少函数以尾递归方式定义,就可以轻松地实现继续。如果函数是尾递归的,则所有状态都作为参数传递,因此继续是部分应用的减少函数。
减少器函数。
应使用 enumerable
元素和累加器内容调用。
返回下一个枚举步骤的累加器。
@type result() :: {:done, term()} | {:halted, term()} | {:suspended, term(), continuation()}
减少操作的结果。
当枚举通过到达其结尾而完成时,它可能已完成,或者当枚举被带标签的累加器停止或暂停时,它可能已停止/暂停。
如果给出了带标签的 :halt
累加器,则必须返回带有累加器的 :halted
元组。 Enum.take_while/2
等函数在内部使用 :halt
,可用于测试停止枚举。
如果给出了带标签的 :suspend
累加器,则调用者必须返回带有累加器和继续的 :suspended
元组。然后,调用者负责管理继续,并且调用者必须始终调用继续,最终停止或继续,直到结束。 Enum.zip/2
使用暂停,因此可用于测试您的实现是否正确处理了暂停。您还可以使用 Stream.zip/2
与 Enum.take_while/2
一起测试 :suspend
与 :halt
的组合。
@type slicing_fun() :: (start :: non_neg_integer(), length :: pos_integer(), step :: pos_integer() -> [term()])
一个切片函数,它接收初始位置、切片中的元素数量和步长。
start
位置是一个数字 >= 0
,保证存在于 enumerable
中。长度是一个数字 >= 1
,以使 start + length * step <= count
,其中 count
是枚举中元素的最大数量。
该函数应返回一个非空列表,其中元素数量等于 length
。
@type t() :: term()
所有实现此协议的类型。
@type t(_element) :: t()
类型为 element
的元素的枚举。
此类型等效于 t/0
,但对于文档特别有用。
例如,假设您定义了一个函数,该函数需要一个整数的枚举,并返回一个字符串的枚举
@spec integers_to_strings(Enumerable.t(integer())) :: Enumerable.t(String.t())
def integers_to_strings(integers) do
Stream.map(integers, &Integer.to_string/1)
end
接收一个枚举并返回一个列表。
函数
@spec count(t()) :: {:ok, non_neg_integer()} | {:error, module()}
检索 enumerable
中的元素数量。
如果您可以以比完全遍历它更快的速度计算 enumerable
中元素的数量,则应返回 {:ok, count}
。
否则,它应返回 {:error, __MODULE__}
,并且将使用基于 reduce/3
构建的线性时间运行的默认算法。
检查 enumerable
中是否存在 element
。
如果您可以使用 ===/2
检查给定元素在 enumerable
中的成员资格,而无需遍历整个元素,则应返回 {:ok, boolean}
。
否则,它应返回 {:error, __MODULE__}
,并且将使用基于 reduce/3
构建的线性时间运行的默认算法。
将 enumerable
减少为一个元素。
Enum
中的大多数操作都是根据减少来实现的。此函数应将给定的 reducer/0
函数应用于 enumerable
中的每个元素,并按返回的累加器预期的方式进行。
有关更多信息,请参阅类型 result/0
和 acc/0
的文档。
示例
例如,以下是列表的 reduce
实现
def reduce(_list, {:halt, acc}, _fun), do: {:halted, acc}
def reduce(list, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(list, &1, fun)}
def reduce([], {:cont, acc}, _fun), do: {:done, acc}
def reduce([head | tail], {:cont, acc}, fun), do: reduce(tail, fun.(head, acc), fun)
@spec slice(t()) :: {:ok, size :: non_neg_integer(), slicing_fun() | to_list_fun()} | {:error, module()}
返回一个连续切片数据结构的函数。
它应返回以下之一
{:ok, size, slicing_fun}
- 如果enumerable
有一个已知的边界,并且可以访问enumerable
中的位置,而无需遍历所有以前的元素。slicing_fun
将接收一个start
位置、要获取的元素的amount
和一个step
。{:ok, size, to_list_fun}
- 如果enumerable
有一个已知的边界,并且可以通过首先使用to_list_fun
将其转换为列表来访问enumerable
中的位置。{:error, __MODULE__}
- 枚举不能有效地切片,并且将使用基于reduce/3
构建的线性时间运行的默认算法。
与 count/1
的区别
此函数返回的 size
值用于边界检查,因此此函数只有在检索 enumerable
的 size
便宜、快速且需要常数时间的情况下才会返回 :ok
非常重要。否则,最简单的操作,例如 Enum.at(enumerable, 0)
,将变得过于昂贵。
另一方面,此协议中的 count/1
函数应在您可以计算集合中元素的数量而无需遍历它时实现。