我目前正在学习使用Scala进行函数式编程。
我也在学习关于循环的内容以及为什么应该避免使用循环,因为它们会产生副作用。
这是什么意思?
我目前正在学习使用Scala进行函数式编程。
我也在学习关于循环的内容以及为什么应该避免使用循环,因为它们会产生副作用。
这是什么意思?
纯函数式编程语言中的函数与数学中的函数完全相同:它们基于其参数值生成结果值,且仅基于其参数值生成结果值。
副作用(通常仅称为效应)是指除了读取参数并返回结果之外的所有其他操作。例如:
最后一种情况非常重要:调用不纯的函数会使一个函数变得不纯。在这个意义上,副作用是具有传染性的。
请注意,说“您只能读取参数”有些简化。通常,我们认为函数的环境也是一种“不可见”的参数。这意味着,例如,闭包可以从它所闭合的环境中读取变量。函数可以读取全局变量。
Scala是一种面向对象的语言,具有方法,这些方法具有一个不可见的this
参数,它们可以阅读该参数。
这里的重要属性称为引用透明性。如果可以将函数或表达式替换为其值而不更改程序的含义(反之亦然),则该函数或表达式是引用透明的。
请注意,通常使用术语“纯”或“纯函数式”、“引用透明”和“无副作用”是可以互换的。
例如,在以下程序中,(子)表达式2+3
是具有引用透明性的,因为我可以将其替换为其值5
而不更改程序的含义:
println(2 + 3)
与其意思完全相同
println(5)
println
方法并非引用透明的,因为如果我将其替换为其值,程序的含义会改变:println(2 + 3)
“不是”和“没有”意思不同。
()
这个值简单地是 ()
(发音为“unit”),它是 println
的返回值。
这带来的一个结果是,引用透明函数在传递相同参数时始终返回相同的结果值。对于所有代码,您应该获得相同的输入输出。或者更一般地说,如果您反复执行相同的操作,则应始终产生相同的结果。
这就是循环和副作用之间的联系所在:循环会一遍又一遍地做相同的事情。所以,它应该一遍又一遍地产生相同的结果。但实际上并不是这样:它最少会有一次不同的结果,即它将结束。(除非它是无限循环。)
为了使循环有意义,它们必须具有副作用。然而,纯函数式程序不能具有副作用。因此,在纯函数式程序中,循环根本就没有意义。
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函数
一个返回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
}