【问题标题】:for vs map in functional programming函数式编程中的 for vs map
【发布时间】:2015-05-23 20:10:06
【问题描述】:

我正在学习使用 scala 进行函数式编程。一般来说,我注意到 for 循环在函数式程序中的使用并不多,而是使用 map。

问题

  1. 在性能、可读性等方面,使用 map 优于 for 循环有哪些优势?

  2. 当可以使用循环实现时,引入地图功能的目的是什么?

程序 1:使用 For 循环

val num = 1 to 1000
val another = 1000 to 2000
for ( i <- num )
{
  for ( j <- another) 
  {
    println(i,j)
  }
}

程序 2:使用地图

val num = 1 to 1000
val another = 1000 to 2000
val mapper = num.map(x => another.map(y => (x,y))).flatten
mapper.map(x=>println(x))

程序 1 和程序 2 做同样的事情。

【问题讨论】:

  • 对于您给出的示例(打印输出),您不会使用 map 但会使用 iter。当您将集合元素从类型 A 转换为类型 B 时,您将使用 Map。
  • 请注意,Scala 中的for 循环被编译器转换为包括mapflatMap 在内的操作——例如,参见this question
  • @DNA 所以for循环只是一个语法糖,它会被编译器转换为map和flatMap。
  • 语法盐,更多。我想说,向那些努力转型的人致敬。

标签: scala functional-programming


【解决方案1】:

答案其实很简单。

每当您在集合上使用循环时,它都有语义目的。要么你想迭代集合的项目并打印它们。或者您想将元素的类型转换为另一种类型(地图)。或者您想更改基数,例如计算集合元素的总和(折叠)。

当然,所有这些都可以使用 for - 循环来完成,但是对于代码的读者来说,与众所周知的命名操作(例如 map、iter ,折叠,过滤,...

另一方面,for 循环会导致使用可变状态的阴暗面。在没有可变状态的情况下,您将如何在 for 循环中对集合的元素求和?你不会。相反,您需要编写一个递归函数。所以,为了更好的衡量,最好早点放弃思考 for 循环的习惯,享受勇敢的新功能做事方式。

【讨论】:

  • 这意味着避免了for循环,因为这让程序员想到了一个递归函数来解决问题。
  • 我花了几个月的时间来适应它。但是一旦你准备好新的“技巧包”,你就不会真正错过循环,并且“我需要一个循环”的想法会导致你的手指输入递归的东西。但由于我不是 scala 程序员,所以请注意。我不确定 Scala 是否支持尾递归。不过,我也不确定是否相反。
  • Scala 确实有一些尾递归支持 - 请参阅 this question
【解决方案2】:

我将首先引用 Scala 中的编程。 “每个 for 表达式都可以用三个高阶函数 map、flatMap 和 filter 来表示。本节介绍翻译方案,Scala 编译器也使用该方案。” http://www.artima.com/pins1ed/for-expressions-revisited.html#23.4

所以你注意到 for 循环没有被大量使用的原因是因为它们在技术上是不需要的,而且你看到的任何 for 表达式都只是语法糖,编译器会将其翻译成某种等价物。将 for 表达式转换为 map/flatMap/filter 表达式的规则在上面的链接中列出。

一般来说,在函数式编程中,没有要变异的索引变量。这意味着人们通常会大量使用函数调用(通常以递归的形式),例如用列表折叠代替 while 或 for 循环。

关于使用列表折叠代替 while/for 循环的一个很好的例子,我推荐 Tony Morris 的“Explain List Folds to Yourself”。 https://vimeo.com/64673035

如果一个函数是尾递归的(用@tailrec 表示),那么可以对其进行优化,以免引起递归函数中常见的堆栈的高使用率。在这种情况下,编译器可以将尾递归函数转换为“while 循环等效”。

为了回答问题 1 的第二部分,在某些情况下,可以提出 for 表达式更清晰的论点(当然也有相反的情况。) Coursera 中给出了一个这样的例子.org 课程“使用 Scala 进行函数式编程”,Martin Odersky 博士:

for {
  i <- 1 until n
  j <- 1 until i
  if isPrime(i + j)
} yield (i, j)

可以说比

更清晰
(1 until n).flatMap(i =>
  (1 until i).withFilter(j => isPrime(i + j))
    .map(j => (i, j)))

有关更多信息,请查看 Martin Odersky 博士在 Coursera.org 上的“使用 Scala 进行函数式编程”课程。第 6.5 课“For 的翻译”特别详细讨论了这一点。

另外,作为一个快速的旁注,在你的例子中你使用

mapper.map(x => println(x))

在这种情况下使用 foreach 通常更受欢迎,因为您有副作用的意图。还有就是短手

mapper.foreach(println)

至于问题2,最好用map函数代替循环(尤其是循环中有突变的时候),因为map是一个函数,可以组合。而且,一旦熟悉并习惯了使用地图,就很容易推理。

【讨论】:

    【解决方案3】:

    您提供的两个程序不同相同,即使输出可能表明它们是相同的。确实,for 理解被编译器脱糖,但你拥有的第一个程序实际上相当于:

    val num = 1 to 1000
    val another = 1000 to 2000
    num.foreach(i => another.foreach(j => println(i,j)))
    

    需要注意的是,上面(和你的示例程序)的结果类型是Unit

    在您的第二个程序的情况下,程序的结果类型是由编译器确定的Seq[Unit] - 现在是Seq,具有循环成员乘积的长度。因此,您应该始终使用foreach 来指示导致Unit 结果的效果。

    【讨论】:

      【解决方案4】:

      想想在机器语言层面发生了什么。循环仍然是基本的。函数式编程抽象了传统编程中实现的循环。

      本质上,不是像在传统或非对等编程中那样编写循环,而是在函数式编程中使用链接或流水线允许编译器为用户优化代码,而 map 只是将函数映射到每个元素遍历列表或集合。函数式编程更方便,并且抽象了“for”循环等的普通实现。这种便利性存在局限性,特别是如果您打算使用函数式编程来实现并行处理。

      根据软件工程师或开发人员的不同,编译器会更有效率并提前了解它的实现情况,这是有争议的。恕我直言,熟悉函数式编程的中级软件工程师,精通传统的编程和并行处理方面的知识,将实现传统和功能。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2023-03-12
        • 2018-10-24
        • 1970-01-01
        • 2011-02-26
        • 1970-01-01
        • 2021-05-01
        • 1970-01-01
        相关资源
        最近更新 更多