I'm trying to learn scala and I'm unable to grasp this concept. Why does making an object immutable help prevent side-effects in functions. Can anyone explain like I'm five?
7 回答
有趣的问题,有点难以回答。
函数式编程在很大程度上是关于使用数学来推理程序。为此,需要一种描述程序以及如何证明程序可能具有的属性的形式。
有许多计算模型可以提供这种形式,例如 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的例子。
难怪不熟悉函数式编程的人会对这些东西感到害怕……相比之下,编程世界的其他人仍在使用几十年前的概念。新概念的想法是陌生的。
不幸的是,所有这些模式,所有这些概念,只适用于他们正在使用的代码,不包含可变性(或其他副作用)。如果是这样,那么它们的属性将不再有效,并且您不能依赖它们。你又回到了猜测、测试和调试。
In short, if a function mutates an object then it has side effects. Mutation is a side effect. This is just true by definition.
In truth, in a purely functional language it should not matter if an object is technically mutable or immutable, because the language will never "try" to mutate an object anyway. A pure functional language doesn't give you any way to perform side effects.
Scala is not a pure functional language, though, and it runs in the Java environment in which side effects are very popular. In this environment, using objects that are incapable of mutation encourages you to use a pure functional style because it makes a side-effect oriented style impossible. You are using data types to enforce purity because the language does not do it for you.
Now I will say a bunch of other stuff in the hope that it helps this make sense to you.
Fundamental to the concept of a variable in functional languages is referential transparency.
Referential transparency means that there is no difference between a value, and a reference to that value. In a language where this is true, it makes it much simpler to think about a program works, since you never have to stop and ask, is this a value, or a reference to a value? Anyone who's ever programmed in C recognizes that a great part of the challenge of learning that paradigm is knowing which is which at all times.
In order to have referential transparency, the value that a reference refers to can never change.
(Warning, I'm about to make an analogy.)
Think of it this way: in your cell phone, you have saved some phone numbers of other people's cell phones. You assume that whenever you call that phone number, you will reach the person you intend to talk to. If someone else wants to talk to your friend, you give them the phone number and they reach that same person.
If someone changes their cell phone number, this system breaks down. Suddenly, you need to get their new phone number if you want to reach them. Maybe you call the same number six months later and reach a different person. Calling the same number and reaching a different person is what happens when functions perform side effects: you have what seems to be the same thing, but you try to use it, it turns out it's different now. Even if you expected this, what about all the people you gave that number to, are you going to call them all up and tell them that the old number doesn't reach the same person anymore?
You counted on the phone number corresponding to that person, but it didn't really. The phone number system lacks referential transparency: the number isn't really ALWAYS the same as the person.
Functional languages avoid this problem. You can give out your phone number and people will always be able to reach you, for the rest of your life, and will never reach anybody else at that number.
However, in the Java platform, things can change. What you thought was one thing, might turn into another thing a minute later. If this is the case, how can you stop it?
Scala uses the power of types to prevent this, by making classes that have referential transparency. So, even though the language as a whole isn't referentially transparent, your code will be referentially transparent as long as you use immutable types.
Practically speaking, the advantages of coding with immutable types are:
- Your code is simpler to read when the reader doesn't have to look out for surprising side effects.
- If you use multiple threads, you don't have to worry about locking because shared objects can never change. When you have side effects, you have to really think through the code and figure out all the places where two threads might try to change the same object at the same time, and protect against the problems that this might cause.
- Theoretically, at least, the compiler can optimize some code better if it uses only immutable types. I don't know if Java can do this effectively, though, since it allows side effects. This is a toss-up at best, anyway, because there are some problems that can be solved much more efficiently by using side effects.
我正在使用这个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
美元并提高我们的余额。
当定义类(在 REPL 中)时val
:
error: reassignment to val
myMoney = myMoney.map(_ * 2
Scala 告诉我们,我们想要一个immutable
值,但正在尝试改变它!
感谢 Scala,我们可以切换到val
,重新编写我们的myMoneyWithInterest
方法,并确保我们的Account
类永远不会改变平衡。
函数式编程的一个重要特性是:如果我用相同的参数调用相同的函数两次,我将得到相同的结果。在许多情况下,这使得对代码的推理变得更加容易。
现在想象一个函数返回content
某个对象的属性。如果这content
可以改变函数可能会在具有相同参数的不同调用上返回不同的结果。=> 没有更多的函数式编程。
先来几个定义:
- 副作用是状态的变化——也称为突变。
- 不可变对象是不支持突变的对象(副作用)。
传递可变对象(作为参数或在全局环境中)的函数可能会也可能不会产生副作用。这取决于实施。
但是,仅传递不可变对象(作为参数或在全局环境中)的函数不可能产生副作用。因此,独占使用不可变对象将排除副作用的可能性。
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.
为什么不可变对象支持函数式编程?
他们没有。
以“函数”或“过程”、“例程”或“方法”的一个定义为例,我相信它适用于许多编程语言:“一段代码,通常命名,接受参数和/或返回值。”
采用“函数式编程”的一种定义:“使用函数进行编程”。使用函数进行编程的能力与状态是否被修改无关。
例如,Scheme 被认为是一种函数式编程语言。它具有尾调用、高阶函数和使用函数的聚合操作。它也有可变对象。虽然可变性破坏了一些很好的数学品质,但它并不一定会阻止“函数式编程”。