有什么好的例子:“程序的操作应该将输入值映射到输出值而不是更改数据”

时间:2012-05-27 21:49:52

标签: scala functional-programming

我在Scala中发现了这句话来解释其功能行为

  

程序的操作应该将值的输入映射到输出值而不是更改数据

有人可以用一个很好的例子解释一下吗?

编辑:请在上下文中解释或举例说明上述句子,请不要让其变得更加混乱

4 个答案:

答案 0 :(得分:14)

这指的最明显的模式是,与Scala相比,编写使用Java集合的代码的方式不同。如果您正在编写scala但是在成语Java 中,那么您将通过适当地改变数据来处理集合。用于执行相同操作的惯用标量代码有利于将输入值映射到输出值。

让我们看一下你可能想对集合做的一些事情:

过滤

在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中的习惯用语是创建一个集合并 mutate 它。

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

这有什么好处?良好:

  1. 我的代码永远不会观察到部分构造的结果
  2. 将函数A => B应用于Coll[A]以获取Coll[B]更加清晰。
  3. 累积

    同样,在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实现中存在一个错误! 您是否发现了它?

答案 1 :(得分:2)

我之间的区别是:

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 example

答案 2 :(得分:1)

这意味着如果你像这样写pure functions,你总是从同一输入得到相同的输出,并且没有副作用,这使得更容易推理你的程序并确保它们是正确的。

所以例如函数:

def times2(x:Int) = x*2

是纯粹的,而

def add5ToList(xs: MutableList[Int]) {
    xs += 5
}

是不纯的,因为它将数据编辑为副作用。这是一个问题,因为相同的列表可能在程序的其他地方使用,现在我们无法保证行为,因为它已经改变。

纯版本将使用不可变列表并返回新列表

def add5ToList(xs: List[Int]) = {
    5::xs
}

答案 3 :(得分:1)

有很多收藏品的例子很容易找到,但可能给人留下错误的印象。此概念适用于该语言的所有级别(但不是在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!

这个概念适用于构造不可变集合本身:List永远不会改变。相反,必要时会创建新的List个对象。使用持久性数据结构可以减少在可变数据结构上发生的复制。