我正在看《 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)
}
为什么我们仍然需要再次标记尾巴为懒惰?我对这里的要点不太了解。
答案 0 :(得分:5)
这不仅仅是对@ElBaulP答案的评论,而不仅仅是其本身的答案,但对于评论来说太大了。
我认为您错过了优化的根源,即使上面的注释中明确指出了优化的根源。 val tail
中的constant
是lazy
的事实是实现细节,或者是使优化的主要来源成为可能的技巧。优化的主要来源是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。