Elixir入门教程-枚举类型和流

  1. 枚举类型
  2. 积极 vs 懒惰
  3. 管道运算符

枚举类型

Elixir提供了枚举类型的概念以及Enum模块来处理它们。我们已经学了两个枚举类型:list 和 map 。

1
2
3
4
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也提供了范围这种数据类型:

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

Enum模块里的函数正如它的名字所示,它们被限制只能枚举数据结构的值。对于特殊的操作,比如插入和修改特定的元素,你可能需要根据特定的数据类型使用相应的模块。例如,如果你要在列表的给定位置插入一个元素,你应该使用List模块里的List.insert_at/3函数,因为它插入一个值到一个范围数据里是没意义的。

我们说Enum模块里的函数是多态的,是因为它们可以处理多种数据类型。尤其是,Enum模块里的函数可以处理任何实现了Enumerable协议的数据。我们将在后续的章节讨论协议;现在我们将学习叫做流的特殊枚举类型。

积极 VS 懒惰

Enum模块里的所有函数都是积极的。许多函数都期盼一个枚举类型数据然后返回一个列表。

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

这意味着,当用Enum进行多个操作的时候,每个操作都将生成一个中间列表一直到我们最后得到结果。

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

上面的例子是一个管道操作。我们从一个范围数据开始,然后将它的每个元素乘于3。第一个操作将创建和返回一个有10万元素的列表。接着我们保留这个列表里的所有奇数元素并生成一个新的有5万元素的列表,最后我们统计这些元素的和。

管道运算符

上面例子使用的 |> 符号是管道运算符:它获取它左边表达式的输出然后将这个输出作为第一个参数传给它右边的函数。它和Unix的 | 管道操作符相似。其目的是突出数据被一系列函数变换。为了看清楚它是如何使得代码更简洁的,我们来看看上述例子不用 |> 运算符来写的样子:

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

请阅读相关文档来深入理解管道运算符。

作为Enum的替代物,Elixir提供了支持懒操作的Stream模块

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

流是懒惰的,可组合的枚举数据类型。

在上述例子里,1..100_000 |> Stream.map(&(&1 * 3)) 返回一个数据类型,实际上就是一个流,它表示在范围数据 1..100_000上的映射计算:

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

而且,它们是可组合的,因为我们可以用管道操作来组合多个流:

1
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 这样的函数,因为它们会无限循环下去:

1
2
3
4
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 可以被用来从一个给定的初始值来创建流:

1
2
3
4
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,它可被用来包裹资源,从而保证资源即使在失败的情况下也可以在枚举之前被正确地打开和在后续被关闭。例如,我们可以用它来流式一个文件:

1
2
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的一个核心特性:进程,它将允许我们以简单和可理解的方式来写并发、并行和分布式程序。

原文链接: http://elixir-lang.org/getting-started/enumerables-and-streams.html