用递归实现循环
由于不可修改的特性,Elixir里的循环(如任何其他函数式语言原因)和命令式语言的写法不同。例如,在命令式语言,如C里,循环的写法如下:
|
|
在上述例子里,我们修改数组和变量i。在Elixir里修改变量值是不可能的。相反,函数式语言依赖于递归:一个函数被递归地调用,直到一个条件符合了才停止递归动作继续进行。在这个过程里面,没有数据被修改。思考一下下面的例子,打印一个字符串任意次数:
|
|
和case语句相似,一个函数可能有多个分支。当传递给函数的参数匹配某个分支的参数模式,并且这个分支的卫语句结果为true,那么这个特定的分支将被执行。
当上述例子里的 print_multiple_times/2 函数刚开始被调用的时候,参数 n 等于3。
第一个分支有一个卫语句,它说,“当且仅当n小于等于1的时候使用这个分支”。因为一开始的时候n等于3,这个条件不成立,则Elixir处理下一个分支定义。
第二个分支定义匹配这个模式并且没有卫语句,因此它将被执行。它首先打印我们的 msg 变量,然后传递 n - 1 (2)作为第二个入参并调用自己。
我们的 msg 变量被打印并且 print_multiple_times/2 被再次调用,这次第二个入参为1。因为n现在为1,print_multiple_times/2 的第一个分支定义的卫语句结果为true,那么我们执行这个特定的分支定义。msg被打印,并且没有其他需要执行的。
我们这样定义 print_multiple_times/2 ,不管传递的第二个参数是什么数值,它可能触发我们第一个分支(也就是基准条件),或者它出发我们第二个分支,它将确保我们离我们的基准条件更近一步。
reduce 和 map 算法
现在让我们看看我们如何利用递归的能力来对数字列表进行求和:
|
|
我们用列表[1, 2, 3]和初始值0作为入参来调用 sum_list 。我们将尝试每一个分支直到我们找到一个分支,根据模式匹配规则它匹配了。在这个场景下,列表[1, 2, 3]匹配了[head | tail],它绑定了head为1,tail为[2, 3],accumulator为0。
然后,我们将列表的头元素加到累加器里:head + accumulator,并且传递列表尾部作为第一个参数,再次递归地调用 sum_list 。这个列表尾部再一次匹配 [head | tail],这个情况一直到列表为空为止。参见如下演示:
|
|
当列表为空,它将匹配最后一个分支,然后返回最后结果为6。
将列表分解为一个值的过程称为归约算法,是函数编程的核心。
如果我们想将我们列表的所有元素值都翻倍,我们该如何做呢?
|
|
|
|
|
|
在这里我们用递归遍历列表,将其每个元素值翻倍,并且返回一个新的列表。获取列表并在其上映射的过程称为映射算法。
递归和尾调用是Elixir里重要的部分并且也是通常用来创建循环的方法。然后,当你在Elixir里编程,你将几乎很少像上面例子那样使用递归来操作列表。
我们将在下一章看到的 Enum模块 已经提供了许多操作列表的便捷方式。例如,上述例子可以按如下方式来写:
|
|
或者使用捕获语法:
|
|
让我们更深入地了解可枚举的数据类型:Enumerable,以及它的懒惰对应数据类型:Steam。