Elixir是一门非常年轻的编程语言(于2011年诞生),但正逐渐普及。 我最初对这种语言很感兴趣,因为使用它时,您可以从不同的角度看程序员通常解决的一些常见任务。 例如,您可以找到如何在没有for
循环的情况下遍历集合,或者如何在没有类的情况下组织代码。
Elixir具有一些非常有趣且强大的功能,如果您来自OOP领域,可能很难理解。 但是,经过一段时间,这一切都变得有意义了,您会看到功能代码的表达能力。 理解就是这样的功能之一,本文中我将解释如何使用它们。
理解与映射
一般而言, 列表理解是一种特殊的构造,它使您可以基于现有列表创建新列表。 这种概念可以在Haskell和Clojure等语言中找到。 Erlang也介绍了它,因此Elixir也具有理解力 。
您可能会问,理解力与map / 2函数有何不同? map / 2函数也需要一个集合并产生一个新集合? 那将是一个公平的问题! 好吧,在最简单的情况下,理解几乎是同一件事。 看一下这个例子:
defmodule MyModule do
def do_something(list) do
list |>
Enum.map(fn(el) -> el * 2 end)
end
end
MyModule.do_something([1,2,3]) |> IO.inspect # => [2,4,6]
在这里,我只是简单地获取一个包含三个数字的列表,并生成一个包含所有数字乘以2
的新列表。 map
调用可以进一步简化为Enum.map( &(&1 * 2) )
。
现在可以使用理解力来重写do_something/1
函数:
def do_something(list) do
for el <- list, do: el * 2
end
这就是基本的理解,在我看来,代码比第一个示例更优雅。 在这里,我们再次从列表中获取每个元素,并将其乘以2
。 el <- list
部分称为generator ,它说明了您希望如何从集合中提取值。
请注意,我们并没有被迫将列表传递给do_something/1
函数,该代码将可用于任何可枚举的东西:
defmodule MyModule do
def do_something(collection) do
for el <- collection, do: el * 2
end
end
MyModule.do_something((1..3)) |> IO.inspect
在此示例中,我将范围作为参数传递。
理解也适用于二进制字符串。 语法略有不同,因为您需要用<<
和>>
将生成器括起来。 让我们通过制作一个非常简单的函数来“解密”用Caesar cipher保护的字符串来演示这一点。 这个想法很简单:我们将单词中的每个字母替换为一个固定位置的字母。 为了简单起见,我将移动1
位置:
defmodule MyModule do
def decipher(cipher) do
for << char <- cipher >>, do: char - 1
end
end
MyModule.decipher("fmjyjs") |>
IO.inspect
# => 'elixir'
除了<<
和>>
部分,这看起来与前面的示例几乎相同。 我们将字符串中每个字符的代码减一,然后构造一个字符串。 因此,加密后的消息是“ elixir”!
但是,还有更多。 理解的另一个有用功能是能够滤除某些元素。
理解与过滤
让我们进一步扩展最初的示例。 我将传递从1
到20
的整数范围,仅接受偶数元素,然后将它们乘以2
:
defmodule MyModule do
require Integer
def do_something(collection) do
collection |>
Stream.filter( &Integer.is_even/1 ) |>
Enum.map( &(&1 * 2) )
end
end
MyModule.do_something( (1..20) ) |> IO.inspect
在这里,我必须要求Integer
模块能够使用is_even/1
宏。 另外,我使用Stream
来优化代码并防止迭代执行两次。
现在,让我们再次理解以下示例:
def do_something(collection) do
for el <- collection, Integer.is_even(el), do: el * 2
end
因此,如您所见, for
可以接受一个可选的过滤器以跳过集合中的某些元素。
您不仅限于一个过滤器,因此以下代码也是合法的:
def do_something(collection) do
for el <- collection, Integer.is_even(el), el < 10, do: el * 2
end
所有偶数都将小于10
。 只是不要忘了用逗号分隔过滤器。
将针对集合的每个元素对过滤器进行评估,如果评估返回true
,则执行该块。 否则,将采用新元素。 有趣的是,生成器还可以通过使用when
来过滤元素:
def do_something(collection) do
for el when el < 10 <- collection, Integer.is_even(el), do: el * 2
end
这与编写保护子句时的操作非常相似:
def do_something(x) when is_number(x) do
# ...
end
对多个集合的理解
现在,假设我们一次没有一个,而是两个,我们想产生一个新的集合。 例如,从第一个集合中获取所有偶数,从第二个集合中获取奇数,然后将它们相乘:
defmodule MyModule do
require Integer
def do_something(collection1, collection2) do
for el1 <- collection1, el2 <- collection2, Integer.is_even(el1), Integer.is_odd(el2),
do: el1 * el2
end
end
MyModule.do_something( (1..20), (5..10) ) |> IO.inspect
此示例说明了理解一次可以处理多个集合。 将采用collection1
的第一个偶数并将其乘以collection2
每个奇数。 接下来,将取并乘以collection1
的第二个偶数整数,依此类推。 结果将是:
[10, 14, 18, 20, 28, 36, 30, 42, 54, 40, 56, 72, 50, 70, 90, 60, 84, 108, 70,
98, 126, 80, 112, 144, 90, 126, 162, 100, 140, 180]
而且,结果值不必为整数。 例如,您可以返回一个元组,其中包含来自第一个和第二个集合的整数:
defmodule MyModule do
require Integer
def do_something(collection1, collection2) do
for el1 <- collection1, el2 <- collection2, Integer.is_even(el1), Integer.is_odd(el2),
do: {el1,el2}
end
end
MyModule.do_something( (1..20), (5..10) ) |> IO.inspect
# => [{2, 5}, {2, 7}, {2, 9}, {4, 5}...]
对“进入”选项的理解
到目前为止,我们理解力的最终结果始终是列表。 实际上,这也不是强制性的。 您可以指定一个into
参数,该参数接受一个包含结果值的集合。
该参数接受任何实现Collectable协议的结构,因此例如,我们可以生成如下图:
defmodule MyModule do
require Integer
def do_something(collection1, collection2) do
for el1 <- collection1, el2 <- collection2,
Integer.is_even(el1), Integer.is_odd(el2),
into: Map.new,
do: {el1,el2}
end
end
MyModule.do_something( (1..20), (5..10) ) |> IO.inspect
# => %{2 => 9, 4 => 9, 6 => 9...}
在这里,我只是简单地说into: Map.new
,也可以替换into: %{}
。 通过返回{el1, el2}
元组,我们基本上将第一个元素设置为键,将第二个元素设置为值。
但是,此示例并不是特别有用,因此让我们生成一个以数字为键,其平方为值的地图:
defmodule MyModule do
def do_something(collection) do
for el <- collection, into: Map.new,
do: {el, :math.sqrt(el)}
end
end
squares = MyModule.do_something( (1..20) ) |> IO.inspect
# => %{1 => 1.0, 2 => 1.4142135623730951, 3 => 1.7320508075688772,...}
squares[3] |> IO.puts
# => 1.7320508075688772
在此示例中,我直接使用Erlang的:math
模块,因为毕竟所有模块的名称都是原子。 现在,您可以轻松找到1
到20
任何数字的平方。
理解和模式匹配
最后要提到的是,您还可以全面理解模式匹配。 在某些情况下,它可能非常方便。
假设我们有一张包含员工姓名和原始工资的地图:
%{"Joe" => 50, "Bill" => 40, "Alice" => 45, "Jim" => 30}
我想生成一个新地图,其中名称被小写并转换为原子,并且薪金是使用税率计算的:
defmodule MyModule do
@tax 0.13
def format_employee_data(collection) do
for {name, salary} <- collection, into: Map.new,
do: {format_name(name), salary - salary * @tax}
end
defp format_name(name) do
name |>
String.downcase |>
String.to_atom
end
end
MyModule.format_employee_data( %{"Joe" => 50, "Bill" => 40, "Alice" => 45, "Jim" => 30} ) |>
IO.inspect
# => %{alice: 39.15, bill: 34.8, jim: 26.1, joe: 43.5}
在此示例中,我们定义了具有任意数字的模块属性@tax
。 然后,我使用{name, salary} <- collection
-collection解构理解中的数据。 最后,格式化名称并根据需要计算薪水,然后将结果存储在新地图中。 非常简单但富有表现力。
结论
在本文中,我们看到了如何使用Elixir理解。 您可能需要一些时间来习惯它们。 这种构造确实很整洁,在某些情况下可以比map
和filter
这样的功能更好。 您可以在Elixir的官方文档和入门指南中找到更多示例。
希望您发现本教程有用且有趣! 感谢您与我在一起,很快再见。
翻译自: https://code.tutsplus.com/tutorials/how-to-work-with-elixir-comprehensions--cms-28823