18-列表速构(Comprehension)

生成器和过滤器
比特串生成器
Into

Comprehensions翻译成“速构”不知道贴不贴切,这参照了《Erlang/OTP in Action》译者的用辞。 “速构”是函数式语言中常见的概念,它大体上指的是用一套规则(比如从另一个列表,过滤掉一些元素)来生成元素填充新列表。
这个概念我们在中学的数学课上就可能已经接触过,在大学高数中更为常见:如{ x | x ∈ N },指的是这个集合里所有元素是自然数。 该集合是由自然数集合的元素映射生成过来的。相关知识可见WIKI

Elixir中,使用枚举类型(如列表)来做循环操作是很常见的。对对象列表进行枚举时,通常要有选择性地过滤掉其中一些元素,还有可能做一些变换。列表速构(comprehensions)就是为此目的诞生的语法糖:把这些常见任务分组,放到特殊的for中执行。

例如,我们可以这样计算列表中每个元素的平方:

iex> for n <- [1, 2, 3, 4], do: n * n
[1, 4, 9, 16]

注意看,<-符号就是模拟自的形象。 这个例子用熟悉(当然,如果你高数课没怎么听那就另当别论)的数学符号表示就是:

S = { X^2 | X ∈ [1,4], X ∈ N}

这个例子用常见的编程语言去理解,基本上类似于foreach...in...什么的。但是更强大。

一个列表速构由三部分组成:生成器,过滤器和收集器。

18.2-生成器和过滤器

在上面的例子中,n <- [1, 2, 3, 4]就是生成器。它字面意思上生成了即将要在速构中使用的数值。 任何枚举类型都可以传递给生成器表达式的右端:

iex> for n <- 1..4, do: n * n
[1, 4, 9, 16]

这个例子中的生成器是一个范围

生成器表达式支持模式匹配,它会忽略所有不匹配的模式。 想象一下如果不用范围而是用一个键值列表,键只有:good:bad两种,来计算中间被标记成‘good’的元素的平方:

iex> values = [good: 1, good: 2, bad: 3, good: 4]
iex> for {:good, n} <- values, do: n * n
[1, 4, 16]

过滤器能过滤掉某些产生的值。例如我们可以只对奇数进行平方运算:

iex> require Integer
iex> for n <- 1..4, Integer.odd?(n), do: n * n
[1, 9]

过滤器会保留所有判断结果是非nil或非false的值。

总的来说,速构比直接使用枚举或流模块的函数提供了更精确的表述。 不但如此,速构还接受多个生成器和过滤器。下面就是一个例子,代码接受目录列表,删除这些目录下的所有文件:

for dir  <- dirs,
    file <- File.ls!(dir),
    path = Path.join(dir, file),
    File.regular?(path) do
      File.rm!(path)
end

需要记住的是,在速构中,变量赋值这种事应在生成器中进行。因为在过滤器或代码块中的赋值操作不会反映到速构外面去。

18.2-比特串生成器

速构也支持比特串作为生成器,而且这种生成器在组织处理比特串的流时非常有用。 下面的例子中,程序从二进制数据(表示为<<像素1的R值,像素1的G值,像素1的B值,像素2的R值,像素2的G...>>)中接收一个像素的列表,把它们转换为元组:

iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
iex> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b}
[{213,45,132},{64,76,32},{76,0,0},{234,32,15}]

比特串生成器可以和“普通的”枚举类型生成器混合使用,过滤器也是。

18.3-Into

在上面的例子中,速构返回一个列表作为结果。 但是,通过使用:into选项,速构的结果可以插入到一个不同的数据结构中。 例如,你可以使用比特串生成器加上:into来轻松地构成无空格字符串:

iex> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>
"helloworld"

集合、图以及其他字典类型都可以传递给:into选项。总的来说,:into接受任何实现了Collectable协议的数据结构。

例如,IO模块提供了流。流既是Enumerable也是Collectable。 你可以使用速构实现一个回声终端,让其返回任何输入的东西的大写形式:

iex> stream = IO.stream(:stdio, :line)
iex> for line <- stream, into: stream do
...>   String.upcase(line) <> "\n"
...> end

现在在终端中输入任意字符串,你会看到同样的内容以大写形式被打印出来。 不幸的是,这个例子会让你的shell陷入到该速构代码中,只能用Ctrl+C两次来退出。