Scala 中的“副作用”是什么?

问题描述

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

我也在学习循环以及如何避免它们的副作用。

这是什么意思?

解决方法

纯函数式编程语言中的函数完全类似于数学中的函数:它们根据其参数值产生结果值,并且仅根据其参数值。 >

副作用(通常简称为效果)是其他一切。 IE。不读取参数并返回结果的所有内容都是副作用。

这包括但不限于:

  • 改变状态,
  • 从控制台读取输入,
  • 将输出打印到控制台,
  • 读取、创建、删除或写入文件,
  • 从网络读取或写入,
  • 反思,
  • 根据当前时间,
  • 启动或中止线程或进程,或
  • 任何类型的 I/O,最重要的是
  • 调用一个不纯的函数。

最后一个非常重要:调用不纯函数会使函数不纯。从这个意义上说,副作用具有传染性

请注意,说“您只能阅读参数”有些简化。一般来说,我们认为函数的环境也是一种“隐形”参数。这意味着,例如,允许 Closure 从它关闭的环境中读取变量。允许函数读取全局变量。

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

这里的重要属性称为引用透明。一个函数或一个表达式引用透明,如果你能用它的值替换它而不改变程序的含义(反之亦然)。

请注意,一般而言,术语“纯”或“纯功能”、“参考透明”和“无副作用”可互换使用。

例如,在下面这个程序中,(子)表达式 2 + 3 是引用透明的,因为我可以用它的值 5 替换它而不改变程序的含义:

println(2 + 3)

具有完全相同的含义
println(5)

然而,println 方法不是引用透明的,因为如果我用它的值替换它,程序的含义就会改变:

println(2 + 3)

不是

具有相同的含义吗
()

这只是值 ()(发音为“unit”),它是 println 的返回值。

这样做的结果是,当传递相同的参数时,引用透明函数总是返回相同的结果值。对于所有代码,您应该为相同的输入获得相同的输出。或者更一般地说,如果你一遍又一遍地做同样的事情,同样的结果应该一遍又一遍地发生。

这就是循环和副作用之间的联系所在:循环一遍又一遍地做同样的事情。所以,它应该一遍又一遍地得到相同的结果。但事实并非如此:它将有不同的结果至少一次,即它会完成。 (除非是无限循环。)

为了使循环有意义,它们必须有副作用。然而,一个纯函数式程序不能有副作用。因此,在纯函数式程序中,循环不可能有意义。

,

作为@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)
,

所有这些都是很好的答案。如果您来自另一种语言,只需添加一个简短的点。

空函数

函数不返回任何内容,例如 void,暗示存在副作用。

例如,如果您在 c# 中有此代码

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

这会导致副作用,即向记录器写入一些内容。

有关系吗?可能你不在乎。然而,这又如何呢?

def SubmitOrder(order: Order): Unit = 
{
   // code that submits an order 
}

这不会好。稍后再看。

为什么副作用不好?

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

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

最重要的是,测试很烦。

如何避免副作用?

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

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

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

这样会好很多,它告诉读者有副作用以及可能会发生什么。

循环中的副作用

现在回到你关于循环的问题,不分析你的真实案例,很难建议如何避免循环的副作用。

但是,如果您正在编写一个函数,然后又想编写一个调用该函数的循环,请确保该函数不会修改其他地方的局部变量或状态。