简体   繁体   English

Scala中的按名称致电与Haskell中的惰性评估?

[英]Call-by-name in Scala vs lazy evaluation in Haskell?

Haskell's lazy evaluation will never take more evaluation steps than the eager evaluation. Haskell的惰性评估永远不会比渴望的评估采取更多的评估步骤。

On the other hand, Scala's call-by-name evaluation may require more evaluation steps than call-by-value (if the short-circuit benefits are more than offset by the cost of repeated calculations). 另一方面,Scala的按名称进行的评估可能比按值进行的评估需要更多的评估步骤(如果短路收益远大于重复计算的成本所抵消)。

I thought call-by-name is roughly equivalent to lazy evaluation. 我认为按名称致电大致相当于懒惰的评估。 Why then such a difference in the time guarantee? 为什么在时间保证上有如此大的差异?

I am guessing that maybe Haskell language specifies that memoization must be used during evaluation; 我猜想也许Haskell语言指定在评估过程中必须使用记忆。 but in that case, why doesn't Scala do the same? 但是在那种情况下,Scala为什么不这样做呢?

There is some breadth in the names given to the evaluation strategies, but they come down to roughly this: 评估策略的名称有一定的广度,但大致可以归结为:

  • call by name an argument is pretty much just substituted into the function body in whatever (unevaluated) form it was in when the function was called. 按名称调用参数几乎是以调用函数时所用的任何形式(未经评估)替换为函数主体。 That means it may need to be evaluated multiple times in the body. 这意味着可能需要在体内对其进行多次评估。

    In Scala, you write this as: 在Scala中,您将其编写为:

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

    In Haskell you have no built in way of doing this, but you could always represent call-by-name values as functions of type () -> a . 在Haskell中,您没有内置的方法可以执行此操作,但是始终可以将按名称调用的值表示为() -> a类型的函数。 This is a bit more blurry though, because of referential transparency - you won't be able to test this out the way you would with Scala (and the compiler might optimize away the "by name" part of your call). 但是,由于参照透明性,这有点模糊-您将无法像使用Scala那样进行测试(并且编译器可能会优化调用的“按名称”部分)。

  • call by need (lazy... sort of) an argument is not evaluated when the function is called, but on the first time is is needed. 按需调用 (懒惰...之类的),在调用函数时不会评估参数,但是在第一次时是必需的。 At that moment, it is also cached. 那时,它也被缓存。 Afterwards, whenever the argument is needed again, the cached value is looked up. 之后,每当再次需要该参数时,都会查询缓存的值。

    In Scala, you don't declare your function arguments to be lazy, you make a declaration lazy: 在Scala中,您不会将函数参数声明为惰性的,而是将声明设为惰性的:

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

    In Haskell this is how all functions work by default. 在Haskell中,这就是默认情况下所有功能的工作方式。

  • call by value (eager, what almost every language does) arguments are evaluated when the function is called, even if the function doesn't end up using those arguments. 按值调用 (渴望,几乎每种语言都执行),在调用函数时会评估参数,即使函数最终没有使用这些参数也是如此。

    In Scala this is how functions work by default. 在Scala中,这是默认情况下函数的工作方式。

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

    In Haskell, you can force this behaviour with bang patterns on function arguments: 在Haskell中,您可以在函数参数上使用bang模式强制执行此行为:

     ghci> :{ ghci> f :: Int -> Int ghci> f !x = x ghci> :} 

So if call by need (lazy) does as much or less evaluation (as either of the other strategies), why use anything else? 因此,如果按需调用(懒惰)进行了或多或少的评估(与其他策略之一一样),为什么还要使用其他方法呢?

Lazy evaluation is tough to reason about unless you have referential transparency, because then you need to figure out exactly when your lazy value was evaluated. 除非您具有参照透明性,否则很难推断出惰性评估,因为这样一来,您就需要准确计算出何时评估您的惰性值。 Since Scala is built to interoperate with Java, it needs to support imperative, side-effectful programming. 由于Scala是为与Java互操作而构建的,因此它需要支持命令式,副作用较大的编程。 As a consequence, it is in many cases not a good idea to use lazy in Scala. 因此,在许多情况下,在Scala中使用lazy 不是一个好主意。

Furthermore, lazy has a performance overhead: you need to have an extra indirection to check if the value has been already evaluated or not. 此外, lazy会带来性能开销:您需要进行额外的间接检查,以检查该值是否已被评估。 In Scala, that translates to a bunch more objects, which puts an even greater strain on the garbage collector. 在Scala中,这转化为一堆更多的对象,这给垃圾收集器带来了更大的压力。

Finally, there are cases where having lazy evaluation leaves "space" leaks. 最后,在有些情况下,延迟评估会留下“空间”泄漏。 For example, in Haskell, folding a large list of numbers from the right by adding them together is a bad idea because Haskell will build up this gigantic series of lazy calls to (+) before evaluating them (when in reality you just need it to have an accumulator. A famous example of space issues you get even in simple contexts is foldr vs foldl vs foldl' . 例如,在Haskell中,通过将它们加在一起从右边折叠一个大的数字列表是一个坏主意,因为Haskell会在评估它们之前建立对(+)一系列懒惰调用(+)实际上,您只需要即使在简单的情况下,您也会遇到一个空间问题的著名例子,例如foldr vs foldl vs foldl'

I don't know why Scala 我不知道为什么斯卡拉 doesn't have 没有 Turns out it does “proper” lazy evaluation – likely it's just not so simple to implement, especially when you want the language to interact smoothly with the JVM. 事实证明,它可以 “适当地”进行惰性评估-可能实施起来并不那么简单,尤其是当您希望该语言与JVM顺利交互时。

Call-by-name is (as you've observed) not equivalent to lazy evaluation, but to replacing an argument of type a with an argument of type () -> a . 如您所见,按名称调用不等同于惰性求值,而是将类型a的参数替换为类型() -> a Such a function contains the same amount of information as a plain a value (the types are isomorphic), but to actually get at that value you always need to apply the function to the () dummy argument. 这样的功能包含作为一个普通的相同的信息量a值(类型是同构的),但在那个值实际上让你总是需要的功能,适用于()伪参数。 When you evaluate the function twice, you'll get twice the same result , but it must each time be calculated anew (since automatically memoising functions is not feasible ). 对函数进行两次评估时,将获得两次相同的结果 ,但是每次都必须重新计算一次(因为自动记忆函数是不可行的 )。

Lazy evaluation is equivalent to replacing an argument of type a with an argument of a type that behaves like the following OO class: 惰性计算相当于替换类型的参数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);
  }
}

This is essentially just a memoisation-wrapper around the specific call-by-name–function type. 本质上,这只是围绕特定的“按名称调用”功能类型的备注包装。 What's not so nice is that this wrapper relies in a fundamental way on side-effects: when the lazy value is first evaluated, you must mutate the containedValue to represent the fact that the value is now known. 什么是不太好的是,这种包装在副作用的根本途径依赖:当懒惰值先进行计算,则必须将变异containedValue代表事实值是目前已知的。 Haskell has this mechanism hard-baked at the heart of its runtime, well-tested for thread-safety etc.. But in a language that tries to use an imperative VM as openly as possible, it would probably cause massive headaches if these spurious mutations were interleaved with the explicit side-effects. Haskell的这种机制在其运行时的心脏处扎根,并且经过了线程安全性等方面的良好测试。但是,在一种尝试尽可能公开地使用命令式VM的语言中,如果这些虚假的突变可能会引起巨大的麻烦。与明显的副作用交织在一起。 Especially, because the really interesting applications of lazyness don't just have a single function argument lazy (that wouldn't buy you much) but scatter lazy values all through the spine of a deep data structure. 尤其是,因为真正有趣的懒惰应用程序不仅具有单个函数参数lazy(不会为您带来很多好处),而且还可以将惰性值分散到整个深度数据结构的脊柱中。 In the end, it's not just one delay-function that you evaluate later than entering the lazy function, it's a whole torrent of nested calls to such functions (indeed, possibly infinitely many!) as the lazy data structure is consumed. 最后,不仅是延迟函数要比进入惰性函数更晚进行评估,而且随着惰性数据结构的消耗,它是对此类函数的嵌套调用的全部种子 (实际上,可能无限多个!)。

So, Scala avoids the dangers of this by not making anything lazy by default, though as Alec says it does offer a lazy keyword that basically adds a memoised-function wrapper like the above to a value. 因此,Scala通过默认情况下不使任何内容成为惰性来避免这种危险,尽管正如Alec所说的那样,它确实提供了一个lazy关键字,该关键字基本上在值中添加了类似上面的记忆功能包装。

This may be useful and doesn't really fit in a comment. 这可能很有用,但实际上并不适合评论。

You can write a function in Scala which behaves like Haskell's call-by-need (for arguments) by making the arguments call-by-name and evaluating them lazily at the start of the function: 您可以在Scala中编写一个函数,使其行为类似于Haskell的按需调用(对参数),方法是按名称进行参数调用,并在函数开始时进行惰性计算:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM