Haskell的懒惰评估will never比急切的评估采取更多的评估步骤。
另一方面,Scala的名字呼叫评估may require评估步骤多于按值调用(如果短路效益超过重复计算的成本)。
我认为按名称呼叫大致相当于懒惰的评估。为什么那么时间上的这种差异保证了?
我猜测也许Haskell语言指定在评估期间必须使用memoization;但在那种情况下,为什么Scala不这样做呢?
答案 0 :(得分:12)
评估策略的名称有一定的广度,但它们大致分为:
按名称调用一个参数几乎只是在调用函数时以任何(未评估的)形式替换到函数体中。这意味着它可能需要在体内多次评估。
在Scala中,您将其写为:
scala> def f(x:=> Int): Int = x + x
scala> f({ println("evaluated"); 1 })
evaluated
evaluated
2
在Haskell中,您没有内置的方法来执行此操作,但您始终可以将按名称的值表示为类型() -> a
的函数。这有点模糊,因为参考透明度 - 你不能像Scala一样测试它(并且编译器可能会优化"名称"部分你的电话)。
按需调用(懒惰......排序)调用函数时不会计算参数,但是第一次需要参数。那一刻,它也被缓存了。之后,只要再次需要参数,就会查找缓存的值。
在Scala中,你没有声明你的函数参数是懒惰的,你做一个懒惰的声明:
scala> lazy x: Int = { println("evaluated"); 1 }
scala> x + x
evaluated
2
在Haskell中,这是默认情况下所有函数的工作方式。
按值调用(急切地,几乎每种语言都会这样做)参数在调用函数时被计算,即使函数最终没有使用这些参数。< / p>
在Scala中,这是默认情况下函数的工作方式。
scala> def f(x: Int): Int = x + x
scala> f({ println("evaluated"); 1 })
evaluated
2
在Haskell中,您可以在函数参数上使用爆炸模式强制执行此行为:
ghci> :{
ghci> f :: Int -> Int
ghci> f !x = x
ghci> :}
除非你具有参照透明度,否则懒惰评估很难推理,因为那时你需要确定 时你的懒惰值是否被评估。由于Scala是为与Java互操作而构建的,因此它需要支持命令式的,有效的编程。因此,在许多情况下,在Scala中使用lazy
是个好主意。
此外,lazy
具有性能开销:您需要有一个额外的间接检查以检查该值是否已经过评估。在Scala中,这会转化为更多的对象,这会给垃圾收集器带来更大的压力。
最后,有些情况下,懒惰的评估会离开&#34;空间&#34;泄漏。例如,在Haskell中,通过将它们加在一起从右侧折叠大量数字是一个坏主意,因为Haskell会在评估它们之前建立这个对(+)
的大量惰性调用(实际上你只需要它们)它有一个累加器。即使在简单的情境中,你得到的一个着名的空间问题例子是foldr
vs foldl
vs foldl'
。
答案 1 :(得分:4)
我不知道为什么Scala 没有 原来它做 “正确”的懒惰评估 - 可能它不是那么易于实现,尤其是当您希望语言与JVM平滑交互时。
按名称调用(如您所见)并不等同于延迟评估,而是使用类型a
的参数替换类型为() -> a
的参数。这样的函数包含与普通a
值相同数量的信息(类型是同构的),但要实际获得该值,您总是需要将函数应用于()
伪参数。当您评估该函数两次时,您将得到twice the same result,但每次都必须重新计算(自automatically memoising functions is not feasible起)。
延迟评估等效于将类型为a
的参数替换为类似于以下OO类的类型的参数:
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);
}
}
这基本上只是一个围绕特定的按名称调用函数类型的memoisation-wrapper。不太好的是,这个包装器依赖于副作用的基本方式:当首先评估惰性值时,必须改变containedValue
以表示该值现在已知的事实。 Haskell在其运行时的核心部署了这种机制,经过了良好的线程安全性测试等。但是在一种试图尽可能公开使用命令式VM的语言中,如果这些虚假的突变,它可能会引起巨大的麻烦与明确的副作用交错。特别是,因为懒惰的真正有趣的应用程序不仅仅有一个单独的函数参数lazy(这不会给你带来太大的帮助),而是通过深层数据结构的主干来分散惰性值。最后,它不仅仅是一个延迟函数,你在进入惰性函数之后进行评估,它是对这些函数的嵌套调用的整个 torrent (实际上,可能无限多!)作为惰性数据结构被消耗。
因此,Scala通过默认不做任何延迟来避免这种危险,尽管Alec说它确实提供了一个lazy
关键字,它基本上将一个如上所述的memoised-function包装器添加到一个值。
答案 2 :(得分:3)
这可能很有用,并不适合评论。
您可以在Scala中编写一个函数,其行为类似于Haskell的需要调用(用于参数),方法是调用名称并在函数开头懒惰地评估它们:
def foo(x: => Int) = {
lazy val _x = x
// make sure you only use _x below, not x
}