查看源代码 可枚举和流

虽然 Elixir 允许我们编写递归代码,但我们对集合进行的大多数操作都是借助于 EnumStream 模块完成的。让我们学习如何使用它们。

可枚举

Elixir 提供了可枚举的概念,以及 Enum 模块来处理它们。我们已经学习了两种可枚举:列表和映射。

iex> Enum.map([1, 2, 3], fn x -> x * 2 end)
[2, 4, 6]
iex> Enum.map(%{1 => 2, 3 => 4}, fn {k, v} -> k * v end)
[2, 12]

Enum 模块提供了大量的函数来转换、排序、分组、过滤和从可枚举中检索项目。它是开发者在 Elixir 代码中频繁使用的模块之一。有关 Enum 模块中所有函数的概述,请参阅 Enum 速查表

Elixir 还提供了范围(请参阅 Range),它们也是可枚举的。

iex> Enum.map(1..3, fn x -> x * 2 end)
[2, 4, 6]
iex> Enum.reduce(1..3, 0, &+/2)
6

Enum 模块中的函数仅限于,顾名思义,枚举数据结构中的值。对于特定的操作,例如插入和更新特定元素,您可能需要使用特定于数据类型的模块。例如,如果您想在列表中的给定位置插入一个元素,您应该使用 List.insert_at/3 函数,因为将值插入例如范围中是没有意义的。

我们说 Enum 模块中的函数是多态的,因为它们可以处理不同的数据类型。特别是,Enum 模块中的函数可以处理任何实现 Enumerable 协议的数据类型。我们将在后面的章节中讨论协议,现在我们将继续讨论一种称为流的特定类型的可枚举。

急切 vs 延迟

Enum 模块中的所有函数都是急切的。许多函数期望一个可枚举,并返回一个列表。

iex> odd? = fn x -> rem(x, 2) != 0 end
#Function<6.80484245/1 in :erl_eval.expr/5>
iex> Enum.filter(1..3, odd?)
[1, 3]

这意味着,当使用 Enum 执行多个操作时,每个操作都将生成一个中间列表,直到我们到达结果。

iex> 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum()
7500000000

上面的例子有一个操作流水线。我们从一个范围开始,然后将范围中的每个元素乘以 3。这个第一个操作现在将创建一个包含 100_000 个项目的列表并返回它。然后,我们从列表中保留所有奇数元素,生成一个新的列表,现在包含 50_000 个项目,然后我们对所有条目求和。

管道操作符

上面的代码段中使用的 |> 符号是 **管道操作符**:它获取其左侧表达式的输出,并将其作为第一个参数传递给其右侧的函数调用。它的目的是突出显示由一系列函数转换的数据。要查看它如何使代码更简洁,请查看上面不使用 |> 操作符重写的示例。

iex> Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), odd?))
7500000000

您可以 阅读管道操作符的文档,以了解有关它的更多信息。

作为 Enum 的替代方案,Elixir 提供了 Stream 模块,它支持延迟操作。

iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum()
7500000000

流是延迟的、可组合的可枚举。

在上面的例子中,1..100_000 |> Stream.map(&(&1 * 3)) 返回一种数据类型,即实际的流,它代表了对范围 1..100_000 进行 map 计算。

iex> 1..100_000 |> Stream.map(&(&1 * 3))
#Stream<[enum: 1..100000, funs: [#Function<34.16982430/1 in Stream.map/2>]]>

此外,它们是可组合的,因为我们可以将许多流操作连接起来。

iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?)
#Stream<[enum: 1..100000, funs: [...]]>

流不会生成中间列表,而是构建一系列计算,只有当我们将底层流传递给 Enum 模块时,才会执行这些计算。流在处理大型、可能无限的 集合时非常有用。

Stream 模块中的许多函数接受任何可枚举作为参数,并返回一个流作为结果。它还提供了创建流的函数。例如,Stream.cycle/1 可用于创建一个无限循环给定可枚举的流。请注意,不要对这样的流调用 Enum.map/2 等函数,因为它们将无限循环。

iex> stream = Stream.cycle([1, 2, 3])
#Function<15.16982430/2 in Stream.unfold/2>
iex> Enum.take(stream, 10)
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1]

另一方面,Stream.unfold/2 可用于从给定的初始值生成值。

iex> stream = Stream.unfold("hełło", &String.next_codepoint/1)
#Function<39.75994740/2 in Stream.unfold/2>
iex> Enum.take(stream, 3)
["h", "e", "ł"]

另一个有趣的函数是 Stream.resource/3,它可以用于包装资源,保证它们在枚举之前打开,并在之后关闭,即使在出现故障的情况下也是如此。例如,File.stream!/1 基于 Stream.resource/3 来流式传输文件。

iex> stream = File.stream!("path/to/file")
%File.Stream{
  line_or_bytes: :line,
  modes: [:raw, :read_ahead, :binary],
  path: "path/to/file",
  raw: true
}
iex> Enum.take(stream, 10)

上面的例子将获取您所选文件的前 10 行。这意味着流对于处理大型文件甚至网络资源等缓慢资源非常有用。

EnumStream 模块提供了大量的函数,但您不必记住所有函数。熟悉 Enum.map/2Enum.reduce/3 以及名称中包含 mapreduce 的其他函数,您将自然而然地对最重要的用例形成直觉。您也可以先专注于 Enum 模块,只有在需要延迟的特定场景中才转向 Stream,以便处理缓慢的资源或大型、可能无限的集合。

接下来,我们将研究 Elixir 的核心功能之一,进程,它允许我们以简单易懂的方式编写并发、并行和分布式程序。