为什么使用foldLeft而不是程序版?

时间:2011-03-28 14:10:12

标签: scala

所以在阅读this question时,有人指出,而不是程序代码:

def expand(exp: String, replacements: Traversable[(String, String)]): String = {
  var result = exp
  for ((oldS, newS) <- replacements)
    result = result.replace(oldS, newS)
  result
}

您可以编写以下功能代码:

def expand(exp: String, replacements: Traversable[(String, String)]): String = {
  replacements.foldLeft(exp){
    case (result, (oldS, newS)) => result.replace(oldS, newS)
  }
}

我几乎肯定会编写第一个版本,因为熟悉程序或函数样式的编码人员可以轻松阅读和理解它,而只有熟悉函数风格的编码人员才能轻松阅读和理解第二个版本。

但是暂时将可读性放在一边,有没有什么能使foldLeft成为比程序版更好的选择?我可能认为它会更有效率,但事实证明implementation of foldLeft实际上只是上面的程序代码。那么它只是一种风格选择,还是有充分理由使用一个版本或另一个版本?

编辑:为了清楚起见,我不是在询问其他功能,只是foldLeft。我对使用foreachmapfilter等非常满意,这些都可以很好地映射到for-comprehensions。

答案:这里确实有两个很好的答案(由delnanDave Griffith提供),即使我只能接受一个:

  • 使用foldLeft因为还有其他优化,例如使用一个比for循环更快的while循环。
  • 如果它被添加到常规集合中,请使用fold,因为这会使向并行集合的过渡变得微不足道。

4 个答案:

答案 0 :(得分:16)

它更短更清晰 - 是的,您需要知道什么是折叠才能理解它,但是当您使用50%功能的语言进行编程时,您应该知道这些基本构建块。折叠正是程序代码所做的(重复应用操作),但它给出了一个名称并进行了概括。虽然它只是一个小轮子你正在重新发明,但它仍然是轮子改造。

如果foldLeft 的实现应该获得一些特殊的好处 - 比如额外的优化 - 你可以免费获得,而无需更新无数的方法。

答案 1 :(得分:9)

除了对可变变量(甚至是可变的本地人)的厌恶之外,在这种情况下使用折叠的基本原因是清晰度,偶尔简洁。 fold版本的大部分含义是因为你必须使用带有解构绑定的显式函数定义。如果列表中的每个元素在折叠操作中仅使用一次(常见情况),则可以简化为使用缩写形式。因此,经典定义了数字集合的总和

collection.foldLeft(0)(_+_) 

比任何等效的命令式构造更简单,更短。

使用功能集合操作的另一个元原因虽然在这种情况下不直接适用,但是如果性能需要,可以使用并行集合操作。折叠无法并行化,但折叠操作通常可以转换为可交换关联的减少操作,并且可以并行化。使用Scala 2.9,使用多个处理核心将某些内容从非并行功能更改为并行功能有时可以像将.par放到要执行并行操作的集合上一样简单。

答案 2 :(得分:8)

我还没有看到这里提到的一个词是 declarative

  

声明性编程通常被定义为任何非必要的编程风格。存在许多其他常见定义,试图将该术语定义为除了简单地将其与命令式编程进行对比之外的定义。例如:

     
      
  • 描述应执行哪些计算的程序 计算
  • 的程序   
  • 任何缺乏副作用的编程语言(或更具体地说,是参考透明的)
  •   
  • 与数学逻辑明确对应的语言。
  •   
     

这些定义大致重叠。

高阶函数(HOF)是声明性的关键推动因素,因为我们只指定 what (例如“使用这个值集合,将每个值乘以2,求和结果”)而不是如何(例如初始化累加器,用for循环迭代,从集合中提取值,添加到累加器......)。

比较以下内容:

// Sugar-free Scala (Still better than Java<5)
def sumDoubled1(xs: List[Int]) = {
  var sum = 0                     // Initialized correctly?
  for (i <- 0 until xs.size) {    // Fenceposts?
    sum = sum + (xs(i) * 2)       // Correct value being extracted? 
                                  // Value extraction and +/* smashed together
  }       
  sum                             // Correct value returned?
}

// Iteration sugar (similar to Java 5)
def sumDoubled2(xs: List[Int]) = {
  var sum = 0
  for (x <- xs)          // We don't need to worry about fenceposts or 
    sum = sum + (x * 2)  // value extraction anymore; that's progress
  sum                               
}

// Verbose Scala
def sumDoubled3(xs: List[Int]) = xs.map((x: Int) => x*2). // the doubling
                                    reduceLeft((x: Int, y: Int) => x+y) // the addition

// Idiomatic Scala
def sumDoubled4(xs: List[Int]) = xs.map(_*2).reduceLeft(_+_)
//                                       ^ the doubling  ^
//                                                       \ the addition

请注意,我们的第一个示例sumDoubled1已经比C(C ++ / Java&lt; 5 for循环)更具声明性,因为我们不必对迭代状态和终止进行微观管理。逻辑,但我们仍然容易受到一个错误的影响。

接下来,在sumDoubled2中,我们基本上处于Java&gt; = 5的级别。还有一些问题可能会出错,但我们在阅读这种代码形式方面已经相当不错,因此错误的可能性很小。 然而,不要忘记在扩展到生产代码时,玩具示例中的模式并不总是那么可读!

对于sumDoubled3,为了教学目的而去了,而sumDoubled4,惯用的Scala版本,迭代,初始化,值提取和返回值的选择都消失了。

当然,学习阅读功能版本需要时间,但我们已经大大取消了我们犯错误的选择。 “业务逻辑”已清楚标记,管道从其他人正在阅读的相同菜单中选择。

答案 3 :(得分:2)

值得指出的是,还有另一种调用foldLeft的方法,它利用了以下优势:

  • 在标识符中使用(几乎)任何Unicode符号的能力
  • 如果方法名称以冒号:结尾,并且称为中缀,则切换目标和参数的功能

对我来说这个版本更加清晰,因为我可以看到我将expr折叠成 replacements集合

def expand(expr: String, replacements: Traversable[(String, String)]): String = {
  (expr /: replacements) { case (r, (o, n)) => r.replace(o, n) }
}
相关问题