8

我自己理解函数式编程的不同概念:副作用、不变性、纯函数、引用透明性。但我无法在我的脑海中将它们联系在一起。例如,我有以下问题:

  1. ref之间的关系是什么?透明度和不变性。一个是否暗示另一个?

  2. 有时副作用和不变性可以互换使用。这是对的吗?

4

3 回答 3

8

这个问题需要一些特别挑剔的答案,因为它是关于定义常用词汇的。

首先,函数是输入“域”和输出“范围”(或共域)之间的一种数学关系。每个输入都会产生一个明确的输出。例如,整数加法函数+接受域中的输入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.+与其输入相关联的结果,xy我们不能缓存Ex1.+与其输入相关联的结果x。我们用来描述可缓存性(或者更准确地说,函数调用及其输出的可替代性)的术语是引用透明性

所有纯函数都是引用透明的,但一些引用透明的函数不是纯函数。例如:

object Ex2 {
  var lastResult: Int
  def +(x: Int, y: Int): Int = {
    lastResult = x + y
    lastResult
  }
}

在这里,我们不是从任何外部上下文中读取,并且任何输入产生的值都Ex2.+始终是可缓存的,如. 这是引用透明的,但它确实有一个副作用,即存储函数计算的最后一个值。其他人可以稍后出现并抓住,这将使他们对正在发生的事情有一些偷偷摸摸的洞察力!xy Ex0lastResultEx2.+

附注:您也可以争辩说它不是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。请注意,没有副作用的函数(例如纯函数)也是幂等的!

那么可变性不变性是如何影响这一切的呢?好吧,回头看看Ex2and Ex3。他们引入了 mutable vars。Ex2.+和的副作用Ex3.+是改变各自var的 s! 所以可变性和副作用是齐头并进的;仅对不可变数据进行操作的函数必须没有副作用。它可能仍然不是纯的(也就是说,它可能不是引用透明的),但至少它不会产生副作用。

一个合乎逻辑的后续问题可能是:“纯函数式风格有什么好处?” 这个问题的答案更复杂;)

于 2012-08-24T23:00:40.690 回答
2

对第一个表示“否” - 一个表示另一个,但不是相反,对第二个表示一个合格的“是”。

  1. 如果可以在不改变程序行为的情况下将表达式替换为其值,则称该表达式是引用透明的”。 不可变输入表明表达式(函数)将始终计算为相同的值,因此是引用透明的。

    但是,(在这一点上,mergeconflict 善意地纠正了我)引用透明并不一定需要immutability

  2. 根据定义,副作用是函数的一个方面;这意味着当你调用一个函数时,它会改变一些东西不变性数据的一个方面;它不能改变。调用这样的函数确实意味着不会有副作用。(在 Scala 中,这仅限于“不更改不可变对象”——开发人员有责任和决定)。

    虽然副作用不变性并不意味着同一件事,但它们是函数和函数所应用的数据密切相关的方面。

由于 Scala 不是一种纯函数式编程语言,因此在考虑诸如“不可变输入”之类的语句的含义时必须小心——函数的输入范围可能包括作为参数传递的元素以外的元素。同样考虑副作用。

于 2012-08-24T21:24:34.190 回答
1

这取决于您使用的具体定义(可能存在分歧,例如纯度与参考透明度),但我认为这是一个合理的解释:

引用透明度和“纯度”是函数/表达式的属性。函数/表达式可能有也可能没有副作用。另一方面,不变性是对象的属性,而不是函数/表达式。

参照透明、副作用和纯度密切相关:“纯粹”和“参照透明”是等价的,这些概念等价于没有副作用。

不可变对象可能具有引用不透明的方法:这些方法不会更改对象本身(因为这会使对象可变),但可能具有其他副作用,例如执行 I/O 或操作其(可变)参数.

于 2012-08-24T22:04:57.443 回答