为什么constant()解决方案比“ Scala FP”中的简单解决方案5.8更有效?

时间:2019-01-04 19:45:18

标签: scala functional-programming lazy-evaluation

我正在看《 FP Scala》一书中的练习5.8,问题是:

“将其略微归纳到函数常量,这将返回给定值的无限流。”

def constant[A](a: A): Stream[A]

我的解决方法是:

def constant[A](a: A): Stream[A] =
  Stream.cons(a, constant(a))

当我提到标准解决方案时,它是:

// This is more efficient than `cons(a, constant(a))` since it's just
// one object referencing itself.
def constant[A](a: A): Stream[A] = {
  lazy val tail: Stream[A] = Cons(() => a, () => tail) 
  tail
}

显示“更有效”,请参见here

我能知道为什么效率更高吗? AFAIK,Streams中的cons构造函数已经将头部和尾部标记为惰性:

def cons[A](hd: => A, tl: => Stream[A]): Stream[A] = { 
  lazy val head = hd 
  lazy val tail = tl 
  Cons(() => head, () => tail) 
}

为什么我们仍然需要再次标记尾巴为懒惰?我对这里的要点不太了解。

2 个答案:

答案 0 :(得分:5)

这不仅仅是对@ElBaulP答案的评论,而不仅仅是其本身的答案,但对于评论来说太大了。

我认为您错过了优化的根源,即使上面的注释中明确指出了优化的根源。 val tail中的constantlazy的事实是实现细节,或者是使优化的主要来源成为可能的技巧。优化的主要来源是tail是自引用的。

暂时让我们摆脱所有lazy的内容。假设Cons被定义为

case class Cons[+A](h: A, t: () => Stream[A]) extends Stream[A]

然后将constant定义为

def constant[A](a: A): Stream[A] = {
   val tailFunc: () => Stream[A] =  () => tail
   val tail: Stream[A] = Cons(a, tailFunc)
   tail
}

是的,这是一个语法无效的程序,因为tailFunc使用仅在下一行中定义的tail。但是想象一下Scala可以编译它。我认为现在很清楚,constant的这种实现每次调用只会创建一个类Cons的实例,并且无论您尝试迭代返回的流多长时间,都将使用该实例。

tail定义为lazy所允许的只是使代码在逻辑上等同于上述编译而没有错误。从实现的角度来看,它类似于以下内容:

class Lazy[A](var value:A)

def constant[A](a: A): Stream[A] = {
   val lazyTail: Lazy[Stream[A]] = new Lazy(null)
   // this tailFunc works because lazyTail is already fixed and can be safely 
   // captured although lazyTail.value will be changed
   val tailFunc: () => Stream[A] =  () => lazyTail.value 
   lazyTail.value = new Stream(a, tailFunc)
   lazyTail.value
}

此代码在很多细节上与实际的lazy实现并不完全匹配,因为它确实很渴望,但我认为它表明您并不需要lazy来进行优化(但在使用var的成本,Scala编译器在其实际的,更复杂的lazy实现中仍会这样做)。

在您幼稚的实现中

def constant[A](a: A): Stream[A] =  Stream.cons(a, constant(a))

tail的惰性求值使您不会立即由于堆栈递归调用constant而导致堆栈溢出而不会立即失败。但是仍然在执行constant(whatever).tail时会评估tail,因此再次调用constant方法并创建一个新的Cons对象。每当在新的tail上调用head时,就会发生这种情况。

再次重申一下,lazy val tail只是允许tail在创建时引用自身的一种技巧,而真正重要的部分是tail引用自身的事实只能使用一个对象进行迭代,而不是每次下一个.tail调用都使用一个对象。

答案 1 :(得分:4)

我认为这是因为使用懒惰的实现,您只创建对象一次并对其进行记忆,因此,当您调用constant时,您是在引用同一对象再来一次,像这样:

tail -----
  ^------'

有了您的实现(与我在阅读本书时遇到的一样),您每次调用该函数都将创建新对象。因此,如果您多次调用实现,那么您将:

Stream.cons(a, Stream.cons(a, Stream.cons(a, constant(a)))) 

让我们看一个例子:

def constant[A](a: A): Stream[A] = { println("CALLED"); cons(a, constant(a)) }

// This is more efficient than `cons(a, constant(a))` since it's just
// one object referencing itself.
def constant_1[A](a: A): Stream[A] = {
   lazy val tail: Stream[A] = Cons(() ⇒ a, () ⇒ tail)
   println("CALLED_1")
   tail
  }

println(s"Cons ${Stream.constant_1(0).take(10).toListFast}")
println(s"Cons ${Stream.constant(0).take(10).toListFast}")

上面的代码产生以下输出:

CALLED_1
Cons List(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
CALLED
CALLED
CALLED
CALLED
CALLED
CALLED
CALLED
CALLED
CALLED
CALLED
Cons List(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)

如您所见,惰性实现的println语句仅被调用一次。

有关详细说明,请参见@SergGr。