我自己理解函数式编程的不同概念:副作用、不变性、纯函数、引用透明性。但我无法在我的脑海中将它们联系在一起。例如,我有以下问题:
ref之间的关系是什么?透明度和不变性。一个是否暗示另一个?
有时副作用和不变性可以互换使用。这是对的吗?
我自己理解函数式编程的不同概念:副作用、不变性、纯函数、引用透明性。但我无法在我的脑海中将它们联系在一起。例如,我有以下问题:
ref之间的关系是什么?透明度和不变性。一个是否暗示另一个?
有时副作用和不变性可以互换使用。这是对的吗?
这个问题需要一些特别挑剔的答案,因为它是关于定义常用词汇的。
首先,函数是输入“域”和输出“范围”(或共域)之间的一种数学关系。每个输入都会产生一个明确的输出。例如,整数加法函数+
接受域中的输入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.+
引用透明的,因为尽管缓存对于函数的结果是安全的,但在缓存“命中”的情况下,副作用会被默默地忽略。换句话说,如果副作用很重要(因此Norman Ramsey 的评论),引入缓存会改变程序的含义!如果您更喜欢这个定义,那么函数必须是纯函数才能实现引用透明。
现在,这里要注意的一件事是,如果我们Ex2.+
使用相同的输入连续调用两次或多次,lastResult
则不会改变。调用n次方法的副作用相当于只调用一次方法的副作用,所以我们说它Ex2.+
是幂等的。我们可以改变它:
object Ex3 {
var history: Seq[Int]
def +(x: Int, y: Int): Int = {
result = x + y
history = history :+ result
result
}
}
现在,每次我们调用Ex3.+
,历史都会改变,所以函数不再是幂等的。
好的,到目前为止的回顾:纯函数是既不读取也不写入任何外部上下文的函数。它是参照透明和无副作用的。从某些外部上下文读取的函数不再是引用透明的,而写入某些外部上下文的函数不再没有副作用。最后,使用相同输入多次调用的函数与只调用一次具有相同的副作用,称为idempotent。请注意,没有副作用的函数(例如纯函数)也是幂等的!
那么可变性和不变性是如何影响这一切的呢?好吧,回头看看Ex2
and Ex3
。他们引入了 mutable var
s。Ex2.+
和的副作用Ex3.+
是改变各自var
的 s! 所以可变性和副作用是齐头并进的;仅对不可变数据进行操作的函数必须没有副作用。它可能仍然不是纯的(也就是说,它可能不是引用透明的),但至少它不会产生副作用。
一个合乎逻辑的后续问题可能是:“纯函数式风格有什么好处?” 这个问题的答案更复杂;)
对第一个表示“否” - 一个表示另一个,但不是相反,对第二个表示一个合格的“是”。
“如果可以在不改变程序行为的情况下将表达式替换为其值,则称该表达式是引用透明的”。
不可变输入表明表达式(函数)将始终计算为相同的值,因此是引用透明的。
但是,(在这一点上,mergeconflict 善意地纠正了我)引用透明并不一定需要immutability。
根据定义,副作用是函数的一个方面;这意味着当你调用一个函数时,它会改变一些东西。
不变性是数据的一个方面;它不能改变。调用这样的函数确实意味着不会有副作用。(在 Scala 中,这仅限于“不更改不可变对象”——开发人员有责任和决定)。
虽然副作用和不变性并不意味着同一件事,但它们是函数和函数所应用的数据密切相关的方面。
由于 Scala 不是一种纯函数式编程语言,因此在考虑诸如“不可变输入”之类的语句的含义时必须小心——函数的输入范围可能包括作为参数传递的元素以外的元素。同样考虑副作用。
这取决于您使用的具体定义(可能存在分歧,例如纯度与参考透明度),但我认为这是一个合理的解释:
引用透明度和“纯度”是函数/表达式的属性。函数/表达式可能有也可能没有副作用。另一方面,不变性是对象的属性,而不是函数/表达式。
参照透明、副作用和纯度密切相关:“纯粹”和“参照透明”是等价的,这些概念等价于没有副作用。
不可变对象可能具有引用不透明的方法:这些方法不会更改对象本身(因为这会使对象可变),但可能具有其他副作用,例如执行 I/O 或操作其(可变)参数.