我知道在Scala中,流应该是一种惰性求值的序列,但是我认为我遭受了某种基本误解,因为它们似乎比我预期的更加急切。
在这个例子中:
val initial = Stream(1)
lazy val bad = Stream(1/0)
println((initial ++ bad) take 1)
我遇到了java.lang.ArithmeticException
异常,看起来是由于零除导致的。我原本以为只请求一次流,因此bad
永远不会被评估。出了什么问题?
我知道在Scala中,流应该是一种惰性求值的序列,但是我认为我遭受了某种基本误解,因为它们似乎比我预期的更加急切。
在这个例子中:
val initial = Stream(1)
lazy val bad = Stream(1/0)
println((initial ++ bad) take 1)
我遇到了java.lang.ArithmeticException
异常,看起来是由于零除导致的。我原本以为只请求一次流,因此bad
永远不会被评估。出了什么问题?
流是惰性的事实并不改变方法参数会被急切地计算的事实。
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, ?)
好的,评论了其他回答后,我想我可以将我的评论变成一个合适的答案。
流确实是惰性的,只有在需要时才会计算它们的元素(您可以使用#::
逐个构造流元素,就像 List
中的 ::
一样)例如,以下代码不会抛出任何异常:
(1/2) #:: (1/0) #:: Stream.empty
这是因为在应用#::
时,尾部是通过名称传递的,以避免急切地对其进行评估,而只在需要时进行评估(有关更多详细信息,请参见ConsWrapper.# ::
、const.apply
和Stream.scala
中的Cons
类)。
另一方面,头部是按值传递的,这意味着它将始终被急切地评估,无论如何(正如Senthil所提到的)。这意味着执行以下操作实际上会抛出ArithmeticException:
(1/0) #:: Stream.empty
这是关于流值得知道的一个细节。然而,这并不是你所面临的问题。
在你的情况下,在实例化任何单个流之前算术异常就已经发生了。当调用lazy val bad = Stream(1/0)
中的Stream.apply
时,参数会被急切地执行,因为它没有声明为按名称传递的参数。Stream.apply
实际上接受一个可变参数,并且这些参数必须按值传递。
即使它是通过名称传递的,ArithmeticException
也会很快被触发,因为正如先前所述,流的头始终被提前评估。
1/0
在调用Stream.apply
之前就被计算出来了,甚至在实例化流之前。 - Régis Jean-Gilles
1#::(1/0)#:: Stream.empty
。但是,如果您调用Stream工厂方法,则仍将因我上面解释的相同原因而中断。 - DaoWen#::
方法实现为某种惰性操作符。只是Stream.apply
具有varargs参数,这些参数必须按值传递(因此在调用之前会被评估,就像您所解释的那样)。 - Régis Jean-Gilles=> T
与() => T
仍然不同。 - Régis Jean-Gilles=> T
不是一种一等类型,而() => T
是。 - Régis Jean-Gilles