函数式编程中的 for vs map

for vs map in functional programming

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

问题

  1. 在性能、可读性等方面使用 map over for 循环有什么优势?

  2. 既然可以用loop实现,那么引入map函数的用意是什么?

程序一:使用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 做同样的事情。

其实答案很简单。

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

当然,所有这些也可以使用 for - 循环来完成,但是对于代码的 reader,与众所周知的命名相比,要弄清楚循环的语义目​​的需要更多的工作map、iter、fold、filter 等操作...

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

您提供的两个程序相同,即使输出可能表明它们是相同的。 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 结果的效果。

我将从引用 Scala 编程 开始。 "Every for expression can be expressed in terms of the three higher-order functions map, flatMap and filter. This section describes the translation scheme, which is also used by the Scala compiler." http://www.artima.com/pins1ed/for-expressions-revisited.html#23.4

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

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

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

如果函数是尾递归的(用@tailrec 表示),那么可以对其进行优化,以免导致递归函数中常见的堆栈使用率过高。在这种情况下,编译器可以将尾递归函数转换为 "while loop equivalent".

为了回答问题 1 的第二部分,在某些情况下可以论证 for 表达式更清晰(尽管当然也有相反的情况。)中给出了一个这样的例子Martin Odersky 博士的 Coursera.org 课程 "Functional Programming with Scala":

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 的 "Functional Programming with Scala" 课程。第 6.5 讲 "Translation of For" 特别对此进行了更详细的讨论。

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

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

在这种情况下使用 foreach 通常更容易被接受,因为您有副作用的意图。还有,还有简写

mapper.foreach(println)

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

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

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

根据软件工程师或开发人员的不同,编译器会更高效并且提前知道它的实现情况是有争议的。恕我直言,熟悉函数式编程的中级软件工程师,嗯精通常规编程,熟悉并行处理,将实现常规和功能。