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

2次阅读

共计 3391 个字符,预计需要花费 9 分钟才能阅读完成。

这个博客的目标原本是探讨数据(用 \(\tau\)示意)和函数式编程 / 计算机科学 (用 \(\lambda\) 示意)的两类主题的。但事实上,本博客还没写过任何对于 函数式编程 的内容,显得有些「徒有虚名」。而近几年在一些我的项目上和本人实践学习中的实际,对于函数式编程有了一些不大不小的洞识。心愿能借由这个系列来给大家传递一些函数式编程十分有用的办法,以及更督促本人对这方面进行思考和学习。

当然,介绍函数式编程的不错的博客 / 文章或者书籍,然而,因为 Python 在函数式编程方面的反对不算是十分好的(比方递归减速之类),所以以 Python 实现函数式编程的例子往往基于 functoolsitertools 以及一些递归概念的介绍。所以这个博客系列试图补救这方面的有余,并且将视线扩充,让咱们在哲学、领域学等畛域的内容退出进来,进一步「元」思考编程自身。

何为函数

当然,作为本系列的第一篇文章,咱们要来探讨的是「无副作用」这个概念。

首先,咱们要回到一个思考,就是 Pythonfunction(s)是一个什么概念。function探讨最好当然是剖析哲学的先驱弗雷泽(具体可参考上面的援用)。但咱们具体形象进去,数学上的函数有个明确的定义,即「一个自变量映射到惟一一个因变量」。也就是说,函数重复带入同一个值,理当这个后果是统一的。比方咱们举上面一个例子,无论咱们带入几次 x = 1都能够返回 2 的后果。

def f(x):
    return x + 1

也有书中(例如援用中提到的Functional Programming in Scala)也将这种准则示意为「符号代换准则」,即咱们齐全能够用申明函数的等式代换到上面的式子中。这个也是一个引申而来的判断是否是函数的例子,譬如:

def f(x):
    return x + 1

def g(x):
    return f(x) ** 2

这个例子中,咱们齐全能够应用上面的代换准则来实现。这个是 数学定义的函数 的最佳例子。

def g(x):
    return (x + 1) ** 2

咱们把下面这些明确合乎数学定义的函数就叫做「无副作用」的,因为它们的计算只波及到了计算本人的概念,并且所有符号,只是某一个值 / 函数的批示词,不带有别的意涵。

事实上 Python 中的函数

然而事实上 Python 中永远容许咱们定义一些不合乎下面标准,然而 Python 术语中还是叫函数的货色,比方上面一个全局变量的例子:

a = 1

def f(x):
    global a
    a += x
    return a

咱们在两次带入 x = 1 时,后果第一次后果是2,第二次是3。这就不合乎咱们函数的定义了,而究其原因,它是扭转了一个函数外的变量a,因而它除了计算之外,还扭转了什么,咱们于是说它是有「副作用」的。

第二个产生的起因波及可变变量,其实下面的 a 也是一个可变变量的例子。然而更加突出的乃是 listdict 之类的可变变量或者原地操作的值,例如上面一个例子:

def f(ls, a):
    ls.append(a)
    return ls

在这个例子里,没有波及操作全局变量,然而当咱们带入了 ls = [1, 2, 3] 以及 a = 1 后,咱们仍旧发现每一次带入的时候,返回值还是不一样的。咱们也能够把这种对于 ls 的扭转表述为产生了副作用。如果咱们仅仅是想得到 ls 加了一个元素后的后果,那么这个问题将显得十分重大。

SCIP(Structure and Interpretation of Computer Programs)一书中,十分好的概述了这两种思路处理函数的不同。在前者「无副作用」的例子里,af之类的货色,仅仅具备批示一个值 / 函数的意义;然而对于有「副作用」概念的后者的函数里,咱们必须得结构一个「环境」的概念。这个环境里,有一个个屋子(在计算机里可能就是内存 /CPU 缓存的概念),af 批示的是屋子(但有的时候又是指屋子里放的货色),更可怕的是,屋子的大小也会变动;而「无副作用」的例子里,咱们只有晓得符号永远指的是一个货色就好了,一次指定后就不在变动,并不需要屋子的变动。

副作用的好和坏

当初,咱们就来看「无副作用」和「副作用」到底益处和害处是什么。

1. 回溯问题

如果一个函数,它对于一个确定的输出必然有一个确定的输入,这意味着,咱们很容易找到问题、定位问题、以及复现问题。而如果一个程序蕴含有十分多的「副作用」,这意味着咱们无法控制它在函数体外批改了什么,小到一个可变函数、大到计算机的环境变量。这也就导致为什么,很多程序的报错反馈,都要打印那么多的环境变量、计算机环境之类的概念。

而无副作用意味着十分强的「可测性」,咱们在前面的文章中也会一一列举进去。此外,「基于性质的测试」也成为可能。也就意味着,咱们能更加强势地管制咱们的程序。甚至对于一个动态的函数式语言(惋惜 Python 不是),编译阶段就能裸露和解决绝大部分的问题。

2. 无奈和环境交互的程序其实大概率是没啥用的

单纯的应用函数式的概念,咱们事实上结构的是一个逻辑符号运算零碎,如果没有和外界环境交互,则它就是楼台的玩具。咱们甚至应用 print 都是在产生一个函数外的屏幕的副作用。所以,无副作用也就意味着它的利用很难。当然,「单子」的概念、把副作用限缩在一个十分小的范畴里,这些办法都能够让咱们对本人的程序把握还是十分强,并且 又有肯定的「交互自在」。这个也是咱们这个系列要强调的编程思路

3. 效率

事实上,咱们下面的举例中,曾经能够看出,计算机 (/ 图灵机) 自身的概念就是基于环境或者说基于副作用的。而函数式编程在一个副作用机子上实现,原本就会效率降落。更何况,如果咱们不应用相似 append 之类的原地操作,这就意味着更多空间,更多的值的复制的概念。这些都让程序的效率大打折扣。此外,Python对诸如递归等函数式的速度优化成果并不好,这也使得副作用可能更容易让人青眼。

不过,这个效率的概念可能还有一些更加暧昧的中央。如果更大视野地对待函数式编程,外面有各种属于本人的优化计划,咱们将会在前面一一介绍。

4. 表达能力(新)

在下面对于「环境」和「代换」的探讨中,咱们也发现,如果应用「环境」的概念咱们将要多出很多概念,譬如「传值」、「传址」、「可变变量」、「全局变量」之类的概念,而且这些概念是必须内生在语言内的。一部分水平上,这是表白效率弱的体现。事实上,函数式编程仅仅靠值和函数两个概念,加上根本的类型、运算就能实现简直所有的事(或者说图灵齐全的)。而咱们前面提的「递归」、「单子」等概念,某种程度上是「派生的」而不是「内生的」。这更有 Top-down 数学的特色(当然数学是否如此那又是另一个问题了)。

一个对于定义域的阐明

最初,咱们要提到一个对于「定义域」的小问题,我在下面的阐述中没有提到,因为咱们略微用到类型 / 定义域的概念。譬如上面一个函数(我特意带上了类型注解):

def f(x: int) -> int:
    if x > 0:
        return x + 1

这个例子中,其实 \(x\)的取值范畴只能是 \(x > 0\)(尽管在例外的状况 Python 会输入 None),但事实上这种操作和数学中的函数还是有稍微的区别,因为它在申明时的定义域为int 事实上的定义域是 \(x \in N\)。int里并非所有取值都没用到。

在诸如 scala 或者 haskell 等函数式反对较好的语言里,也称这种函数为 Partial Function(留神和Curry 化 中的 Partial Applied Function 的区别),意思是并不是所有的定义域的自变量都申明过了,比方,scala 中定义上述的函数 f 会用到PartialFunction

val f: PartialFunction[Int, Int] = {case x if x > 0 => x + 1}

但在理论使用状况下,咱们更偏向于用「无副作用」表述函数式编程的根本个性和性质,所以这种细节层面的探讨在大多数场合都被疏忽,然而重视数学表白的你应该值得注意一下。

References

  • 张翠媛. 浅谈弗雷格的“函数和概念”. 古代交际 14 (2018).
  • Chiusano, Paul, and Runar Bjarnason. Functional Programming in Scala. Simon and Schuster, 2014.
  • Abelson, Harold, and Gerald Jay Sussman. Structure and Interpretation of Computer Programs. The MIT Press, 1996.
正文完
 0