小贴士和技巧gydF4y2Ba

功能“控制流”-编写没有循环的程序gydF4y2Ba

控制流的函数式编程特性概述——没有循环和if-elsegydF4y2Ba

回顾gydF4y2Ba

在我之前的帖子上gydF4y2Ba函数式编程的主要原则gydF4y2Ba,我解释了函数式编程范式与命令式编程的不同之处,并讨论了等幂和避免副作用的概念如何与在函数式编程中实现等式推理的引用透明属性联系在一起。gydF4y2Ba

在深入研究函数式编程的一些特性之前,让我们先从我编写Scala代码的前3个月的个人轶事开始。gydF4y2Ba

函数式代码中没有“If-Else”gydF4y2Ba

我在写一个纯粹的ScalgydF4y2Ba一个gydF4y2Ba函数用于自定义Spark UDF,该函数基于JSON字符串表示的自定义分层调整计算收入调整。当我试图用纯函数式代码表达业务逻辑时(因为这是团队的编码风格),我感到非常沮丧,因为我在代码中引入了“if-else”逻辑,以“完成工作”。gydF4y2Ba

我们只能说,在对那个特定的合并请求进行代码审查时,我学到了一个非常艰难的教训。gydF4y2Ba

“函数式代码中没有if-else,这不是命令式编程……gydF4y2Ba没有如果,没有其他。gydF4y2Ba”gydF4y2Ba

如果没有“if-else”,我们如何在函数式编程中编写“控制流”?gydF4y2Ba

简单的回答是:gydF4y2Ba函数组合gydF4y2Ba.gydF4y2Ba

长话短说:函数组合和函数式数据结构的组合。gydF4y2Ba

由于对每种功能设计模式的深入研究可能相当冗长,因此本文的重点是概述函数组合,以及它如何实现更直观的数据管道设计方法。gydF4y2Ba

函数组合简介gydF4y2Ba

函数组成(图片来自作者)gydF4y2Ba

在数学中,gydF4y2Ba函数组合gydF4y2Ba一个操作需要两个函数吗gydF4y2BafgydF4y2Ba而且gydF4y2BaggydF4y2Ba并形成一个复合函数gydF4y2BahgydF4y2Ba这样gydF4y2BaH (x) = g(f(x))gydF4y2Ba——函数gydF4y2BaggydF4y2Ba是否应用于应用函数的结果gydF4y2BafgydF4y2Ba泛型输入gydF4y2BaxgydF4y2Ba.在数学上,这个操作可以表示为:gydF4y2Ba

f: X→Y, g: Y→Z⟹g◦f: X→ZgydF4y2Ba

在哪里gydF4y2BaG◦fgydF4y2Ba是复合函数。gydF4y2Ba

直观地说,复合函数映射gydF4y2BaxgydF4y2Ba在gydF4y2BaXgydF4y2Ba来gydF4y2Bag (f (x))gydF4y2Ba在域gydF4y2BaZgydF4y2Ba对于定义域内的所有值gydF4y2BaXgydF4y2Ba.gydF4y2Ba

说明函数组合概念的一个有用类比是用一片面包和冷黄油在烤箱中制作黄油吐司。有两种可能的操作:gydF4y2Ba

  1. 烤箱烤(操作f)gydF4y2Ba
  2. 在最宽的表面涂黄油(操作g)gydF4y2Ba

如果我们先在烤箱里烤面包,然后在烤箱出来的面包最宽的表面涂上冷黄油,我们就得到了一片烤面包gydF4y2Ba冷黄油酱gydF4y2Ba(gydF4y2BaG◦fgydF4y2Ba).gydF4y2Ba

如果我们先在面包最宽的表面涂上冷黄油,然后在烤箱里用冷黄油烤面包,我们就得到了一片烤面包gydF4y2Ba温黄油酱gydF4y2Ba(gydF4y2BaF◦ggydF4y2Ba).我们知道gydF4y2Ba"冷黄油酱" != "热黄油酱"gydF4y2Ba.gydF4y2Ba

从这些例子中,我们可以直观地推断出函数应用的顺序在函数组合中很重要。(gydF4y2BaG◦f≠f◦GgydF4y2Ba)gydF4y2Ba

类似地,在设计数据管道时,我们经常通过将函数应用于其他函数的结果来编写数据转换。鼓励组合函数的能力gydF4y2Ba重构gydF4y2Ba的重复代码段的函数gydF4y2Ba可维护性gydF4y2Ba而且gydF4y2Ba可重用性gydF4y2Ba.gydF4y2Ba

作为一类对象的函数gydF4y2Ba

函数式编程的核心思想是:gydF4y2Ba函数是值gydF4y2Ba.gydF4y2Ba

这个特征意味着一个函数可以是[2,3]:gydF4y2Ba

  1. 赋值给一个变量gydF4y2Ba
  2. 作为参数传递给其他函数gydF4y2Ba
  3. 作为其他函数的值返回gydF4y2Ba

为此,函数必须是运行时环境中的一级对象(并存储在数据结构中)——就像数字、字符串和数组一样。所有函数式语言(包括Scala)以及一些解释型语言(如Python)都支持一级函数。gydF4y2Ba

高阶函数gydF4y2Ba

函数作为一类对象的概念所产生的一个关键含义是,函数组合可以自然地表示为gydF4y2Ba高阶函数gydF4y2Ba.gydF4y2Ba

高阶函数至少具有以下属性之一:gydF4y2Ba

  1. 接受函数作为参数gydF4y2Ba
  2. 将函数作为值返回gydF4y2Ba

高阶函数的一个例子是gydF4y2Ba地图gydF4y2Ba.gydF4y2Ba

当我们查看Python内置函数的文档时gydF4y2Ba地图gydF4y2Ba,有人说gydF4y2Ba地图gydF4y2BaFunction接受另一个函数和一个可迭代对象作为输入参数,并返回一个迭代器,其结果为[4]。gydF4y2Ba

在Scala中,包中的每个集合类gydF4y2Bascala.collectionsgydF4y2Ba它的子集包含gydF4y2Ba地图gydF4y2Ba在ScalaDoc[5]上由以下函数签名定义的方法:gydF4y2Ba

def map[B](f: (A) => B): Iterable[B] //用于集合类gydF4y2Ba
def map[B](f: (A) => B): Iterator[B] //用于访问集合元素的迭代器gydF4y2Ba

函数签名的意思是gydF4y2Ba地图gydF4y2Ba接受一个函数输入参数gydF4y2BafgydF4y2Ba,gydF4y2BafgydF4y2Ba转换类型的泛型输入gydF4y2Ba一个gydF4y2Ba类型的结果值gydF4y2BaBgydF4y2Ba.gydF4y2Ba

若要对整数集合中的每个值进行平方,可以使用gydF4y2Ba迭代方法gydF4y2Ba是遍历集合中的每个元素,对元素进行平方,并将结果附加到结果集合中,结果集合的长度随着每次迭代而扩展。gydF4y2Ba

  • 在Python中:gydF4y2Ba
def广场(x):gydF4y2Ba
返回x * xgydF4y2Ba

def主要(args):gydF4y2Ba
集合= [1,2,3,4,5]gydF4y2Ba
#初始化列表保存结果gydF4y2Ba
Squared_collection = []gydF4y2Ba
#循环直到集合结束gydF4y2Ba
对于collection中的num:gydF4y2Ba
#平方当前数字gydF4y2Ba
平方=平方(num)gydF4y2Ba
#添加结果到列表中gydF4y2Ba
squared_collection.append(平方)gydF4y2Ba

打印(squared_collection)gydF4y2Ba

在迭代方法中,在循环中的每次迭代中会发生两个状态变化:gydF4y2Ba

  1. 的gydF4y2Ba的平方gydF4y2Ba方法返回的结果gydF4y2Ba广场gydF4y2Ba函数;而且gydF4y2Ba
  2. 保存平方函数结果的集合。gydF4y2Ba

执行相同的操作gydF4y2Ba功能的方法gydF4y2Ba(即不使用可变变量),则gydF4y2Ba地图gydF4y2Ba函数可用于将集合中的每个元素“映射”到与输入集合具有相同数量元素的新集合—通过对每个元素应用平方操作并将结果收集到新集合中。gydF4y2Ba

  • 在Python中:gydF4y2Ba
def广场(x):gydF4y2Ba
返回x * xgydF4y2Ba

def主要(args):gydF4y2Ba

集合= [1,2,3,4,5]gydF4y2Ba
square = list(map(square, collection))gydF4y2Ba
打印(平方)gydF4y2Ba
  • 在Scala中:gydF4y2Ba
对象MapSquare {gydF4y2Ba

def square(x: Int): Int = {gydF4y2Ba
X * XgydF4y2Ba
}gydF4y2Ba

def main(args: Array[String]) {gydF4y2Ba

val collection = List[1,2,3,4,5]gydF4y2Ba
Val平方= collection.map(square)gydF4y2Ba
println(平方)gydF4y2Ba
}gydF4y2Ba
}gydF4y2Ba

在这两个实现中,gydF4y2Ba地图gydF4y2Ba函数接受一个输入函数,该输入函数应用于值集合中的每个元素,并返回包含结果的新集合。作为gydF4y2Ba地图gydF4y2Ba具有接受另一个函数作为参数的性质,它是一个高阶函数。gydF4y2Ba

关于Python和Scala实现之间的差异的一些快速注释:gydF4y2Ba

  • PythongydF4y2Ba地图gydF4y2Bavs ScalagydF4y2Ba地图gydF4y2Ba:可迭代函数,例如gydF4y2Ba列表gydF4y2Ba需要转换从PythongydF4y2Ba地图gydF4y2Ba函数转换为可迭代对象。在Scala中,不需要显式地将结果从gydF4y2Ba地图gydF4y2Ba方法中的所有方法一样,将其转换为可迭代对象gydF4y2Ba可迭代的gydF4y2Ba特征是用抽象的方法定义的,gydF4y2Ba迭代器gydF4y2Ba类的实例gydF4y2Ba迭代器gydF4y2Ba一个接一个地生成集合元素的特征[6]。gydF4y2Ba
  • 函数如何返回值gydF4y2Ba返回gydF4y2Ba关键字在Python中用于返回函数结果,即gydF4y2Ba返回gydF4y2Ba关键字在Scala中很少使用。相反,在Scala中定义函数时,函数声明中的最后一行被求值并返回结果值。事实上,使用gydF4y2Ba返回gydF4y2Ba关键字在Scala中并不是函数式编程的好实践,因为它放弃了当前的计算,并且不是引用透明的[7-8]。gydF4y2Ba

匿名函数gydF4y2Ba

在使用高阶函数时,通常能够使用函数字面量或调用输入函数参数是很方便的gydF4y2Ba匿名函数gydF4y2Ba无需在高阶函数中使用之前将它们定义为命名函数对象。gydF4y2Ba

在Python中,匿名函数也称为gydF4y2Balambda表达式gydF4y2Ba因为它们起源于lambda微积分。方法创建匿名函数gydF4y2BaλgydF4y2Ba关键字并包装单个表达式而不使用gydF4y2BadefgydF4y2Ba或gydF4y2Ba返回gydF4y2Ba关键词。例如,gydF4y2Ba广场gydF4y2Ba中的匿名函数可以表示为gydF4y2Ba地图gydF4y2Ba函数,其中lambda表达式gydF4y2Bax: x * xgydF4y2Ba用作函数输入参数gydF4y2Ba地图gydF4y2Ba:gydF4y2Ba

def主要(args):gydF4y2Ba

Collection =[1,2,3,4,5]²= map(lambda x: x * x, Collection)gydF4y2Ba
打印(平方)gydF4y2Ba

在Scala中,匿名函数的定义与gydF4y2Ba= >gydF4y2Ba符号——函数参数定义在gydF4y2Ba= >gydF4y2Ba的右侧定义了函数表达式gydF4y2Ba= >gydF4y2Ba箭头。例如,gydF4y2Ba广场gydF4y2Ba的函数可以表示为匿名函数gydF4y2Ba(x: Int) => x * xgydF4y2Ba语法并用作函数的输入参数gydF4y2Ba地图gydF4y2Ba:gydF4y2Ba

对象MapSquareAnonymous {gydF4y2Ba

def main(args: Array[String]) {gydF4y2Ba
val collection = List[1,2,3,4,5]gydF4y2Ba
Val平方=集合。map((x: Int) => x * x)gydF4y2Ba
println(平方)gydF4y2Ba
}gydF4y2Ba
}gydF4y2Ba

在高阶函数中使用匿名函数的一个关键好处是,一次性使用的单表达式函数不需要显式地包装在命名函数定义中,因此gydF4y2Ba优化代码行gydF4y2Ba而且gydF4y2Ba提高代码可维护性gydF4y2Ba.gydF4y2Ba

递归是“函数迭代”的一种形式gydF4y2Ba

递归gydF4y2Ba是自我参照的一种形式吗gydF4y2Ba函数组合gydF4y2Ba递归函数接受自身(较小实例)的结果,并将它们作为自身另一个实例的输入。为了防止递归调用的无限循环,可以使用gydF4y2Ba基本情况gydF4y2Ba作为不使用递归返回结果的终止条件。gydF4y2Ba

递归的一个经典例子是阶乘函数,它被定义为所有小于或等于整数的正整数的乘积gydF4y2BangydF4y2Ba:gydF4y2Ba

n != n⋅(n-1)⋅(n-2)⋅⋯3⋅2⋅1gydF4y2Ba

实现阶乘函数有两种可能的迭代方法:使用gydF4y2Ba为gydF4y2Ba循环,使用gydF4y2Ba而gydF4y2Ba循环。gydF4y2Ba

  • 在Python中:gydF4y2Ba
def factorial_for (n):gydF4y2Ba
#初始化变量保存阶乘gydF4y2Ba
事实= 1gydF4y2Ba
#循环从n到1,减1gydF4y2Ba
对于范围为(n, 1, -1)的num:gydF4y2Ba
#将当前数字与当前产品相乘gydF4y2Ba
事实=事实* numgydF4y2Ba
返回的事实gydF4y2Ba

def factorial_while (n):gydF4y2Ba
#初始化变量保存阶乘gydF4y2Ba
事实= 1gydF4y2Ba
#循环直到n达到1gydF4y2Ba
当n >= 1时:gydF4y2Ba
#将当前数字与当前产品相乘gydF4y2Ba
事实=事实* ngydF4y2Ba
#减去1gydF4y2Ba
N = N - 1gydF4y2Ba
返回的事实gydF4y2Ba

在阶乘函数的两个迭代实现中,在循环中的每次迭代中都会发生两个状态变化:gydF4y2Ba

  1. 存储当前乘积的阶乘变量;而且gydF4y2Ba
  2. 数字相乘。gydF4y2Ba

实现阶乘函数使用gydF4y2Ba功能的方法gydF4y2Ba,递归在将问题划分为相同类型的子问题时是有用的——在这种情况下,是的乘积gydF4y2BangydF4y2Ba而且gydF4y2Ba(n - 1) !gydF4y2Ba.gydF4y2Ba

阶乘函数的基本递归方法是这样的:gydF4y2Ba

  • 在Python中:gydF4y2Ba
def阶乘(n):gydF4y2Ba
#基本情况返回值gydF4y2Ba
n <= 0:返回1gydF4y2Ba
使用另一组输入调用#递归函数gydF4y2Ba
返回n *阶乘(n-1)gydF4y2Ba
  • 在Scala中:gydF4y2Ba
def阶乘(n: Int): Long = {gydF4y2Ba
If (n <= 0) 1 else n *阶乘(n-1)gydF4y2Ba
}gydF4y2Ba

对于基本的递归方法,5的阶乘以以下方式计算:gydF4y2Ba

阶乘(5)gydF4y2Ba
If (5 <= 0) 1 else 5 *阶乘(5 - 1)gydF4y2Ba
5 * factorial(4) // factorial(5)被添加到调用堆栈中gydF4y2Ba
5 *(4 *阶乘(3))//阶乘(4)被添加到调用堆栈gydF4y2Ba
5 *(4 *(3 *阶乘(2)))//阶乘(3)被添加到调用堆栈gydF4y2Ba
5 *(4 *(3 *(2 *阶乘(1))))//阶乘(2))被添加到调用堆栈gydF4y2Ba
将5 *(4 *(3 *(2 *(1 *阶乘(0)))))//阶乘(1))添加到调用堆栈gydF4y2Ba
5 * (4 * (3 * (2 * (1 * 1))))) // factorial(0)返回1到factorial(1)gydF4y2Ba
5 * (4 * (3 * (2 * 1))) // factorial(1)返回1 * factorial(0) = 1到factorial(2)gydF4y2Ba
5 * (4 * (3 * 2)) // factorial(2)返回2 * factorial(1) = 2到factorial(3)gydF4y2Ba
5 * (4 * 6) // factorial(3)返回3 * factorial(2) = 6到factorial(4)gydF4y2Ba
5 * 24 // factorial(4)返回4 * factorial(3) = 24到factorial(5)gydF4y2Ba
120 // factorial(5)返回5 * factorial(4) = 120到全局执行上下文gydF4y2Ba

为gydF4y2BaN = 5gydF4y2Ba,阶乘函数的计算涉及6次对阶乘函数的递归调用,包括基本情况。gydF4y2Ba

虽然与迭代方法相比,基本的递归方法更接近于它的定义(也更自然)地表达阶乘函数,但它也使用了更多的内存,因为每个函数调用都作为堆栈帧推入调用堆栈,并在函数调用返回值时从调用堆栈中取出。gydF4y2Ba

对于较大的值gydF4y2BangydF4y2Ba,递归会随着对自身的更多函数调用而变得更深,并且必须为调用堆栈分配更多的空间。当存储函数调用所需的空间超过调用堆栈的容量时,将使用agydF4y2Ba堆栈溢出gydF4y2Ba发生!gydF4y2Ba

尾递归和尾调用优化gydF4y2Ba

为了防止无限递归导致堆栈溢出和程序崩溃,必须对递归函数进行一些优化,以减少调用堆栈中堆栈帧的消耗。优化递归函数的一种可能的方法是将其重写为gydF4y2Ba尾递归gydF4y2Ba函数。gydF4y2Ba

尾部递归函数递归地调用自身,并且在递归调用返回后不执行任何计算。函数调用是gydF4y2Ba尾部调用gydF4y2Ba当它只返回函数调用的值时。gydF4y2Ba

在函数式编程语言中,比如Scala,gydF4y2Ba尾部调用优化gydF4y2Ba通常包含在编译器中,以识别尾部调用并将递归编译为迭代循环,迭代循环不为每次迭代消耗堆栈帧。事实上,栈帧可以用于递归函数和递归函数[1]中被调用的函数。gydF4y2Ba

通过这种优化,递归函数的空间性能可以从gydF4y2BaO (N)gydF4y2Ba来gydF4y2BaO (1)gydF4y2Ba-每个调用从一个堆栈帧到所有调用[8]的一个堆栈帧。在某种程度上,尾递归函数是一种与循环性能比较的“函数迭代”形式。gydF4y2Ba

例如,阶乘函数可以在Scala中以尾部递归的形式表示:gydF4y2Ba

def factorialTailRec(n: Int): Long = {gydF4y2Ba
def fact(n: Int, product: Long): Long = {gydF4y2Ba
如果(n <= 0)乘积gydF4y2Ba
Else fact(n-1, n * product)gydF4y2Ba
}gydF4y2Ba

事实(n, 1)gydF4y2Ba
}gydF4y2Ba

在Scala中,尾部调用优化是在编译期间自动执行的,而Python则不是这样。此外,Python中有一个递归限制(默认值为1000),作为防止CPython实现的C调用堆栈溢出的措施。gydF4y2Ba

接下来:高阶函数gydF4y2Ba

在这篇文章中,我们将了解:gydF4y2Ba

  1. 函数组合gydF4y2Ba
  2. 高阶函数是函数式编程的一个重要含义gydF4y2Ba
  3. 递归是“函数迭代”的一种形式gydF4y2Ba

我们找到" if-else "的替代品了吗?不完全是,但我们现在知道如何使用高阶函数和尾递归在函数式编程中编写“循环”。gydF4y2Ba

在下一篇文章中,我将更多地探讨高阶函数以及如何在设计函数式数据管道中使用它们。gydF4y2Ba

想了解更多关于我作为数据专业人员学习之旅的幕后文章吗?看看我的网站gydF4y2Bahttps://ongchinhwee.megydF4y2Ba!gydF4y2Ba

参考文献gydF4y2Ba

[1]保罗·奇乌萨诺和Rúnar比纳松,gydF4y2BaScala函数式编程gydF4y2Ba(2014)gydF4y2Ba

阿尔文·亚历山大,gydF4y2Ba“函数也是变量”gydF4y2Ba(2018),函数式编程简化gydF4y2Ba

史蒂文·f·洛特,gydF4y2Ba函数式Python编程(第二版)gydF4y2Ba) (2018)gydF4y2Ba

[4]gydF4y2Ba内置函数- Python 3.9.6文档gydF4y2Ba

[5]gydF4y2BaScala标准库2.13.6 - Scala .collections. iterablegydF4y2Ba

[6]gydF4y2BaTrait Iterable |集合| Scala文档gydF4y2Ba

[7]gydF4y2Batpolecat -不归路gydF4y2Ba

[8]gydF4y2Ba在Scala中不使用Return ?—问题—Scala用户gydF4y2Ba

迈克尔·r·克拉克森,gydF4y2Ba尾递归gydF4y2Ba(2021), OCaml函数式编程gydF4y2Ba

原载于gydF4y2Bahttps://ongchinhwee.megydF4y2Ba2021年7月4日。gydF4y2Ba

白天与DT One一起构建大规模的数据管道,晚上与技术演讲者和作家一起构建数据管道。90%是自学成才的开发人员和终身工程师。- - - - - -gydF4y2Bahttps://ongchinhwee.megydF4y2Ba

Baidu
map