我正在努力学习斯卡拉,但我无法理解这个概念。为什么使对象不可变有助于防止函数中的副作用。任何人都可以解释我是五岁吗?
答案 0 :(得分:29)
有趣的问题,有点难以回答。
功能编程非常关注使用数学推理程序。要做到这一点,人们需要一种描述程序的形式主义,以及如何能够证明他们可能拥有的属性。
有许多计算模型可以提供这样的形式,例如lambda演算和图灵机。它们之间存在一定程度的等效性(参见this question,讨论)。
在非常真实的意义上,具有可变性和一些其他副作用的程序可以直接映射到功能程序。考虑这个例子:
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程序员提出了Functors,Monads,Co-monads,Zippers,Applicatives,Lenses ......数十个具有数学支持的概念,最重要的是,编写代码的实际模式来组成程序。您可以使用的东西来推断您的程序,提高可重用性并提高正确性。请查看Typeclassopedia示例。
难怪不熟悉函数式编程的人对这些东西感到有些害怕......相比之下,编程世界的其余部分仍在使用几十年前的概念。新概念的想法是陌生的。
不幸的是,所有这些模式,所有这些概念,仅适用于他们使用的代码不包含可变性(或其他副作用)。如果确实如此,那么它们的属性就不再有效,你就不能依赖它们。你回到猜测,测试和调试。
答案 1 :(得分:12)
简而言之,如果一个函数改变了一个对象,那么它就会产生副作用。突变是一种副作用。根据定义,这是正确的。
事实上,在一个纯粹的函数式语言中,一个对象在技术上是可变的还是不可变的并不重要,因为语言永远不会“尝试”改变一个对象。纯函数式语言不会给你任何方法来执行副作用。
Scala不是一种纯函数式语言,它在Java环境中运行,其中副作用非常流行。在这种环境中,使用无法突变的对象会鼓励您使用纯粹的功能样式,因为它使得面向副作用的样式变得不可能。您正在使用数据类型来强制执行纯度,因为该语言不适合您。
现在我要说一些其他的东西,希望它对你有意义。
功能语言中变量概念的基础是参考透明度。
参照透明度意味着值与对该值的引用之间没有区别。在一种真实的语言中,它使得考虑一个程序的工作变得简单得多,因为你永远不必停下来问,这是一个值,还是一个值的引用?任何曾用C语言编程的人都认识到学习这种范式的挑战很大一部分就是知道哪些是任何时候。
为了具有引用透明性,引用引用的值永远不会改变。
(警告,我即将打个比方。)
可以这样想:在你的手机中,你已经保存了一些其他人手机的电话号码。您认为无论何时拨打该电话号码,您都会联系到您打算与之通话的人。如果其他人想与您的朋友交谈,您可以给他们电话号码,然后他们就会联系到同一个人。
如果有人更改了手机号码,则此系统会崩溃。突然间,如果你想要接触他们,你需要获得他们的新电话号码。也许你六个月后打电话给同一个号码并联系另一个人。当函数执行副作用时,调用相同的数字并联系到不同的人:你有什么似乎是相同的东西,但是你尝试使用它,现在发现它是不同的。即使你预料到这一点,你给这个号码的所有人怎么样,你打算把它们全部打电话告诉他们旧的号码不会再到达同一个人了吗?
你依靠与该人相对应的电话号码,但事实并非如此。电话号码系统缺乏参考透明度:这个数字并不总是和那个人一样。
功能语言避免了这个问题。您可以提供您的电话号码,人们将始终能够在您的余生中与您联系,并且永远不会以该号码与其他人联系。
但是,在Java平台中,事情可能会发生变化。你认为是一回事,一分钟后可能变成另一件事。如果是这种情况,你怎么能阻止它?
Scala通过创建具有引用透明性的类来使用类型的强大功能来防止这种情况。因此,即使整个语言不是引用透明的,只要您使用不可变类型,您的代码将引用 。
实际上,使用不可变类型编码的优点是:
答案 2 :(得分:4)
我正在使用 5岁的解释:
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")
}
}
假设我们在ATM,并且正在使用此代码向我们提供帐户信息。
您执行以下操作:
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
美元并提高我们平衡。
使用val
定义类(在REPL中):
error: reassignment to val
myMoney = myMoney.map(_ * 2
Scala告诉我们,我们想要immutable
值,但正在尝试更改它!
感谢Scala,我们可以切换到val
,重新编写我们的myMoneyWithInterest
方法,并确保我们的Account
课程永远不会改变余额。
答案 3 :(得分:3)
函数式编程的一个重要特性是:如果我使用相同的参数两次调用相同的函数,我将得到相同的结果。在许多情况下,这使得对代码的推理变得更加容易。
现在假设一个函数返回某个对象的属性content
。如果content
可以更改,则函数可能会在具有相同参数的不同调用上返回不同的结果。 =>没有更多的功能编程。
答案 4 :(得分:2)
首先是几个定义:
传递可变对象的函数(作为参数或在全局环境中)可能会或可能不会产生副作用。这取决于实施。
但是,对于仅传递不可变对象(作为参数或在全局环境中)产生副作用的函数,不可能。因此,独占使用不可变对象将排除副作用的可能性。
答案 5 :(得分:1)
Nate的答案很棒,这是一些例子。
在函数式编程中,有一个重要的特性,当你用相同的参数调用一个函数时,你总是得到相同的返回值。
对于不可变对象总是如此,因为在创建它之后无法修改它们:
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.
答案 6 :(得分:1)
为什么不可变对象启用函数式编程?
他们没有。
采用"函数的一个定义,"或" prodecure," "程序"或"方法,"我认为它适用于许多编程语言:"一段代码,通常命名为,接受参数和/或返回值。"
采用"函数式编程的一个定义:" "使用函数编程。"使用函数编程的能力取决于状态是否被修改。
例如,Scheme被认为是一种函数式编程语言。它具有尾调用,高阶函数和使用函数的聚合操作。它也有可变对象。虽然可变性破坏了一些不错的数学特性,但它并不一定能阻止"函数式编程。"