Haskell懒惰的评估永远不会比热切的评估花费更多的评估步骤。
另一方面,Scala的按名称调用求值可能需要比按值调用更多的求值步骤(如果短路的好处被重复计算的成本所抵消)。
我认为直呼其名大致相当于懒惰的评价。那么,为什么在时间保证上会有这样的差异呢?
我猜测,也许Haskell语言指定在评估过程中必须使用记忆;但既然如此,Scala为什么不这么做呢?
评估策略的名称有一定的广度,但大致可以归结为:
-
按名称调用参数几乎只是以调用函数时的任何(未赋值)形式替换到函数体中。这意味着它可能需要在体内进行多次评估。
在Scala中,您可以将其写成:
scala> def f(x:=> Int): Int = x + x scala> f({ println("evaluated"); 1 }) evaluated evaluated 2
在Haskell中,您没有内置的方法来实现这一点,但您可以始终将按名称调用值表示为
() -> a
类型的函数。不过,这有点模糊,因为引用透明性——你将无法像使用Scala那样测试它(编译器可能会优化掉调用中的"按名称"部分)。 -
按需调用(lazy…有点像)在调用函数时不计算参数,但在第一次调用时需要。此时,它也被缓存。之后,每当再次需要参数时,都会查找缓存的值。
在Scala中,你不会声明你的函数参数是惰性的,而是声明惰性:
scala> lazy x: Int = { println("evaluated"); 1 } scala> x + x evaluated 2
在Haskell中,默认情况下所有函数都是这样工作的。
-
按值调用(热切,几乎每种语言都是这样做的)参数在调用函数时进行求值,即使函数最终没有使用这些参数。
在Scala中,函数默认是这样工作的。
scala> def f(x: Int): Int = x + x scala> f({ println("evaluated"); 1 }) evaluated 2
在Haskell中,您可以在函数参数上使用bang模式强制执行这种行为:
ghci> :{ ghci> f :: Int -> Int ghci> f !x = x ghci> :}
所以,如果按需调用(lazy)执行的评估与其他策略一样多或一样少,为什么要使用其他策略呢
懒惰评估是很难推理的,除非你有引用透明度,因为你需要准确地计算出你的懒惰值是在什么时候评估的。由于Scala是为与Java进行互操作而构建的,因此它需要支持命令式、副作用式编程。因此,在许多情况下在Scala中使用lazy
不是一个好主意。
此外,lazy
还有一个性能开销:您需要有一个额外的间接性来检查是否已经评估了值。在Scala中,这转化为更多的对象,这给垃圾收集器带来了更大的压力。
最后,在有些情况下,惰性评估会留下"空间"泄漏。例如,在Haskell中,通过将数字相加来从右边折叠一个大的数字列表是一个坏主意,因为Haskell在评估它们之前会建立一系列对(+)
的懒惰调用(而实际上你只需要它有一个累加器。即使在简单的上下文中,你也会遇到空间问题的一个著名例子是foldr
vsfoldl
vsfoldl'
。
我不知道Scala
按名称调用(正如您所观察到的)并不等同于延迟求值,而是将a
类型的参数替换为() -> a
类型的参数。这样的函数包含与普通a
值相同数量的信息(类型是同构的),但要真正达到该值,您总是需要将函数应用于()
伪参数。当你对函数求值两次时,你会得到两次相同的结果,但每次都必须重新计算(因为自动记忆函数是不可行的)。
惰性求值相当于用行为类似以下OO类的类型的参数替换a
类型的参数:
class Lazy<A> {
function<A()> computer;
option<A> containedValue;
public:
Lazy(function<A()> computer):
computer = computer
, containerValue = Nothing
{}
A operator()() {
if isNothing(containedValue) {
containedValue = Just(computer());
}
return fromJust(containedValue);
}
}
这本质上只是一个基于名称-函数类型的特定调用的记忆包装器。不太好的是,这个包装器在根本上依赖于副作用:当第一次计算懒惰值时,必须对containedValue
进行变异,以表示该值现在已知。Haskell在其运行时的核心就已经对这种机制进行了严格的测试,并对线程安全性等进行了良好的测试。但在一种试图尽可能公开地使用命令式VM的语言中,如果这些虚假的突变与显式的副作用交织在一起,可能会引起巨大的头痛。特别是,因为lazyness真正有趣的应用程序不仅仅有一个函数参数lazy(这不会给你带来太多好处),而是将lazy值分散到深层数据结构的整个主干中。最后,这不仅仅是一个延迟函数的求值晚于进入懒惰函数,而是懒惰数据结构被消耗时对这些函数的嵌套调用(实际上,可能是无限多!)的整个洪流。
因此,Scala通过默认情况下不让任何东西变得懒惰来避免这种危险,尽管正如Alec所说,它确实提供了一个lazy
关键字,基本上是在值中添加了一个类似上面的记忆函数包装器。
这可能很有用,但并不适合注释。
您可以在Scala中编写一个函数,它的行为类似于Haskell的按需调用(对于参数),方法是按名称调用参数,并在函数开始时对其进行惰性求值:
def foo(x: => Int) = {
lazy val _x = x
// make sure you only use _x below, not x
}