Scala中的call-by-name与Haskell中的懒惰评估?

时间:2017-01-22 00:14:20

标签: scala haskell lazy-evaluation evaluation

Haskell的懒惰评估will never比急切的评估采取更多的评估步骤。

另一方面,Scala的名字呼叫评估may require评估步骤多于按值调用(如果短路效益超过重复计算的成本)。

我认为按名称呼叫大致相当于懒惰的评估。为什么那么时间上的这种差异保证了?

我猜测也许Haskell语言指定在评估期间必须使用memoization;但在那种情况下,为什么Scala不这样做呢?

3 个答案:

答案 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
}