Scala:流不懒惰?
Posted
技术标签:
【中文标题】Scala:流不懒惰?【英文标题】:Scala: Streams not acting lazy? 【发布时间】:2012-11-04 19:52:49 【问题描述】:我知道在 Scala 中流应该是惰性求值的序列,但我认为我遇到了某种根本性的误解,因为它们似乎比我预期的更热切。
在这个例子中:
val initial = Stream(1)
lazy val bad = Stream(1/0)
println((initial ++ bad) take 1)
我得到一个java.lang.ArithmeticException
,这似乎是由零除法引起的。我希望bad
永远不会被评估,因为我只要求流中的一个元素。怎么了?
【问题讨论】:
【参考方案1】:好的,所以在评论了其他答案之后,我想我也可以将我的 cmets 变成一个正确的答案。
流确实是惰性的,只会按需计算其元素(您可以使用#::
逐个元素构造流,就像::
用于List
)。例如,以下不会抛出任何异常:
(1/2) #:: (1/0) #:: Stream.empty
这是因为在应用 #::
时,尾部是按名称传递的,因此不会急切地评估它,而是仅在需要时才进行评估(参见 Stream.scala
中的 ConsWrapper.# ::
、const.apply
和类 Cons
了解更多信息细节)。
另一方面,head 是按值传递的,这意味着它总是会被急切地评估,无论如何(正如 Senthil 所提到的)。这意味着执行以下操作实际上会引发 ArithmeticException:
(1/0) #:: Stream.empty
这是一个值得了解的关于流的问题。但是,这不是您面临的问题。
在您的情况下,算术异常甚至在实例化单个流之前发生。在lazy val bad = Stream(1/0)
中调用Stream.apply
时,该参数被急切地执行,因为它没有被声明为按名称参数。 Stream.apply
实际上需要一个 vararg 参数,并且这些参数必须按值传递。
即使它是按名称传递的,ArithmeticException
也会在不久之后触发,因为如前所述,Stream 的头部总是被提前评估。
【讨论】:
【参考方案2】:Streams 是惰性的这一事实并没有改变方法参数被急切求值的事实。
Stream(1/0)
扩展为 Stream.apply(1/0)
。该语言的语义要求在调用方法之前评估参数(因为 Stream.apply
方法不使用按名称调用的参数),因此它尝试评估 1/0
作为参数传递给Stream.apply
方法,这会导致您的 ArithmeticException。
不过,有几种方法可以让这个工作正常进行。由于您已经将 bad
声明为 lazy val
,因此最简单的方法可能是使用同样惰性的 #:::
流连接运算符来避免强制评估:
val initial = Stream(1)
lazy val bad = Stream(1/0)
println((initial #::: bad) take 1)
// => Stream(1, ?)
【讨论】:
@Senthil 在他的回答中提出了一个很好的观点,即始终热切地评估 Stream 的头部,因此如果您将除零代码移至尾部,它将起作用:1 #:: (1/0) #:: Stream.empty
。但是,如果您调用 Stream
工厂方法,那么它仍然会因为我上面解释的相同原因而中断。
说语言要求在调用方法之前评估参数是非常误导的。按值参数是正确的,但 scala 具有按名称参数。允许将#::
方法实现为某种惰性运算符的功能。碰巧Stream.apply
有可变参数,这些参数必须按值传递(因此如您所解释的那样在调用之前进行评估)
@RégisJean-Gilles - 该语言在调用函数之前仍会评估所有参数,因为它是底层 JVM 的要求——甚至是名称参数。不同之处在于,按名称参数包装在 Lambda 中,并且 Lambda 的主体在您调用它之前不会被评估。
不正确。 JVM 内部发生的事情与语言的语义不同。按名称参数在底层实现为 Function0,但 => T
在语言级别上仍然与 () => T
不同。
事实上=> T
甚至不是一流的类型,而() => T
是。【参考方案3】:
Stream 将评估头部,而剩余的尾部将延迟评估。在您的示例中,两个流都只有头部,因此会出错。
【讨论】:
以上是关于Scala:流不懒惰?的主要内容,如果未能解决你的问题,请参考以下文章