查看源代码 可枚举和流
虽然 Elixir 允许我们编写递归代码,但我们对集合进行的大多数操作都是借助于 Enum
和 Stream
模块完成的。让我们学习如何使用它们。
可枚举
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 行。这意味着流对于处理大型文件甚至网络资源等缓慢资源非常有用。
Enum
和 Stream
模块提供了大量的函数,但您不必记住所有函数。熟悉 Enum.map/2
、Enum.reduce/3
以及名称中包含 map
或 reduce
的其他函数,您将自然而然地对最重要的用例形成直觉。您也可以先专注于 Enum
模块,只有在需要延迟的特定场景中才转向 Stream
,以便处理缓慢的资源或大型、可能无限的集合。
接下来,我们将研究 Elixir 的核心功能之一,进程,它允许我们以简单易懂的方式编写并发、并行和分布式程序。