我理解功能编程本身的不同概念:副作用,不变性,纯函数,参考透明度。但我无法将它们连接在一起。例如,我有以下问题:
ref之间的关系是什么?透明度和不变性。有人暗示另一个吗?
有时副作用和不变性可互换使用。这是正确的吗?
答案 0 :(得分:8)
这个问题需要一些特别挑剔的答案,因为它是关于定义常用词汇。
首先,函数是输入的“域”与输出的“范围”(或codomain)之间的一种数学关系。每个输入都会产生明确的输出。例如,整数加法函数+
接受域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
相关联的结果,并且我们不能 cache 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's comment)!如果您更喜欢这个定义,那么函数必须是纯粹的,以便在引用上是透明的。
现在,要注意的一件事是,如果我们使用相同的输入连续两次或多次调用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
s。 Ex2.+
和Ex3.+
的副作用是改变他们各自的var
!所以可变性和副作用是相辅相成的;仅对不可变数据起作用的函数必须是无副作用的。它可能仍然不是纯粹的(也就是说,它可能不是引用透明的),但至少它不会产生副作用。
对此的逻辑后续问题可能是:“纯粹的功能风格有什么好处?”该问题的答案更为复杂;)
答案 1 :(得分:2)
第一个“否” - 一个意味着另一个,但不是反过来,而第二个意味着“是”。
“An expression is said to be referentially transparent if it can be replaced with its value without changing the behavior of a program”。
不可变输入表明表达式(函数)将始终评估为相同的值,因此引用透明。
但是,(mergeconflict在这一点上善意纠正我)引用透明并不一定需要不变性。
根据定义,副作用是功能的一个方面;意思是当你调用一个函数时,它改变了一些东西。
不变性是数据的一个方面;它无法改变。
在这样的情况下调用函数意味着不会产生副作用。 (在Scala中,这仅限于“对不可变对象没有任何改变” - 开发人员有责任和决定)。
虽然副作用和不变性并不意味着相同,但它们是函数的紧密相关方面以及函数应用的数据。
由于Scala不是纯函数式编程语言,因此在考虑“不可变输入”等语句的含义时必须小心 - 函数输入的范围可能包括除了作为参数传递的元素之外的元素 。同样考虑副作用。
答案 2 :(得分:1)
它取决于您使用的具体定义(可能存在分歧,例如参见Purity vs Referential transparency),但我认为这是一个合理的解释:
参照透明度和'纯度'是函数/表达式的属性。函数/表达可能有也可能没有副作用。另一方面,不变性是对象的属性,而不是函数/表达式。
参考透明度,副作用和纯度密切相关:'纯'和'参考透明'是等价的,这些概念相当于没有副作用。
不可变对象可能具有不引用透明的方法:这些方法不会更改对象本身(因为这会使对象变为可变),但可能有其他副作用,例如执行I / O或操作它们(可变的)参数。