我正在学习Scala中的Functional Programming一书。这是Stream定义和函数的代码段,这些代码段使用智能构造函数并使用constant
来构造unfold
:
sealed trait Stream[+A]
case object Empty extends Stream[Nothing]
case class Cons[+A](h: () => A, tl: () => Stream[A]) extends Stream[A]
object Stream {
def cons[A](h: => A, tl: => Stream[A]): Stream[A] = {
lazy val head = h
lazy val tail = tl
Cons(() => head, () => tail)
}
def empty[A]: Stream[A] = Empty
def constant[A](a: A): Stream[A] = cons(a, constant(a))
def unfold[A, S](z: S)(f: S => Option[(A, S)]): Stream[A] =
f(z).fold(empty[A])(x => cons(x._1, unfold(x._2)(f)))
def constantViaUnfold[A](a: A): Stream[A] = unfold(a)(x => Some((x, x)))
}
有一个脚注说:
使用
,共享也会被破坏unfold
定义constant
意味着我们不会像递归定义那样获得共享。即使我们遍历遍历它时,递归定义也会消耗常量内存,而基于展开的实现则不会。在使用流进行编程时,保留共享并不是我们通常依赖的东西,因为共享极其精细并且不受类型的跟踪。例如,即使呼叫xs.map(x => x).
当作者说我们没有得到分享时,这是什么意思?究竟共享了什么?为什么不保留在unfold
版本中?同样,关于持续内存消耗的句子也不清楚。
答案 0 :(得分:4)
假设您创建了这样的新列表:
val n0 = Nil //List()
val n1 = 1 :: n0 //List(1)
val n2 = 2 :: n1 //List(2,1)
val n3 = 3 :: n2 //List(3,2,1)
如果您建立这样的列表,很容易注意到 n3 确实是 n2 ,其中带有 3 和 n2 < / em>只是 n1 加上 2 的前缀,依此类推。因此, 共享了对 1 的引用n1 , n2 和 n3 和 2 的引用由 n2 和 n2 ,等等。 您也可以这样写:
Cons(3, Cons(2, Cons(1, Nil)))
在您以递归方式创建 Stream 的 FPinS 中的示例中也是如此。您的 Stream 是由嵌套的 Cons 构建的,每个子流都与其父元素共享元素。因此,在创建下一个 Stream 时,只需将旧内容包装在 Cons 中并添加新元素。但是由于 Stream 是懒惰的,因此只有在具体化时,才能完成所有 Cons 层次结构的构建,例如通过调用 toList < / strong>。
像这样构建流还会使您不断消耗内存,因为从上一个创建下一个流只会消耗新元素的内存。
为什么unfold
不是这种情况?因为它可以“其他方式”构建Stream。就像这样:
Cons(x, next_iteration_of_unfold) //1st iteration
Cons(x, Cons(x, next_iteration_of_unfold)) //2nd iteration
Cons(x, Cons(x, Cons(x, next_iteration_of_unfold))) //3rd iteration, etc.
因此您可以看到没有任何东西可以共享。
编辑:
通过在书的最后implementation上调用take
并将toString
添加到Cons
,可以看到实例化流的外观:
override def toString: String = s"Cons(${h()}, ${tl()})"
然后:
Stream.ones.take(3)
您将看到:
Cons(1, Cons(1, Cons(1, Empty)))
答案 1 :(得分:2)
为了避免引起混淆,我将提供原始书籍(2014年版)的报价:
使用展开来定义常量和1意味着我们不会像递归定义那样得到共享 val个:Stream [Int] = cons(1,个)。递归定义消耗常量内存,即使我们 在遍历时对其进行引用,而基于展开的实现则不会。在使用流进行编程时,保留共享并不是我们通常所依赖的,因为它非常微妙 而不是按类型跟踪。例如,即使调用xs.map(x => x),共享也会被破坏。
如您所见,这里的问题有所不同。这与重用Cons
实例有关,因此Stream
仅在此处引用自身。
因此,作者认为它具有这样的实现方式:
//Imp1
def constant[A](a: A): Stream[A] = {
lazy val ones: Stream[A] = cons(a, ones)
ones
}
而不是
//Imp2
def constant[A](a: A): Stream[A] = cons(a, constant(a))
如您所见,Imp1
仅为整个流创建Cons
的一个实例。您示例中的Imp2
和unfold
版本会在每次下一次调用时生成新的Cons
,因此这两种方法都生成不共享Cons
实例的流。