我在 Scala 中遇到了这句话来解释它的功能行为。
程序的操作应该将值的输入映射到输出值,而不是就地更改数据
有人可以用一个很好的例子来解释吗?
编辑: 请在上下文中解释或举例说明上述句子,请不要使其复杂化以引起更多混乱
我在 Scala 中遇到了这句话来解释它的功能行为。
程序的操作应该将值的输入映射到输出值,而不是就地更改数据
有人可以用一个很好的例子来解释吗?
编辑: 请在上下文中解释或举例说明上述句子,请不要使其复杂化以引起更多混乱
这里所指的最明显的模式是,与 Scala 相比,在 Java 中编写使用集合的代码的方式是不同的。如果您正在编写 scala 但使用Java 的习语,那么您将通过在适当的位置改变数据来处理集合。执行相同操作的惯用scala 代码有利于将输入值映射到输出值。
让我们看一下您可能想要对集合执行的一些操作:
在 Java 中,如果我有一个List<Trade>
并且我只对与Deutsche Bank执行的交易感兴趣,我可能会执行以下操作:
for (Iterator<Trade> it = trades.iterator(); it.hasNext();) {
Trade t = it.next();
if (t.getCounterparty() != DEUTSCHE_BANK) it.remove(); // MUTATION
}
在这个循环之后,我的trades
收藏只包含相关的交易。但是,我使用突变实现了这一点——粗心的程序员很容易错过trades
输入参数、实例变量或方法中其他地方使用的参数。因此,他们的代码现在很可能被破坏了。此外,出于同样的原因,这样的代码对于重构来说非常脆弱;希望重构一段代码的程序员必须非常小心,不要让变异的集合逃出它们打算使用的范围,反之亦然,他们不会意外地在应该使用的地方使用未变异的集合使用了一个变异的。
与 Scala 比较:
val db = trades filter (_.counterparty == DeutscheBank) //MAPPING INPUT TO OUTPUT
这将创建一个新集合!它不会影响正在查看的任何人,trades
并且本质上更安全。
假设我有一个List<Trade>
并且我想为Set<Stock>
我一直在交易的独特股票获得一个。同样,Java 中的习惯用法是创建一个集合并对其进行变异。
Set<Stock> stocks = new HashSet<Stock>();
for (Trade t : trades) stocks.add(t.getStock()); //MUTATION
使用 scala 正确的做法是映射输入集合,然后转换为集合:
val stocks = (trades map (_.stock)).toSet //MAPPING INPUT TO OUTPUT
或者,如果我们关心性能:
(trades.view map (_.stock)).toSet
(trades.iterator map (_.stock)).toSet
这里有什么优势?好:
A => B
到 aColl[A]
以获取 aColl[B]
更加清晰。同样,在 Java 中,成语必须是突变。假设我们试图将我们所做交易的十进制数量相加:
BigDecimal sum = BigDecimal.ZERO
for (Trade t : trades) {
sum.add(t.getQuantity()); //MUTATION
}
同样,我们必须非常小心,不要意外观察到部分构造的结果!在 scala 中,我们可以在一个表达式中做到这一点:
val sum = (0 /: trades)(_ + _.quantity) //MAPPING INTO TO OUTPUT
或其他各种形式:
(trades.foldLeft(0)(_ + _.quantity)
(trades.iterator map (_.quantity)).sum
(trades.view map (_.quantity)).sum
哦,顺便说一句,Java 实现中有一个错误! 你发现了吗?
我会说这是之间的区别:
var counter = 0
def updateCounter(toAdd: Int): Unit = {
counter += toAdd
}
updateCounter(8)
println(counter)
和:
val originalValue = 0
def addToValue(value: Int, toAdd: Int): Int = value + toAdd
val firstNewResult = addToValue(originalValue, 8)
println(firstNewResult)
这是一个粗略的简化,但更完整的示例是使用 foldLeft 来建立结果而不是自己做艰苦的工作:foldLeft 示例
这意味着如果你编写这样的纯函数,你总是从相同的输入中得到相同的输出,并且没有副作用,这使得你的程序更容易推理并确保它们是正确的。
例如函数:
def times2(x:Int) = x*2
是纯净的,而
def add5ToList(xs: MutableList[Int]) {
xs += 5
}
是不纯的,因为它会在适当的位置编辑数据作为副作用。这是一个问题,因为同一个列表可能在程序的其他地方使用,现在我们不能保证行为,因为它已经改变了。
纯版本将使用不可变列表并返回一个新列表
def add5ToList(xs: List[Int]) = {
5::xs
}
有很多关于收藏的例子,很容易找到,但可能会给人留下错误的印象。这个概念适用于语言的所有级别(但它不适用于 VM 级别)。一个例子是案例类。考虑以下两种选择:
// Java-style
class Person(initialName: String, initialAge: Int) {
def this(initialName: String) = this(initialName, 0)
private var name = initialName
private var age = initialAge
def getName = name
def getAge = age
def setName(newName: String) { name = newName }
def setAge(newAge: Int) { age = newAge }
}
val employee = new Person("John")
employee.setAge(40) // we changed the object
// Scala-style
case class Person(name: String, age: Int) {
def this(name: String) = this(name, 0)
}
val employee = new Person("John")
val employeeWithAge = employee.copy(age = 40) // employee still exists!
这个概念适用于不可变集合本身的构造:aList
永远不会改变。相反,List
在必要时会创建新对象。持久数据结构的使用减少了在可变数据结构上发生的复制。