Scala的理解效率如何?

时间:2010-11-18 02:23:37

标签: scala for-comprehension

在第23章“Scala编程”一书中,作者给出了一个例子:

case class Book(title: String, authors: String*)
val books: List[Book] = // list of books, omitted here
// find all authors who have published at least two books

for (b1 <- books; b2 <- books if b1 != b2;
    a1 <- b1.authors; a2 <- b2.authors if a1 == a2)
yield a1

作者说,这将转化为:

books flatMap (b1 =>
   books filter (b2 => b1 != b2) flatMap (b2 =>
      b1.authors flatMap (a1 =>
        b2.authors filter (a2 => a1 == a2) map (a2 =>
           a1))))

但是如果你看一下map和flatmap方法定义( TraversableLike.scala ),你可能会发现,它们被定义为for循环:

   def map[B, That](f: A => B)(implicit bf: CanBuildFrom[Repr, B, That]): That = {
    val b = bf(repr)
    b.sizeHint(this) 
    for (x <- this) b += f(x)
    b.result
  }

  def flatMap[B, That](f: A => Traversable[B])(implicit bf: CanBuildFrom[Repr, B, That]): That = {
    val b = bf(repr)
    for (x <- this) b ++= f(x)
    b.result
  }

好吧,我想这个for将不断翻译成foreach,然后翻译成while语句,这是一个构造而不是表达式,scala没有for构造,因为它希望for总是产生一些东西。

所以,我想与你讨论的是,为什么Scala这样做“翻译”? 作者的例子使用了4个生成器,最后将被转换为4级嵌套for循环,我认为当books很大时它会有非常可怕的性能。

Scala鼓励人们使用这种“语法糖”,你总能看到大量使用过滤器,地图和平面图的代码,似乎程序员忘记了他们真正做的是将一个循环嵌套在另一个循环中,以及什么实现只是使代码看起来更短。你有什么想法?

5 个答案:

答案 0 :(得分:7)

因为理解是monadic变换的语法糖,因此在各种各样的地方都很有用。在那里,它们在Scala中比在等效的Haskell构造中更冗长(当然,Haskell默认情况下是非严格的,因此无法像Scala那样谈论构造的性能)。

同样重要的是,这个结构可以保持清晰的内容,并避免快速升级缩进或不必要的私有方法嵌套。

至于最后的考虑,无论是否隐藏复杂性,我都会认为:

for {
  b1 <- books
  b2 <- books
  if b1 != b2
  a1 <- b1.authors
  a2 <- b2.authors 
  if a1 == a2
} yield a1

很容易看出正在做什么,复杂性很明显:b ^ 2 * a ^ 2(过滤器不会改变复杂性),书籍数量和作者数量。现在,使用深度缩进或私有方法在Java中编写相同的代码,并尝试快速确定代码的复杂性。

所以,imho,这并不掩盖复杂性,相反,却说清楚了。

至于您提到的map / flatMap / filter定义,它们不属于List或任何其他类,因此不会应用它们。基本上,

for(x <- List(1, 2, 3)) yield x * 2

被翻译成

List(1, 2, 3) map (x => x * 2)

这与

不一样
map(List(1, 2, 3), ((x: Int) => x * 2)))

这是你传递的定义的调用方式。为了记录,mapList的实际实施是:

def map[B, That](f: A => B)(implicit bf: CanBuildFrom[Repr, B, That]): That = {
  val b = bf(repr)
  b.sizeHint(this) 
  for (x <- this) b += f(x)
  b.result
}

答案 1 :(得分:6)

我编写代码以便于理解和维护。然后我介绍一下如果存在瓶颈,那就是我投入的注意力。如果它像你所描述的那样,我将以不同的方式攻击问题。在那之前,我喜欢“糖”。这样可以省去写东西或思考它的麻烦。

答案 2 :(得分:4)

实际上有6个循环。每个过滤器/ flatMap / map

的一个循环

filter-&gt; map对可以通过使用集合的延迟视图(迭代器方法)在一个循环中完成

一般来说,tt正在为书籍运行2个嵌套循环以查找所有书籍对,然后运行两个嵌套循环以查找一本书的作者是否在另一本书的作者列表中。

使用简单的数据结构,在明确编码时也会这样做。

当然,这里的例子是显示一个复杂的'for'循环,而不是编写最有效的代码。例如,可以使用一个Set而不是一系列作者,然后查找交集是否为空:

for (b1 <- books; b2 <- books; a <- (b1.authors & b2.authors)) yield a

答案 3 :(得分:2)

请注意,在2.8中,filter调用已更改为withFilter这是懒惰的,并且会避免构建中间结构。请参阅guide to move from filter to withFilter?

我认为for被翻译为mapflatMapwithFilter(以及值定义,如果存在)的原因是为了更容易使用monad

一般来说,我认为如果您正在进行的计算涉及循环4次,那么使用for循环就可以了。如果计算可以更有效地完成并且性能很重要,那么您应该使用更有效的算法。

答案 4 :(得分:0)

@ IttayD对算法效率的回答的一个后续行动。值得注意的是,原始帖子(以及书中)中的算法是嵌套循环连接。实际上,对于大型数据集而言,这不是一种有效的算法,而且大多数数据库在此处使用哈希聚合。在Scala中,哈希聚合看起来像:

(for (book <- books;
      author <- book.authors) yield (book, author)
).groupBy(_._2).filter(_._2.size > 1).keys