Elixir学习笔记——递归

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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值