【问题标题】:When does Scala force a stream value?Scala 何时强制使用流值?
【发布时间】:2016-07-17 23:37:15
【问题描述】:

我对流很满意,但我承认我对这种行为感到困惑:

import collection.immutable.Stream
object StreamForceTest extends App {
  println("Computing fibs")
  val fibs: Stream[BigInt] = BigInt(0) #:: BigInt(1) #::
     fibs.zip(fibs.tail).map((x: (BigInt, BigInt)) => {
       println("Adding " + x._1 + " and " + x._2);
       x._1 + x._2
     })
  println("Taking first 5 elements")
  val fibs5 = fibs.take(5)
  println("Computing length of that prefix")
  println("fibs5.length = " + fibs5.length)
}

有输出

Computing fibs
Taking first 5 elements
Computing length of that prefix
Adding 0 and 1
Adding 1 and 1
Adding 1 and 2
fibs5.length = 5

为什么take(5)应该强制计算流的值, 而length 这样做吗?随便谁都不需要 看看这些值,但我会认为take 更多 可能比length 做到这一点。检查github上的源代码, 我们找到了take 的这些定义(包括一个有启发性的 评论):

override def take(n: Int): Stream[A] = (
  // Note that the n == 1 condition appears redundant but is not.
  // It prevents "tail" from being referenced (and its head being evaluated)
  // when obtaining the last element of the result. Such are the challenges
  // of working with a lazy-but-not-really sequence.
  if (n <= 0 || isEmpty) Stream.empty
  else if (n == 1) cons(head, Stream.empty)
  else cons(head, tail take n-1)
)

length:

override def length: Int = {
  var len = 0
  var left = this
  while (!left.isEmpty) {
    len += 1
    left = left.tail
  }
  len
}

headtail的定义是从具体的 子类(EmptyCons)。 (当然Empty 是一个对象,而不是一个 类,它的 headtail 的定义只是 throw 例外。)有一些微妙之处,但它们似乎与制作有关 确保 Constail 被延迟评估; head 定义直接来自关于 Scala 构造函数的第 0 课。 请注意,length 不会靠近 head,但它是 做强迫症。

所有这些都是关于 Scala 流有多接近的普遍困惑的一部分 是 Haskell 列表。我以为 Haskell 对待头部和尾部 对称(我不是一个认真的 Haskell 黑客),Scala 强制 在更多情况下进行头部评估。我想弄清楚 这些情况究竟是什么。

【问题讨论】:

  • 我对流不满意,但对于迭代器,我希望 take 变得懒惰。但我习惯了iterator.size 耗尽迭代器只是为了检查长度。是否存在其他一些上下文或语言,其中 take 意味着力量?只是好奇。我可以理解动词 take 听起来像是强制一个值。
  • 我不确定你有什么困惑(特别是,如果你真的“对流很满意”):.take(5) 只需要扫描流的前五个成员来计算结果。 .length 需要一直到最后一个元素来计算它们。真的,我只是看不出这有什么令人费解的地方。
  • @Dima 我不明白。 take 什么都不做,因为它仍然是惰性的,但长度必须强制。那么needs to scan through the first five members 是什么意思呢?另外,if you are really "comfortable with the streams"?例如,当我说“我对流媒体不满意”时,我打算使用“自嘲式幽默”,我希望它不会被视为讽刺。无论如何,我们的自我理解就是这样。
  • @Dima 谢谢;是的,当然我应该意识到take 中的递归将被暂停,因为递归调用在cons 内。但是如果你递归地写lengthif (s.isEmpty) 0 else 1 + tail.length,就会发现不对称。如果不强迫头部,您将无法访问尾部。它没有必须那样。
  • @airfoyle 恐怕我不明白你在说什么。也许,这对我来说太计算机科学了 :) 我的意思是,我不确定你在说什么“不对称”,但它不清楚,即使对实现一无所知,为了告诉流的长度,你必须扫描到最后,并设置一个限制,你可以遍历它......好吧,不需要做任何事情吗? :) 或者你的问题是关于实现的,因为你对 cons 的 pass-by-name 参数感到困惑?

标签: scala stream lazy-evaluation


【解决方案1】:

Stream 的头部是严格的,尾部是惰性的,正如您在 cons.applyCons constructor 中看到的那样:

def apply[A](hd: A, tl: => Stream[A]) = new Cons(hd, tl)

class Cons[+A](hd: A, tl: => Stream[A]) extends Stream[A]

注意take 方法引用tail 的上下文:

cons(head, tail take n-1)

因为表达式tail take n-1 用作cons 的第二个参数,它传递给by name,它不会强制评估tail take n-1,因此不会强制评估tail

而在length 中,声明

left = left.tail

,通过将left.tail 分配给var,确实会强制对其进行评估。

Scala 是“默认严格的”。在大多数 情况下,您引用的所有内容都会被评估。我们仅在方法/构造函数参数使用 =&gt; 声明按名称调用参数的情况下进行惰性评估,并且在文化中我们通常不使用它,除非有特殊原因。

【讨论】:

  • 我不确定by assigning left.tail to a var。只需说left.tail 即可调用它;正如他们所说,分配是正交的?也许您的意思是将 expr 按名称传递给方法不会对其进行评估。 in the culture 是一个非常有趣的短语。
  • 我的意思是除了进入一个按名称参数之外的任何上下文都会强制它,而赋值的 RHS 就是这样一个上下文。并不是说任务是特殊的。说“只要说 left.tail 就可以调用它”可能会产生误导,因为它确实取决于你说它的上下文。
【解决方案2】:

让我提供另一个答案,只是从高层次上看,即没有实际考虑代码。

如果您想知道 Stream 有多长,您必须从头到尾评估它。否则,你只能猜测它的长度。诚然,您可能并不真正关心这些值(因为您只想计算它们),但这并不重要。

另一方面,当您从流(或任何集合)中“获取”一定数量的元素时,您只是在说您最多想要该数量的元素。结果仍然是一个流,即使它可能已被截断。

【讨论】:

  • 这是真的吗?我可以想象一个 Stream 知道它会产生十个元素,但会懒惰地计算它们,因为运行每个计算需要一天的时间。从高层次上看,不看代码,我不明白“必须”评估长度。
  • @som-snytt 我也可以想象这样一个知道十个惰性求值元素的数据结构。但它不会是 Stream,因为 Stream 无法知道它的尾部,直到它被强制评估。
猜你喜欢
  • 2019-01-31
  • 1970-01-01
  • 2012-12-24
  • 2017-05-14
  • 2017-01-14
  • 2017-02-09
  • 1970-01-01
  • 2014-06-07
  • 2015-04-07
相关资源
最近更新 更多