枚举接口与流

  1. 1. 枚举接口
  2. 2. 积极与懒惰
  3. 3. 管道操作符
  4. 4.

枚举接口

Elixir提供了可枚举性的概念,以及Enum模块来操作它们。我们已经学习了两个可枚举结构:列表和映射。

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

Enum模块提供了巨量的函数用于对可枚举体中的元素进行变换,排序,组合,筛选和检索。这是Elixir开发者最常用的模块之一。

Elixir也提供范围:

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

Enum模块中的函数只适用于对数据结构中的值进行枚举。对于特定操作,例如插入和更新特定的元素,你可能需要使用针对于某种数据类型的模块。例如,如果你想要往列表的指定位置插入指定元素,你应当使用List模块中的List.insert_at/3函数,因为将值插入到一个范围中是没有意义的。

我们说Enum模块中的函数是多态的,因为它们适用于多种数据类型。特别地,Enum模块中的函数适用于任何实现了Enumerable协议的数据类型。我们将在之后的章节讨论协议;现在我们将讲到一种特殊的枚举体,流。

积极与懒惰

Enum模块中的所有函数都是积极的。许多函数接受了一个枚举体并返回了一个列表:

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

这意味着当使用Enum进行多次操作时,每个操作都在生成一个中间列表,直到获得最终结果:

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

上述例子用到了管道操作符。我们以一个范围开始,让后将范围中的每个数乘以3.第一个操作将会创造并返回一个100_000个元素的列表。之后我们留下了列表中的所有奇数,并生成了一个有50_000个元素的新列表,然后我们将其全部相加。

管道操作符

上述片段中用到的|>符号叫做管道操作符:它将左边的表达式的结果作为第一个参数传递给右边的函数。这与Unix中的|操作符类似。它的目的在于使被一系列函数处理的数据流变得醒目。看看不用|>操作符,重写的上述代码,就知道它是如何让代码变整洁的了:

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

Elixir提供了支持懒惰操作的String模块来作为Enum的替代品:

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

流是懒惰的,组合的枚举接口。

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

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

而且它们是可组合的,因为我们可以用管连接许多流操作:

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

流没有生成中间列表,而是构建了一系列的计算,它们只会在我们将潜在的流传递给Enum模块时才会被调用。流在处理大量的,有可能是无限的,集合时非常有用。

Stream模块中的许多函数接受任何枚举体作为参数,并返回一个流作为结果。也有用于创建流的函数。例如,Stream.cycle/1用于创建一个将给定枚举体无限循环的流,注意不要以这种流为参数调用例如Enum.map/2之类的函数,因为他们会无限循环下去:

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

另一方面,Stream.unfold/2可以用于从给定的初始值中生成多个值:

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

另一个有趣的函数是Stream.resource/3,它可以用于包裹源代码,保证它们在枚举之前是开启的且之后是关闭的,即使在失败的案例中。例如,我们可以用它流一个文件:

  1. iex> stream = File.stream!("path/to/file")
  2. #Function<18.16982430/2 in Stream.resource/3>
  3. iex> Enum.take(stream, 10)

上述例子将会获得所选文件的前十行。这意味着流可以很好地处理巨大的文件,即使是像网络资源那样的慢资源。

EnumStream模块中的函数数量也许一开始很吓人,但你会一个案例一个案例地熟悉他们。首先专注于Enum模块,只在要求懒惰性的情形下使用Stream模块,例如处理慢资源,或巨大的,可能无限的集合。

我们将要讲到Elixir的核心特性,进程,它使得我们能以一种简单易懂的方式编写并发,并行和分布式的程序。