Scala:流不懒惰?

14

我知道在Scala中,流应该是一种惰性求值的序列,但是我认为我遭受了某种基本误解,因为它们似乎比我预期的更加急切。

在这个例子中:

 val initial = Stream(1)
 lazy val bad = Stream(1/0)
 println((initial ++ bad) take 1)

我遇到了java.lang.ArithmeticException异常,看起来是由于零除导致的。我原本以为只请求一次流,因此bad永远不会被评估。出了什么问题?

3个回答

21

流是惰性的事实并不改变方法参数会被急切地计算的事实。

Stream(1/0) 扩展为 Stream.apply(1/0)。语言的语义要求在调用方法之前评估参数(因为Stream.apply方法不使用按名称调用的参数),因此它尝试评估 1/0 以作为传递给 Stream.apply 方法的参数,这会导致算术异常。

不过,您可以通过几种方式使其正常工作。由于您已经将 bad 声明为 lazy val,最简单的方法可能是使用同样惰性的 #::: 流连接运算符来避免强制评估:

val initial = Stream(1)
lazy val bad = Stream(1/0)
println((initial #::: bad) take 1)
// => Stream(1, ?)

1
@Senthil在他的回答中提出了一个很好的观点,即Stream的头部总是被急切地评估,因此如果您将除零代码移动到尾部,则会起作用:1#::(1/0)#:: Stream.empty。但是,如果您调用Stream工厂方法,则仍将因我上面解释的相同原因而中断。 - DaoWen
3
说语言需要在调用方法之前对参数进行评估是非常误导人的。对于按值传递参数是正确的,但Scala有按名称传递参数的功能。正是这种功能使得可以将#::方法实现为某种惰性操作符。只是Stream.apply具有varargs参数,这些参数必须按值传递(因此在调用之前会被评估,就像您所解释的那样)。 - Régis Jean-Gilles
@RégisJean-Gilles - 该语言在调用函数之前仍会评估所有参数,因为这是底层JVM的要求,即使是按名称传递的参数也是如此。不同之处在于,按名称传递的参数被包装在Lambda中,Lambda的主体直到调用它时才被评估。 - DaoWen
1
不是真的。JVM底层发生的事情与语言的语义不同。按名称参数在底层上实现为Function0,但在语言层面上,=> T() => T仍然不同。 - Régis Jean-Gilles
实际上 => T 不是一种一等类型,而 () => T 是。 - Régis Jean-Gilles
@RégisJean-Gilles - 这是真的,它不是语义,而是实现。语言规范只是说它使用按名称调用的语义 - 它没有提到在 lambda 中挂起值的任何内容。我将编辑问题以指定,因为参数未标记为按名称调用,所以它们会被急切地评估。 - DaoWen

21

好的,评论了其他回答后,我想我可以将我的评论变成一个合适的答案。

流确实是惰性的,只有在需要时才会计算它们的元素(您可以使用#:: 逐个构造流元素,就像 List 中的 :: 一样)例如,以下代码不会抛出任何异常:

(1/2) #:: (1/0) #:: Stream.empty

这是因为在应用#::时,尾部是通过名称传递的,以避免急切地对其进行评估,而只在需要时进行评估(有关更多详细信息,请参见ConsWrapper.# ::const.applyStream.scala中的Cons类)。 另一方面,头部是按值传递的,这意味着它将始终被急切地评估,无论如何(正如Senthil所提到的)。这意味着执行以下操作实际上会抛出ArithmeticException:

(1/0) #:: Stream.empty

这是关于流值得知道的一个细节。然而,这并不是你所面临的问题。

在你的情况下,在实例化任何单个流之前算术异常就已经发生了。当调用lazy val bad = Stream(1/0)中的Stream.apply时,参数会被急切地执行,因为它没有声明为按名称传递的参数。Stream.apply实际上接受一个可变参数,并且这些参数必须按值传递。 即使它是通过名称传递的,ArithmeticException也会很快被触发,因为正如先前所述,流的头始终被提前评估。


4
流会评估头部并且剩余的尾部会被懒惰地评估。在你的例子中,两个流都只有头部,因此会出现错误。

这是关于流的一般知识非常好的事情,但在这里这不是罪魁祸首:1/0 在调用Stream.apply之前就被计算出来了,甚至在实例化流之前。 - Régis Jean-Gilles

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接