Elixir 不提供循环构造。相反,我们利用递归和高级函数来处理集合。本章将探讨前者。
通过递归进行循环
由于不可变性,Elixir 中的循环(与任何函数式编程语言一样)的编写方式与命令式语言不同。例如,在 C 等命令式语言中,可以这样写:
for(i = 0; i < sizeof(array); i++) {
array[i] = array[i] * 2;
}
在上面的例子中,我们同时改变了数组和变量 i。但是,Elixir 中的数据结构是不可变的。因此,函数式语言依赖于递归:递归调用函数,直到达到停止递归操作继续的条件。在此过程中不会改变任何数据。考虑下面的示例,该示例打印任意次数的字符串:
defmodule Recursion do
def print_multiple_times(msg, n) when n > 0 do
IO.puts(msg)
print_multiple_times(msg, n - 1)
end
def print_multiple_times(_msg, 0) do
:ok
end
end
Recursion.print_multiple_times("Hello!", 3)
# Hello!
# Hello!
# Hello!
:ok
与 case 类似,函数可能有许多子句。当传递给函数的参数与子句的参数模式匹配并且其保护评估为真时,将执行特定子句。
当上述示例中首次调用 print_multiple_times/2 时,参数 n 等于 3。
第一个子句有一个保护,它表示“当且仅当 n 大于 0 时才使用此定义”。由于情况如此,它会打印消息,然后调用自身并将 n - 1 (2) 作为第二个参数传递。
现在我们再次执行相同的函数,从第一个子句开始。假设第二个参数 n 仍然大于 0,我们打印消息并再次调用自身,现在将第二个参数设置为 1。然后我们最后一次打印消息并调用 print_multiple_times("Hello!", 0),再次从顶部开始。
当第二个参数为零时,保护 n > 0 的计算结果为 false,并且第一个函数子句将不会执行。然后 Elixir 继续尝试下一个函数子句,该子句明确匹配 n 为 0 的情况。此子句也称为终止子句,通过将消息参数分配给 _msg 变量来忽略它并返回原子 :ok。
最后,如果您传递的参数与任何子句都不匹配,Elixir 会引发 FunctionClauseError:
iex> Recursion.print_multiple_times "Hello!", -1
** (FunctionClauseError) no function clause matching in Recursion.print_multiple_times/2
The following arguments were given to Recursion.print_multiple_times/2:
# 1
"Hello!"
# 2
-1
iex:1: Recursion.print_multiple_times/2
归约和映射算法
现在让我们看看如何使用递归的力量来对数字列表求和:
defmodule Math do
def sum_list([head | tail], accumulator) do
sum_list(tail, head + accumulator)
end
def sum_list([], accumulator) do
accumulator
end
end
IO.puts Math.sum_list([1, 2, 3], 0) #=> 6
我们使用列表 [1, 2, 3] 和初始值 0 作为参数来调用 sum_list。我们将尝试每个子句,直到找到一个符合模式匹配规则的子句。在本例中,列表 [1, 2, 3] 与 [head | tail] 匹配,将 head 绑定到 1,将 tail 绑定到 [2, 3]; accumulator 设置为 0。
然后,我们将列表的头部添加到累加器 head + accumulator 中,并再次递归调用 sum_list,将列表的尾部作为其第一个参数传递。尾部将再次匹配 [head | tail],直到列表为空,如下所示:
sum_list [1, 2, 3], 0
sum_list [2, 3], 1
sum_list [3], 3
sum_list [], 6
当列表为空时,它将匹配最后一个子句,该子句返回最终结果 6。
获取列表并将其缩减为一个值的过程称为缩减算法,是函数式编程的核心。
如果我们想将列表中的所有值加倍怎么办?
defmodule Math do
def double_each([head | tail]) do
[head * 2 | double_each(tail)]
end
def double_each([]) do
[]
end
end
$ iex math.exs
iex> Math.double_each([1, 2, 3]) #=> [2, 4, 6]
这里我们使用递归遍历列表,将每个元素加倍并返回一个新列表。获取列表并对其进行映射的过程称为映射算法。
递归和尾调用优化是 Elixir 的重要组成部分,通常用于创建循环。但是,在 Elixir 中编程时,您很少会使用上述递归来操作列表。
我们将在下一章中看到的 Enum 模块已经为使用列表提供了许多便利。例如,上述示例可以写成:
iex> Enum.reduce([1, 2, 3], 0, fn x, acc -> x + acc end)
6
iex> Enum.map([1, 2, 3], fn x -> x * 2 end)
[2, 4, 6]
让我们更深入地了解 Enumerable,顺便了解一下它的惰性对应物 Stream。