在延迟流中,保留共享意味着什么?

时间:2019-04-30 11:06:56

标签: scala functional-programming

我正在学习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版本中?同样,关于持续内存消耗的句子也不清楚。

2 个答案:

答案 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的一个实例。您示例中的Imp2unfold版本会在每次下一次调用时生成新的Cons,因此这两种方法都生成不共享Cons实例的流。