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

发布于 2024-10-28 00:49:31 字数 1462 浏览 0 评论 0原文

因此,在阅读这个问题时,有人指出,而不是程序代码:

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 成为比程序版本更好的选择吗?我可能认为它会更有效,但事实证明 foldLeft的实现实际上就是上面的程序代码。那么这只是一种风格选择,还是有充分的理由使用一个版本或另一个版本?

编辑:需要明确的是,我不是在问其他函数,只是在问foldLeft。我对使用 foreachmapfilter 等非常满意,它们都很好地映射到 for 推导式上。

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

  • 使用 foldLeft 因为还有其他优化,例如使用while 循环比 for 循环更快。
  • 如果将其添加到常规集合中,请使用折叠,因为这将使向并行集合的转换变得微不足道。

So in reading this question it was pointed out that instead of the procedural code:

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

You could write the following functional code:

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

I would almost certainly write the first version because coders familiar with either procedural or functional styles can easily read and understand it, while only coders familiar with functional style can easily read and understand the second version.

But setting readability aside for the moment, is there something that makes foldLeft a better choice than the procedural version? I might have thought it would be more efficient, but it turns out that the implementation of foldLeft is actually just the procedural code above. So is it just a style choice, or is there a good reason to use one version or the other?

Edit: Just to be clear, I'm not asking about other functions, just foldLeft. I'm perfectly happy with the use of foreach, map, filter, etc. which all map nicely onto for-comprehensions.

Answer: There are really two good answers here (provided by delnan and Dave Griffith) even though I could only accept one:

  • Use foldLeft because there are additional optimizations, e.g. using a while loop which will be faster than a for loop.
  • Use fold if it ever gets added to regular collections, because that will make the transition to parallel collections trivial.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

沫雨熙 2024-11-04 00:49:31

它更短、更清晰 - 是的,您需要知道折叠是什么才能理解它,但是当您使用 50% 函数式语言进行编程时,您无论如何都应该了解这些基本构建块。折叠正是过程代码所做的事情(重复应用操作),但它被赋予了名称并进行了概括。虽然你正在重新发明的只是一个小轮子,但它仍然是一个轮子的重新发明。

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

It's shorter and clearer - yes, you need to know what a fold is to understand it, but when you're programming in a language that's 50% functional, you should know these basic building blocks anyway. A fold is exactly what the procedural code does (repeatedly applying an operation), but it's given a name and generalized. And while it's only a small wheel you're reinventing, but it's still a wheel reinvention.

And in case the implementation of foldLeft should ever get some special perk - say, extra optimizations - you get that for free, without updating countless methods.

顾挽 2024-11-04 00:49:31

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

collection.foldLeft(0)(_+_) 

比任何等效的命令式构造要简单得多且短得多。

使用函数式收集操作的另一个元原因(尽管在本例中不直接适用)是为了在性能需要时转向使用并行收集操作。折叠不能并行化,但折叠操作通常可以转化为交换关联归约操作,并且这些操作可以并行化。使用 Scala 2.9,利用多个处理核心将某些内容从非并行功能更改为并行功能有时就像将 .par 拖放到要执行并行操作的集合上一样简单。

Other than a distaste for mutable variable (even mutable locals), the basic reason to use fold in this case is clarity, with occasional brevity. Most of the wordiness of the fold version is because you have to use an explicit function definition with a destructuring bind. If each element in the list is used precisely once in the fold operation (a common case), this can be simplified to use the short form. Thus the classic definition of the sum of a collection of numbers

collection.foldLeft(0)(_+_) 

is much simpler and shorter than any equivalent imperative construct.

One additional meta-reason to use functional collection operations, although not directly applicable in this case, is to enable a move to using parallel collection operations if needed for performance. Fold can't be parallelized, but often fold operations can be turned into commutative-associative reduce operations, and those can be parallelized. With Scala 2.9, changing something from non-parallel functional to parallel functional utilizing multiple processing cores can sometimes be as easy as dropping a .par onto the collection you want to execute parallel operations on.

她说她爱他 2024-11-04 00:49:31

我在这里还没有看到提到的一个词是声明性

声明式编程通常被定义为非命令式的任何编程风格。存在许多其他常见定义,试图为该术语提供一个定义,而不是简单地将其与命令式编程进行对比。例如:

  • 一个程序,描述应该执行什么计算以及而不是如何计算
  • 任何没有副作用(或更具体地说,引用透明)的编程语言
  • 一种与数学逻辑具有明确对应关系的语言。

这些定义实质上重叠。

高阶函数 (HOF) 是声明性的关键推动者,因为我们只指定内容(例如“使用这个值集合,将每个值乘以 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<5 for 循环更具声明性,因为我们不必对迭代状态和终止逻辑,但我们仍然容易受到差一错误的影响。

接下来,在sumDoubled2中,我们基本上处于Java>=5的水平。仍然有一些事情可能会出错,但我们已经非常擅长阅读这种代码形状,因此不太可能出现错误。 但是,不要忘记,在玩具示例中微不足道的模式在扩展到生产代码时并不总是那么可读!

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

当然,学习阅读功能版本需要时间,但我们已经彻底排除了犯错误的选择。 “业务逻辑”被明确标记,并且管道是从其他人正在阅读的同一个菜单中选择的。

One word I haven't seen mentioned here yet is declarative:

Declarative programming is often defined as any style of programming that is not imperative. A number of other common definitions exist that attempt to give the term a definition other than simply contrasting it with imperative programming. For example:

  • A program that describes what computation should be performed and not how to compute it
  • Any programming language that lacks side effects (or more specifically, is referentially transparent)
  • A language with a clear correspondence to mathematical logic.

These definitions overlap substantially.

Higher-order functions (HOFs) are a key enabler of declarativity, since we only specify the what (e.g. "using this collection of values, multiply each value by 2, sum the result") and not the how (e.g. initialize an accumulator, iterate with a for loop, extract values from the collection, add to the accumulator...).

Compare the following:

// 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

Note that our first example, sumDoubled1, is already more declarative than (most would say superior to) C/C++/Java<5 for loops, because we haven't had to micromanage the iteration state and termination logic, but we're still vulnerable to off-by-one errors.

Next, in sumDoubled2, we're basically at the level of Java>=5. There are still a couple things that can go wrong, but we're getting pretty good at reading this code-shape, so errors are quite unlikely. However, don't forget that a pattern that's trivial in a toy example isn't always so readable when scaled up to production code!

With sumDoubled3, desugared for didactic purposes, and sumDoubled4, the idiomatic Scala version, the iteration, initialization, value extraction and choice of return value are all gone.

Sure, it takes time to learn to read the functional versions, but we've drastically foreclosed our options for making mistakes. The "business logic" is clearly marked, and the plumbing is chosen from the same menu that everyone else is reading from.

撩人痒 2024-11-04 00:49:31

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

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

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

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

It is worth pointing out that there is another way of calling foldLeft which takes advantages of:

  • The ability to use (almost) any Unicode symbol in an identifier
  • The feature that if a method name ends with a colon :, and is called infix, then the target and parameter are switched

For me this version is much clearer, because I can see that I am folding the expr value into the replacements collection

def expand(expr: String, replacements: Traversable[(String, String)]): String = {
  (expr /: replacements) { case (r, (o, n)) => r.replace(o, n) }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文