在Scala中,“副作用”是什么?

6

我目前正在学习使用Scala进行函数式编程。

我也在学习关于循环的内容以及为什么应该避免使用循环,因为它们会产生副作用。

这是什么意思?

3个回答

13

纯函数式编程语言中的函数与数学中的函数完全相同:它们基于其参数值生成结果值,且仅基于其参数值生成结果值。

副作用(通常仅称为效应)是指除了读取参数并返回结果之外的所有其他操作。例如:

  • 修改状态
  • 从控制台读取输入
  • 将输出打印到控制台
  • 读取、创建、删除或写入文件
  • 从网络读取或写入
  • 反射
  • 依赖当前时间
  • 启动或终止线程或进程
  • 任何类型的I/O,尤其是
  • 调用不纯的函数

最后一种情况非常重要:调用不纯的函数会使一个函数变得不纯。在这个意义上,副作用是具有传染性的

请注意,说“您只能读取参数”有些简化。通常,我们认为函数的环境也是一种“不可见”的参数。这意味着,例如,闭包可以从它所闭合的环境中读取变量。函数可以读取全局变量。

Scala是一种面向对象的语言,具有方法,这些方法具有一个不可见的this参数,它们可以阅读该参数。

这里的重要属性称为引用透明性。如果可以将函数或表达式替换为其值而不更改程序的含义(反之亦然),则该函数或表达式是引用透明的

请注意,通常使用术语“纯”或“纯函数式”、“引用透明”和“无副作用”是可以互换的。

例如,在以下程序中,(子)表达式2+3是具有引用透明性的,因为我可以将其替换为其值5而不更改程序的含义:

println(2 + 3)

与其意思完全相同

println(5)

然而,println方法并非引用透明的,因为如果我将其替换为其值,程序的含义会改变:
println(2 + 3)

“不是”和“没有”意思不同。

()

这个值简单地是 ()(发音为“unit”),它是 println 的返回值。

这带来的一个结果是,引用透明函数在传递相同参数时始终返回相同的结果值。对于所有代码,您应该获得相同的输入输出。或者更一般地说,如果您反复执行相同的操作,则应始终产生相同的结果。

这就是循环和副作用之间的联系所在:循环会一遍又一遍地做相同的事情。所以,它应该一遍又一遍地产生相同的结果。但实际上并不是这样:它最少会有一次不同的结果,即它将结束。(除非它是无限循环。)

为了使循环有意义,它们必须具有副作用。然而,纯函数式程序不能具有副作用。因此,在纯函数式程序中,循环根本就没有意义。


1
我会说非终止/发散和部分函数会带来副作用。 - user5536315

4
作为@Jörg提供的另一个例子,考虑这个用Scala编写的命令式语言的简单循环:
def printUpTo(limit: Int): Unit = {
  var i = 0
  while(i <= limit)
  {
    println("i = " + i)
    i += 1
    // in another part of the loop
    if (i % 5 == 0) { i += 1 } // ops. We should not evaluate "i" here.
  }
}

在这个循环中,有一个变量声明为var i,它是在每次迭代中改变的状态。虽然这种状态变化在外部看不到(每次进入函数时都会创建一个新副本),但使用var常常会导致代码冗余并且可以简化。确实如此。

作为一个函数式编程者,我们必须努力在任何地方使用不可变状态。在这个循环示例中,如果有人在其他地方更改了var i的值,比如在if (i % 5 == 0) { i += 1 },由于粗心大意,这将很难调试和找到。这是我们必须避免的副作用。因此,使用不可变状态可以避免这些错误。下面是使用不可变状态的相同示例。

def printUpToFunc1(limit: Int): Unit = {
  for(i <- (0 to limit)) {
    println("i = " + i)
  }
}

我们可以仅使用foreach来使代码更加清晰:

def printUpToFunc2(limit: Int): Unit = {
  (0 to limit).foreach {
    i => println("i = " + i)
  }
 }

并且更小的...

def printUpToFunc3(limit: Int): Unit = (0 to limit).foreach(println)

2

所有这些都是好答案。如果你来自另一种语言,请添加一个快速提示。

Void函数

一个返回void的函数意味着它没有返回值,但可能会有副作用。

例如,如果你在c#中有这段代码:

void Log (string message) => Logger.WriteLine(message); 

这会产生副作用,将一些内容写入记录器。
这重要吗?也许您并不在意。但是,这又怎么样呢?
def SubmitOrder(order: Order): Unit = 
{
   // code that submits an order 
}

这不是好的做法。请看后面。

为什么副作用是不好的?

除了一些明显的原因,包括:

  • 难以推理:必须阅读整个函数体才能看到正在发生什么;
  • 可变状态:容易出错,可能不是线程安全的。

最重要的是,它很烦人测试。

如何避免副作用?

一个简单的方法是总是尽量返回某些东西。(当然,还要尽量不改变内部状态,闭包可以)。

例如,前面的例子,如果我们有:

def SubmitOrder(order: Order): Either[SubmittedOrder, OrderSubmissionError] = 
{
   // code that submits an order 
}

这样会更好,它告诉读者有副作用和可能发生的情况。
循环中的副作用
现在回到你关于循环的问题,如果不分析你的实际情况,很难建议如何避免循环中的副作用。
然而,如果你正在编写一个函数,然后想写一个调用该函数的循环,请确保该函数不修改本地变量或其他地方的状态。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接