我正在尝试学习Scala,但无法理解这个概念。为什么将对象设置为不可变的有助于防止函数中的副作用?有人能像我五岁一样解释一下吗?
我正在尝试学习Scala,但无法理解这个概念。为什么将对象设置为不可变的有助于防止函数中的副作用?有人能像我五岁一样解释一下吗?
这是一个有趣的问题,但难以回答。
函数式编程很大程度上使用数学来推理程序。为此,需要一种形式化描述程序及其属性证明方法的形式体系。
有许多计算模型提供这样的形式体系,例如λ演算和图灵机。它们之间存在某种程度的等同性(参见这个问题进行讨论)。
从非函数式编程到函数式编程有一种直接映射关系,例如以下例子:
a = 0
b = 1
a = a + b
以下是两种将其映射到函数式程序的方法。第一种方法是,a
和b
都是“状态”的一部分,每一行都是从一个状态到另一个新状态的函数:
state1 = (a = 0, b = ?)
state2 = (a = state1.a, b = 1)
state3 = (a = state2.a + state2.b, b = state2.b)
这里是另一个例子,每个变量与特定时间相关联:
(a, t0) = 0
(b, t1) = 1
(a, t2) = (a, t0) + (b, t1)
那么,考虑到上述情况,为什么不使用可变性呢?
其实,数学有一个非常有趣的特点:形式越简单的公式,就越容易进行证明。换句话说,对于包含可变性的程序,我们很难进行推理。
因此,在编程中涉及可变性的概念相对较少。著名的设计模式并不是通过研究得出的,也没有任何数学支持。它们是多年试错的结果,其中一些已被证明是误导性的。其他许多“设计模式”是否可靠还无从得知。
与此同时,Haskell程序员提出了Functor、Monad、Co-monad、Zipper、Applicative、Lenses等数十个概念,这些概念都有数学支持,并且最重要的是,它们实际上构成了编程中代码组合的模式。这些概念可以用来推理程序、提高可重用性和正确性。你可以在Typeclassopedia网站上看到一些例子。
毫不奇怪,对于不熟悉函数式编程的人来说,这些东西可能有点吓人......相比之下,其他编程领域仍然在使用几十年前的概念。新概念的提出对他们来说是陌生而奇怪的。
不幸的是,所有这些模式、概念只适用于没有可变性(或其他副作用)的代码,如果存在可变性,它们的特性就不再有效,你将不得不回到猜测、测试和调试的过程中。
*p
)进行解引用操作的表达式只有在所引用的值(以及引用本身)是不可变的情况下才是"referentially transparent",这与你所说的相关。但它也包括在没有引用涉及的情况下将(3+4)*2
替换为14
的情况。 - Ben我采用这个五岁孩子也能理解的解释:
class Account(var myMoney:List[Int] = List(10, 10, 1, 1, 1, 5)) {
def getBalance = println(myMoney.sum + " dollars available")
def myMoneyWithInterest = {
myMoney = myMoney.map(_ * 2)
println(myMoney.sum + " dollars will accru in 1 year")
}
}
scala> val myAccount = new Account()
myAccount: Account = Account@7f4a6c40
scala> myAccount.getBalance
28 dollars available
scala> myAccount.myMoneyWithInterest
56 dollars will accru in 1 year
scala> myAccount.getBalance
56 dollars available
当我们想要查看当前余额加上一年的利息时,我们使用“mutated”更改了账户余额。现在,我们的账户余额是错误的。对于银行来说,这是个坏消息!
如果在类定义中使用“val”而不是“var”来跟踪“myMoney”,我们将无法“mutate”美元并增加我们的余额。
在REPL中定义类时,使用“val”:
error: reassignment to val
myMoney = myMoney.map(_ * 2
Scala告诉我们,我们想要一个不可变的值,但试图改变它!
由于有了Scala,我们可以更换为val
,重新编写我们的myMoneyWithInterest
方法,并放心地确保我们的Account
类永远不会改变余额。
arg
是一个 var
,你可以在需要时对其进行 mutate
],而你只是想打印它!你必须明确告诉 Scala 你想要一个可变的集合/变量。 - Kylefor(var arg <- List(1,2,3,4,5))
无法编译。 - Kyle函数式编程的一个重要特性是:如果我使用相同的参数两次调用相同的函数,我将得到相同的结果。在许多情况下,这使得对代码的推理更加容易。
现在想象一下一个返回某个对象属性content
的函数。如果该content
可以更改,则该函数可能在使用相同的参数进行不同调用时返回不同的结果。=>没有更多的函数式编程。
首先是一些定义:
一个被传递可变对象(作为参数或在全局环境中)的函数可能会产生副作用,也可能不会。这取决于实现。
然而,如果一个函数只传递不可变对象(作为参数或在全局环境中),那么它就不可能产生副作用。因此,完全使用不可变对象将排除副作用的可能性。
class MyValue(val value: Int)
def plus(x: MyValue) = x.value + 10
val x = new MyValue(10)
val y = plus(x) // y is 20
val z = plus(x) // z is still 20, plus(x) will always yield 20
但是如果您有可变对象,就不能保证plus(x)对于MyValue的同一实例始终返回相同的值。
class MyValue(var value: Int)
def plus(x: MyValue) = x.value + 10
val x = new MyValue(10)
val y = plus(x) // y is 20
x.value = 30
val z = plus(x) // z is 40, you can't for sure what value will plus(x) return because MyValue.value may be changed at any point.
我已经阅读了所有的答案,但它们并不能令我满意,因为它们大多谈论的是“不可变性”,而不是它与FP的关系。
主要问题是:
为什么不可变对象能够实现函数式编程?
所以我又搜索了一下,我相信这个问题的简单答案是:“因为函数式编程基本上是建立在易于推理的函数基础之上定义的”。以下是函数式编程的定义:
通过组合纯函数构建软件的过程。
如果一个函数不是纯函数——这意味着接收相同的输入,不能保证始终产生相同的输出(例如,如果函数依赖于全局对象、日期和时间或随机数来计算输出)——那么该函数就是不可预测的!现在关于“不可变性”的故事也是一样的,如果对象不是不可变的,那么具有相同对象作为其输入的函数每次使用时可能会产生不同的结果(即副作用),这将使得对程序进行推理变得困难。
我最初尝试将这个放在评论中,但它超过了限制,我绝不是专业人士,所以请谨慎对待我的回答。