我自己理解函数式编程的不同概念:副作用,不变性,纯函数,引用透明性。但是我无法将它们连接在一起。例如,我有以下问题:
最佳答案
这个问题需要一些特别挑剔的答案,因为它是关于定义常用词汇的。
首先,函数是输入的“域”与输出的“范围”(或共域)之间的一种数学关系。每个输入都会产生明确的输出。例如,整数加法函数+
接受域Int x Int
中的输入,并产生范围Int
中的输出。
object Ex0 {
def +(x: Int, y: Int): Int = x + y
}
给定
x
和y
的任何值,显然+
将始终产生相同的结果。这是一个功能。如果编译器非常聪明,它可以插入代码以为每对输入缓存该函数的结果,并执行缓存查找作为优化。这显然很安全。问题是在软件中,术语“函数”已被滥用:尽管函数接受参数并返回其签名中声明的值,但它们也可以读写某些外部上下文。例如:
class Ex1 {
def +(x: Int): Int = x + Random.nextInt
}
我们再也不能将其视为数学函数,因为对于给定的
x
值,+
会产生不同的结果(取决于随机值,该值不会出现在+
的签名中)。如上所述,无法安全地缓存+
的结果。因此,现在我们遇到了一个词汇问题,可以通过说Ex0.+
是纯来解决,而Ex1.+
不是。好的,因为我们现在已经接受了某种程度的杂质,所以我们需要定义我们正在谈论的杂质类型!在这种情况下,我们说过的区别是我们可以缓存
Ex0.+
与输入x
和y
关联的结果,而不能缓存Ex1.+
与输入x
关联的结果。我们用来描述可缓存性(或更准确地说,是函数调用及其输出的可替换性)的术语是参照透明性。所有纯函数都是参照透明的,但是某些参照透明函数也不是纯的。例如:
object Ex2 {
var lastResult: Int
def +(x: Int, y: Int): Int = {
lastResult = x + y
lastResult
}
}
这里我们不是从任何外部上下文读取的,并且
Ex2.+
为任何输入x
和y
生成的值将始终可缓存,就像Ex0
一样。这是参照透明的,但确实有副作用,它存储了函数计算的最后一个值。其他人可以稍后再拿起lastResult
,这将使他们对Ex2.+
发生的事情有一些偷偷摸摸的了解!现在,要观察的一件事是,如果我们使用相同的输入连续两次或多次调用
Ex2.+
,则lastResult
不会改变。 n次调用该方法的副作用等效于仅调用一次该方法的副作用,因此我们说Ex2.+
是等幂。我们可以更改它:object Ex3 {
var history: Seq[Int]
def +(x: Int, y: Int): Int = {
result = x + y
history = history :+ result
result
}
}
现在,每次调用
Ex3.+
时,历史记录都会更改,因此该函数不再是幂等的。好的,到目前为止,回顾一下:纯函数是既不读取也不写入任何外部上下文的函数。它既是引用透明,又是副作用免费。从某些外部上下文读取的函数不再是引用透明的,而向某些外部上下文写入的函数不再具有副作用。最后,当一个函数用相同的输入多次调用时具有与仅调用一次相同的副作用,该函数称为等幂。请注意,没有副作用的功能(例如纯功能)也是幂等的!
那么可变性和可变性如何在所有这些方面发挥作用?好吧,回头看看
Ex2
和Ex3
。他们引入了可变的var
。 Ex2.+
和Ex3.+
的副作用是要改变它们各自的var
!因此可变性和副作用是齐头并进的。仅对不可变数据进行操作的函数必须没有副作用。它可能仍然不是纯净的(也就是说,它可能不是参照透明的),但至少不会产生副作用。一个合乎逻辑的后续问题可能是:“纯功能样式的好处是什么?”这个问题的答案更多;)