可枚举和流 | Enumerables and Streams
可枚举和流
枚举
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代码中经常使用的模块之一。
Elixir还提供范围:
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
函数,因为将值插入例如List
某个范围是没有意义的。
我们说Enum
模块中的函数是多态的,因为它们可以处理不同的数据类型。特别是,Enum
模块中的功能可以与任何实现该Enumerable
协议的数据类型一起使用。我们将在后面的章节中讨论议定书; 现在我们将转向一种称为流的特定类型的枚举。
渴望vs懒惰
Enum
模块中的所有功能都很渴望。许多函数期望可枚举并返回一个列表:
iex> odd? = &(rem(&1, 2) != 0)
#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
项目,然后我们总结所有条目。
管道操作员
|>上面代码片段中使用的符号是管道运算符:它从左边的表达式获取输出,并将其作为第一个参数传递给右边的函数调用。它与Unix |操作符相似。其目的是突出显示由一系列功能所转换的数据。要了解它如何使代码更清洁,请查看上面没有使用|>运算符而重写的示例:
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))返回一个数据类型,一个实际的数据流,表示map在该范围内的计算1..100_000:
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.cycle/1>
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
可以用来环绕资源,保证它们在枚举之前打开并在事后关闭,即使在失败的情况下也是如此。例如,我们可以使用它来流式传输一个文件:
iex> stream = File.stream!("path/to/file")
#Function<18.16982430/2 in Stream.resource/3>
iex> Enum.take(stream, 10)
上面的例子将获取您选择的文件的前10行。这意味着流可以非常有用地处理大文件或者像网络资源那样缓慢的资源。
开始时模块Enum
和Stream
模块中的功能量可能会让人望而生畏,但您会逐一熟悉它们。特别是,首先关注Enum
模块,并且只Stream
针对需要懒惰的特定情况,以处理缓慢的资源或处理大量可能无限的集合。
接下来,我们将看看Elixir中的一个特性,Processes,它允许我们以简单易懂的方式编写并发,并行和分布式程序。