Elixir入门教程-速构

  1. 生成器和过滤器
  2. 位串生成器
  3. :into选项

在Elixir中,遍历一个枚举类型数据,过滤其中一些值并且将值映射到另一个列表里这种做法是很平常的。速构是对于这种数据结构的语法糖:Elixir里将这种语法组织成 for 的格式。

例如,我们可以将一个整数列表的每个值都求平方:

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

一个速构有三部分组成:生成器、过滤器和收集动作。

生成器和过滤器

上面的表达式中,n <- [1, 2, 3, 4] 是生成器。它逐个生成在速构中被使用的值。任何枚举类型数据都可以置于生成器表达式的右边:

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

生成器表达式的左边也支持模式匹配;所有没有匹配到的数据被忽略。想象一下,我们用一个关键字列表而不是一个范围类型数据,键有 :good 和 :bad ,而我们只是想计算 :good 的值的平方:

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

除了生成器使用模式匹配,过滤器可以被用来选择一些特定的元素。例如,我们可以选择是3的倍数的值而丢弃其他的值:

1
2
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模块里的函数更加简洁的描述。而且,速构也运行有多个生成器和过滤器。这里有一个例子,接收一个目录组成的列表,然后获得这些目录里每一个文件的大小:

1
2
3
4
5
6
7
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

多生成器也可以被用来计算两个列表的笛卡儿积:

1
2
iex> for i <- [:a, :b, :c], j <- [1, 2], do: {i, j}
[a: 1, a: 2, b: 1, b: 2, c: 1, c: 2]

一个更高级的多生成器和多过滤器的例子是毕达哥拉斯三元数组。一个毕达哥拉斯三元数组是满足形如 aa + bb = c*c 这样等式的一个正整数集合,让我们来写一个速构到名为triple.exs的文件里:

1
2
3
4
5
6
7
8
9
10
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

接着在终端里运行:

1
iex triple.exs
1
2
3
4
5
6
7
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}表示的是相同的毕达哥拉斯三元数组,我们的函数产生了重复的结果。我们可以通过在后续的生成器里从前面的生成器引用变量来优化速构并排除重复的结果,例如:

1
2
3
4
5
6
7
8
9
10
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 <= n,
a*a + b*b == c*c,
do: {a, b, c}
end
end

最后,要记住,速构里的,在生成器、过滤器或者内部代码块里赋值的变量,不会反映到速构外面去。

位串生成器

位串生成器也是被支持的,并且当你需要解析位串流的时候是非常有用的。下面的例子里,从一个二进制数据里接收一个由像素组成的列表,数据分别代表红、绿和蓝的值,然后将每个像素转换为三个元素的元组:

1
2
3
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 选项来很容易地删除一个字符串里的所有空格:

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

集合、映射和其他字典类型数据也可以赋给 :into 选项。总而言之,:into 接收任何实现了 Collectable 协议的数据结构。

:into常见用例是转换映射里的值,而不需要理会它的键:

1
2
iex> for {key, val} <- %{"a" => 1, "b" => 2}, into: %{}, do: {key, val * val}
%{"a" => 1, "b" => 4}

让我们用流来做另一个例子。因为IO模块提供流(就是实现了Enumerable 和 Collectable 协议),那么一个回响终端,即无论输入的是什么都将其大写后返回的终端,可以使用速构来实现:

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

现在,输入任何字符串到终端里,你将看到大写化后的相同字符串打印在终端里。不幸的是,这个例子也使得你的IEx停在速构里,所以你要按两次 Ctl+C来退出。:)

原文链接: http://elixir-lang.org/getting-started/comprehensions.html