推导 | Comprehensions
理解
在Elixir中,循环遍历一个Enumerable是很常见的,通常会筛选出一些结果并将值映射到另一个列表。理解是这种结构的语法sugar:它们将这些常见任务分成for
特殊形式。
例如,我们可以将整数列表映射为它们的平方值:
iex> for n <- [1, 2, 3, 4], do: n * n
[1, 4, 9, 16]
理解由三部分组成:生成器,过滤器和可收集物。
生成器和滤波器
在上面的表达式中,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]
作为模式匹配的替代,滤波器可以用来选择一些特定的元素。例如,我们可以选择3的倍数并丢弃所有其他的:
iex> multiple_of_3? = fn(n) -> rem(n, 3) == 0 end
iex> for n <- 0..5, multiple_of_3?.(n), do: n * n
[0, 9]
理解丢弃过滤器表达式返回的所有元素false
或nil
;所有其他值都被选中。
理解通常比使用Enum
和Stream
模块中的等效函数提供更简洁的表示。此外,理解还允许给出多个生成器和过滤器。以下是一个例子,它接收目录列表并获取这些目录中每个文件的大小:
dirs = ['/home/mikey', '/home/james']
for dir <- dirs,
file <- File.ls!(dir),
path = Path.join(dir, file),
File.regular?(path) do
File.stat!(path).size
end
多个生成器也可以用来计算两个列表的笛卡尔乘积:
iex> for i <- [:a, :b, :c], j <- [1, 2], do: {i, j}
[a: 1, a: 2, b: 1, b: 2, c: 1, c: 2]
多发生器和滤波器的更高级的例子是毕达哥拉斯三元组。毕达哥拉斯三元组是一组正整数a*a + b*b = c*c
,让我们在一个文件中写一个理解triple.exs
:
defmodule Triple do
def pythagorean(n) when n > 0 do
for a <- 1..n,
b <- 1..n,
c <- 1..n,
a + b + c <= n,
a*a + b*b == c*c,
do: {a, b, c}
end
end
现在在终端上:
iex triple.exs
iex> Triple.pythagorean(5)
[]
iex> Triple.pythagorean(12)
[{3, 4, 5}, {4, 3, 5}]
iex> Triple.pythagorean(48)
[{3, 4, 5}, {4, 3, 5}, {5, 12, 13}, {6, 8, 10}, {8, 6, 10}, {8, 15, 17},
{9, 12, 15}, {12, 5, 13}, {12, 9, 15}, {12, 16, 20}, {15, 8, 17}, {16, 12, 20}]
当搜索范围很大时,上面的代码非常昂贵。此外,由于元组{b,a,c}
代表相同的毕达哥拉斯三元组{a,b,c}
,因此我们的函数会产生重复的三元组。我们可以通过引用以下生成器中的变量来优化理解并消除重复结果,例如:
defmodule Triple do
def pythagorean(n) when n > 0 do
for a <- 1..n-2,
b <- a+1..n-1,
c <- b+1..n,
a + b >= c,
a*a + b*b == c*c,
do: {a, b, c}
end
end
最后,请记住理解内部的变量赋值,无论是在生成器,过滤器还是块内,都不会在理解之外反映出来。
比特串发生器
比特串生成器也被支持,并且在需要了解比特串流时非常有用。下面的例子从一个二进制的像素列表中获得它们各自的红色,绿色和蓝色值,并将它们转换成三个元素的元组:
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}]
比特串生成器可以与“常规”可枚举生成器混合使用,并支持过滤器。
:into选项
在上面的例子中,所有的理解返回列表作为结果。然而,通过将:into
选项传递给理解,理解的结果可以被插入到不同的数据结构中。
例如,可以将:into
字符串生成器与该选项一起使用,以便轻松删除字符串中的所有空格:
iex> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>
"helloworld"
套件,地图和其他字典也可以提供给:into
选项。一般来说,:into
接受任何实现Collectable
协议的结构。
常见的使用案例:into
可以在地图中转换值,而不必触摸键:
iex> for {key, val} <- %{"a" => 1, "b" => 2}, into: %{}, do: {key, val * val}
%{"a" => 1, "b" => 4}
我们再来举一个使用流的例子。由于该IO
模块提供了流(既是Enumerable
s也是Collectable
s),所以可以使用综合来实现回声终端,该回声终端回击任何类型的upcased版本:
iex> stream = IO.stream(:stdio, :line)
iex> for line <- stream, into: stream do
...> String.upcase(line) <> "\n"
...> end
现在输入任何字符串到终端,你会看到相同的值将被打印成大写。不幸的是,这个例子也让你的IEx shell陷入了理解,所以你需要点击Ctrl+C
两次才能摆脱它。:)