Scala 中的按名称调用 vs Haskell 中的惰性求值?

Call-by-name in Scala vs lazy evaluation in Haskell?

Haskell 的懒惰求值 will never 比急切求值需要更多的求值步骤。

另一方面,Scala 的按名称调用评估 may require 评估步骤比按值调用多(如果短路收益被重复计算的成本抵消)。

我认为按名称调用大致等同于惰性求值。那么为什么在时间保证上会有这样的差异呢?

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

评估策略的名称有一定的广度,但大致可以归结为:

  • 按名称调用 参数几乎只是以调用函数时的任何(未评估)形式替换到函数体中。这意味着它可能需要在体内进行多次评估。

    在 Scala 中,您可以这样写:

    scala> def f(x:=> Int): Int = x + x
    scala> f({ println("evaluated"); 1 })
    evaluated
    evaluated
    2
    

    在 Haskell 中,您没有内置的方法来执行此操作,但您始终可以将 call-by-name 值表示为类型 () -> a 的函数。不过,由于引用透明性,这有点模糊——您将无法像使用 Scala 那样测试它(并且编译器可能会优化您调用的 "by name" 部分)。

  • call by need(懒惰......有点)调用函数时不评估参数,但在第一次需要时.在那一刻,它也被缓存了。之后,每当再次需要该参数时,就会查找缓存的值。

    在 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> :}
    

因此,如果按需调用(惰性)进行的评估与其他策略中的任何一种一样多或少,为什么还要使用其他策略?

惰性求值很难推理,除非你有参照透明性,因为那时你需要弄清楚什么时候你的惰性值被求值了。由于 Scala 是为与 Java 互操作而构建的,因此它需要支持命令式 side-effectful 编程。因此,在许多情况下 不是 在 Scala 中使用 lazy 的好主意。

此外,lazy 有性能开销:您需要有一个额外的间接寻址来检查该值是否已经被计算过。在 Scala 中,这意味着更多的对象,这给垃圾收集器带来了更大的压力。

最后,在某些情况下,惰性求值会留下 "space" 漏洞。例如,在 Haskell 中,通过将它们相加从右边折叠一大列数字是一个坏主意,因为 Haskell 将在计算之前建立对 (+) 的这个巨大系列惰性调用他们(实际上你只需要它来有一个累加器。一个著名的 space 问题的例子是 foldr vs foldl vs foldl'.

我不知道为什么 Scala 没有结果 “适当的”惰性求值——可能它的实现并不那么简单,尤其是当您希望语言与 JVM 顺利交互时。

Call-by-name(如您所见)不等同于惰性求值,而是用 () -> a 类型的参数替换 a 类型的参数。这样的函数包含与普通 a 值(类型是同构的)相同数量的信息,但要实际获得该值,您始终需要将函数应用于 () 伪参数。当您对该函数求值两次时,您将得到 twice the same result, but it must each time be calculated anew (since automatically memoising functions is not feasible).

惰性求值等同于用行为类似于以下 OO class:

的类型的参数替换类型 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);
  }
}

这本质上只是围绕特定 call-by-name– 函数类型的 memoisation-wrapper。不太好的是,这个包装器以一种基本方式依赖于 side-effects:当第一次评估惰性值时,您必须改变 containedValue 以表示该值现在已知的事实。 Haskell 在其运行时的核心有这种机制 hard-baked,well-tested 用于 thread-safety 等等。但是在一种试图尽可能公开地使用命令式 VM 的语言中,如果这些虚假突变与显式 side-effects 交织在一起,可能会引起严重的头痛。特别是,因为真正有趣的惰性应用不仅有一个单一的函数参数惰性(这不会​​给你带来太多好处),而是将惰性值分散到一个深层数据结构的主干中。最后,它不仅仅是一个 delay-function 你在进入惰性函数之后评估的,它是对这些函数的嵌套调用的整个 torrent(实际上,可能是无限多! ) 因为惰性数据结构被消耗了。

因此,Scala 通过默认不做任何惰性操作来避免这种危险,尽管正如 Alec 所说,它确实提供了一个 lazy 关键字,该关键字基本上将 memoised-function 包装器添加到值。

这可能很有用,但不适合发表评论。

您可以在 Scala 中编写一个函数,其行为类似于 Haskell 的 call-by-need(对于参数),方法是将参数设置为 call-by-name 并在函数开始时对它们进行惰性计算:

def foo(x: => Int) = {
  lazy val _x = x
  // make sure you only use _x below, not x
}