当前位置: 首页 > 后端技术 > Python

Python函数式编程系列001:无副作用

时间:2023-03-25 20:19:01 Python

这篇博客的目的本来是讨论数据(用\(\tau\)表示)和函数式编程/计算机科学(用\(\lambda\))主题。但实际上,这篇博客并没有写任何关于函数式编程的东西,显得有点“名不副实”。近年来,在一些项目中和自己理论学习的实践中,对函数式编程有了一些感悟。我希望通过这个系列,将函数式编程的一些非常有用的方法传授给大家,并督促自己去思考和学习这方面的东西。当然,有介绍函数式编程的好博客/文章或书籍,但是由于Python对函数式编程的支持不是很好(比如递归加速),Python中函数式编程的例子往往是基于functools、itertools的介绍,以及一些递归的概念。所以本系列博客试图弥补这一不足,拓宽我们的视野,让我们在哲学、范畴研究等领域加入内容,进一步对编程本身进行“元”思考。什么是函数当然,作为本系列的第一篇文章,我们将讨论“无副作用”的概念。首先,我们要回到一个思考,即Python的function(s)是什么概念。讨论函数最好的当然是分析哲学的先驱弗雷泽(详见下文引文)。但是我们具体抽象一下,数学函数有明确的定义,即“一个自变量映射到只有一个因变量”。也就是说,如果函数反复传入相同的值,结果应该是一致的。比如下面这个例子,无论输入多少次x=1,都可以返回2的结果。deff(x):returnx+1也有书籍(如参考资料中提到的F??unctionalProgramminginScala)也将这一原则表述为“符号替换原则”,即我们可以使用声明的等式函数替换成下面的公式。这也是通过扩展判断是否为函数的例子,例如:deff(x):returnx+1defg(x):returnf(x)**2在这个例子中,我们可以使用如下code改原理实现。这是数学定义函数的最好例子。defg(x):return(x+1)**2我们把上面这些明显符合数学定义的函数称为“无副作用”,因为它们的计算只涉及计算自身的概念,所有的符号都只是A指示值/功能,没有其他含义。其实是Python中的函数,但实际上Python总是允许我们定义一些不符合上述规范的东西,但在Python术语中仍然被称为函数,比如下面这个全局变量的例子:a=1deff(x):globalaa+=xreturna当我们两次传入x=1时,结果第一次是2,第二次是3。这不符合我们函数的定义,原因是它改变了函数外的一个变量a,所以改变了一些计算以外的东西,所以我们说它有“副作用”。第二个原因涉及可变变量。其实上面的a也是可变变量的一个例子。但是比较突出的是list、dict等可变变量或者就地操作的值,比如下面这个例子:deff(ls,a):ls.append(a)returnls在这个例子中,有是没有引用操作全局变量,但是当我们传入ls=[1,2,3]和a=1的时候,还是发现每次传入的返回值还是不一样的。我们也可以把这种变化表达为好像有副作用。如果我们只是想得到ls添加一个元素的结果,那么这个问题就很严重了。在SCIP(StructureandInterpretationofComputerPrograms)一书中,对这两种思想在处理功能上的差异有很好的概述。在前面的“无副作用”的例子中,a、f之类的东西只有表示一个值/函数的意思;但对于后者具有“副作用”概念的功能,我们必须构建一个“环境”的概念。在这个环境中,有房子(在计算机中,可能是内存/CPU缓存的概念),a和f指的是房子(但有时指的是房子里的东西),更可怕的是那房子的大小也会改变;在“无副作用”的例子中,我们只需要知道符号总是指代一个东西,指定一次后就不会改变,也不需要换房间。副作用的好与坏下面,我们就来看看“无副作用”和“有副作用”的优缺点是什么。1.回溯问题如果一个函数对于某个输入有一定的输出,那么说明我们很容易发现问题,定位问题,重现问题。而如果一个程序包含了很多“副作用”,那就意味着我们无法控制它在函数之外修改了什么,小到变量函数,大到计算机的环境变量。这也导致了为什么,很多程序的错误反馈都要打印这么多环境变量、计算机环境等概念。而没有副作用意味着非常强的“可测试性”,我们会在后面的文章中一一列举。此外,“基于属性的测试”也是可能的。换句话说,我们可以更有力地控制我们的程序。即使对于静态函数式语言(不幸的是Python不是),编译阶段也可以暴露并解决大部分问题。2.不能与环境交互的程序最有可能是无用的,只是简单地使用函数概念。实际上,我们构建的是一个逻辑符号操作系统。如果它不与外部环境相互作用,它就是建筑物的玩具。甚至我们对print的使用也在函数之外产生了屏幕的副作用。因此,没有副作用意味着它的应用是困难的。当然,“monads”的概念和将副作用限制在一个很小的范围内,这些方法让我们对自己的程序有非常强的把握,也有一定的“交互自由度”。这也是我们在这个系列中要强调的编程思想3.效率其实从上面的例子中,我们已经可以看出计算机(/图灵机)的概念本身就是基于环境或者副作用的。但是函数式编程是在有副作用的机器上实现的,会降低效率。更重要的是,如果我们不使用像append这样的就地操作,这意味着更多的空间和复制更多值的概念。这些都大大降低了程序的效率。此外,Python在递归等功能速度优化方面表现不佳,这也使得副作用可能更受欢迎。不过,这种效率概念可能存在一些更模糊的地方。如果从更大的角度看函数式编程,有各种适合自己的优化方案,后面会一一介绍。4.表达能力(新)在上面关于“环境”和“代入”的讨论中,我们也发现如果使用“环境”这个概念,我们会有更多的概念,比如“传值”和“传值”地址”。、“可变变量”、“全局变量”等概念,而这些概念一定是语言固有的。在一定程度上,这是表达效率低下的表现。事实上,函数式编程仅仅依靠值和函数这两个概念,再加上基本的类型和操作就可以实现几乎所有的东西(或者图灵完备)。而我们后面提到的“递归”、“monad”等概念在某种程度上是“派生的”而不是“内生的”。这是Top-down数学比较有特点的(当然,数学是不是这样就是另外一个问题了)。关于域的说明最后,我们想提一个关于“域”的小问题,我在上面的讨论中没有提到,因为我们稍微用到了类型/域的概念。比如下面这个函数(我特地加了类型注解):deff(x:int)->int:ifx>0:returnx+1在这个例子中,\(x\)的取值范围只能是就是\(x>0\)(虽然Python在特殊情况下会输出None),但实际上这种运算与数学中的函数略有不同,因为它在声明时的定义域是int。定义域是\(x\inN\)。并非所有int中的值都未被使用。在scala或者haskell等函数支持较好的语言中,这种函数也被称为PartialFunction(注意和currying中的PartialAppliedFunction的区别),意思是不是域的所有变量都声明之后,对于例如,在scala中定义上面的函数f会使用PartialFunction:valf:PartialFunction[Int,Int]={casexifx>0=>x+1}但是在实际使用中,我们更喜欢用它来表达基本的“无副作用”的函数式编程的特点和性质,所以这种详细的讨论在大多数场合都被忽略了,但你应该注意数学表达式。参考文献张翠园.论弗雷格的《函数与概念》。现代传播14(2018)。Chiusano、Paul和RunarBjarnason。Scala中的函数式编程。Simon和Schuster,2014年。Abelson、Harold和GeraldJaySussman。计算机程序的结构和解释。麻省理工学院出版社,1996年。