Scala 中的 "side-effect" 是什么?
What is a "side-effect" in Scala?
我目前正在学习使用 Scala 进行函数式编程。
我也在了解循环以及由于副作用应如何避免循环。
这是什么意思?
作为来自@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)
纯函数式编程语言中的函数完全类似于数学中的函数:它们根据参数值生成结果值,而仅根据参数值生成结果值.
副作用(通常简称为效应)是其他所有。 IE。所有不读取参数和return结果的东西都是副作用。
这包括但不限于:
- 变异状态,
- 正在从控制台读取输入,
- 打印输出到控制台,
- 读取、创建、删除或写入文件,
- 从网络读取或写入,
- 反思,
- 根据当前时间,
- 启动或中止线程或进程,或
- 真的是任何一种 I/O,最重要的是
- 调用不纯函数。
最后一条非常重要:调用不纯函数会使函数不纯。从这个意义上说,副作用是 具有传染性的。
请注意,“只允许您阅读参数”的说法有些简化。一般来说,我们认为函数的环境也是一种“不可见”的参数。这意味着,例如,允许 Closure 从它关闭的环境中读取变量。允许函数读取全局变量。
Scala 是一种面向对象的语言,具有 方法,其中有一个不可见的 this
参数,它们可以读取。
这里重要的属性叫做Referential Transparency。一个函数或表达式是引用透明 如果你可以用它的值替换它而不改变程序的含义(反之亦然)。
请注意,通常,术语“纯”或“纯功能”、“引用透明”和“无副作用”可互换使用。
例如,在下面的程序中,(子)表达式 2 + 3
是引用透明的,因为我可以用它的值 5
替换它而不改变程序的含义:
println(2 + 3)
与
的含义完全相同
println(5)
然而,println
方法不是引用透明的,因为如果我用它的值替换它,程序的含义就会改变:
println(2 + 3)
不和
是同一个意思吗
()
就是 ()
的值(发音为“unit”),也就是 println
的 return 值。
这样做的结果是引用透明函数总是return在传递相同的参数时得到相同的结果值。对于所有代码,您应该为相同的输入获得相同的输出。或者更一般地说,如果你一遍又一遍地做同样的事情,同样的结果应该一遍又一遍地发生。
这就是循环和副作用之间的联系所在:循环一遍又一遍地做同样的事情。所以,它应该一遍又一遍地得到相同的结果。但它不会:它会至少一次有不同的结果,即它会完成。 (除非是无限循环。)
为了使循环有意义,它们必须有副作用。但是,纯函数式程序 不能有副作用 。因此,循环 在纯函数式程序中不可能有意义。
所有这些都是很好的答案。如果您来自另一种语言,只需添加一个快速点。
无效函数
函数return什么都没有,例如void
,暗示有副作用。
例如,如果您在 c# 中有此代码
void Log (string message) => Logger.WriteLine(message);
这会导致副作用,即向记录器写入内容。
重要吗?可能你不在乎。不过,这个呢?
def SubmitOrder(order: Order): Unit =
{
// code that submits an order
}
这样不好。稍后再看。
为什么副作用不好?
除了一些明显的原因,包括:
- 难以推理:必须阅读整个函数体才能了解发生了什么;
- 可变状态:容易出错并且可能不是线程安全的
最重要的是,测试起来很烦
如何避免副作用?
一个简单的方法就是总是尝试 return 一些东西。 (当然,还是尽量不要在内部改变状态,关闭即可)
比如前面的例子,如果不是Unit
,我们有:
def SubmitOrder(order: Order): Either[SubmittedOrder, OrderSubmissionError] =
{
// code that submits an order
}
这会好很多,它告诉 reader 有副作用以及可能发生的情况。
循环中的副作用
现在回到你关于循环的问题,如果不分析你的实际情况,很难提出如何避免循环的副作用。
但是,如果您正在编写一个函数,然后想编写一个调用该函数的循环,请确保该函数不会修改局部变量或其他地方的状态。
我目前正在学习使用 Scala 进行函数式编程。
我也在了解循环以及由于副作用应如何避免循环。
这是什么意思?
作为来自@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)
纯函数式编程语言中的函数完全类似于数学中的函数:它们根据参数值生成结果值,而仅根据参数值生成结果值.
副作用(通常简称为效应)是其他所有。 IE。所有不读取参数和return结果的东西都是副作用。
这包括但不限于:
- 变异状态,
- 正在从控制台读取输入,
- 打印输出到控制台,
- 读取、创建、删除或写入文件,
- 从网络读取或写入,
- 反思,
- 根据当前时间,
- 启动或中止线程或进程,或
- 真的是任何一种 I/O,最重要的是
- 调用不纯函数。
最后一条非常重要:调用不纯函数会使函数不纯。从这个意义上说,副作用是 具有传染性的。
请注意,“只允许您阅读参数”的说法有些简化。一般来说,我们认为函数的环境也是一种“不可见”的参数。这意味着,例如,允许 Closure 从它关闭的环境中读取变量。允许函数读取全局变量。
Scala 是一种面向对象的语言,具有 方法,其中有一个不可见的 this
参数,它们可以读取。
这里重要的属性叫做Referential Transparency。一个函数或表达式是引用透明 如果你可以用它的值替换它而不改变程序的含义(反之亦然)。
请注意,通常,术语“纯”或“纯功能”、“引用透明”和“无副作用”可互换使用。
例如,在下面的程序中,(子)表达式 2 + 3
是引用透明的,因为我可以用它的值 5
替换它而不改变程序的含义:
println(2 + 3)
与
的含义完全相同println(5)
然而,println
方法不是引用透明的,因为如果我用它的值替换它,程序的含义就会改变:
println(2 + 3)
不和
是同一个意思吗()
就是 ()
的值(发音为“unit”),也就是 println
的 return 值。
这样做的结果是引用透明函数总是return在传递相同的参数时得到相同的结果值。对于所有代码,您应该为相同的输入获得相同的输出。或者更一般地说,如果你一遍又一遍地做同样的事情,同样的结果应该一遍又一遍地发生。
这就是循环和副作用之间的联系所在:循环一遍又一遍地做同样的事情。所以,它应该一遍又一遍地得到相同的结果。但它不会:它会至少一次有不同的结果,即它会完成。 (除非是无限循环。)
为了使循环有意义,它们必须有副作用。但是,纯函数式程序 不能有副作用 。因此,循环 在纯函数式程序中不可能有意义。
所有这些都是很好的答案。如果您来自另一种语言,只需添加一个快速点。
无效函数
函数return什么都没有,例如void
,暗示有副作用。
例如,如果您在 c# 中有此代码
void Log (string message) => Logger.WriteLine(message);
这会导致副作用,即向记录器写入内容。
重要吗?可能你不在乎。不过,这个呢?
def SubmitOrder(order: Order): Unit =
{
// code that submits an order
}
这样不好。稍后再看。
为什么副作用不好?
除了一些明显的原因,包括:
- 难以推理:必须阅读整个函数体才能了解发生了什么;
- 可变状态:容易出错并且可能不是线程安全的
最重要的是,测试起来很烦
如何避免副作用?
一个简单的方法就是总是尝试 return 一些东西。 (当然,还是尽量不要在内部改变状态,关闭即可)
比如前面的例子,如果不是Unit
,我们有:
def SubmitOrder(order: Order): Either[SubmittedOrder, OrderSubmissionError] =
{
// code that submits an order
}
这会好很多,它告诉 reader 有副作用以及可能发生的情况。
循环中的副作用
现在回到你关于循环的问题,如果不分析你的实际情况,很难提出如何避免循环的副作用。
但是,如果您正在编写一个函数,然后想编写一个调用该函数的循环,请确保该函数不会修改局部变量或其他地方的状态。