11

Haskell's lazy evaluation will never take more evaluation steps than the eager evaluation.

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).

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; but in that case, why doesn't Scala do the same?

Community
  • 1
  • 1
max
  • 49,282
  • 56
  • 208
  • 355

3 Answers3

13

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> 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. 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).

  • 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> lazy x: Int = { println("evaluated"); 1 }
    scala> x + x
    evaluated
    2
    

    In Haskell this is how all functions work by default.

  • 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> 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:

    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. As a consequence, it is in many cases not a good idea to use lazy in Scala.

Furthermore, lazy has a performance overhead: you need to have an extra indirection to check if the value has been already evaluated or not. In Scala, that translates to a bunch more objects, which puts an even greater strain on the garbage collector.

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'.

Alec
  • 31,829
  • 7
  • 67
  • 114
  • Apart from caching, aren't *call by name* and *call by need* equivalent? – max Jan 22 '17 at 00:49
  • @max Sort of... Minus the fact that call-name in Scala is only supported at call site as opposed lazy is supported at definition site only – Alec Jan 22 '17 at 00:52
  • Also, quoting @user7337271 answer: `in Haskell side effects are explicit. Scala is much more pragmatic and there is no explicit way to model side effects`; is this the main reason Scala chose its default evaluation to be call-by-name rather than call-by-need (given that functions with side effects cannot be memoized)? Or was it also due to space or other considerations? – max Jan 22 '17 at 01:02
  • @max, well, in a language with side-effects, you might not want caching. For example if you're writing a function which is supposed to evaluate its argument multiple times, e.g. some kind of looping function. If the function wants to cache it it always can explicitly, whereas you can't go the other way around. Call-by-name is thereby more expressive. In Haskell this is not done because functions do not have side-effects, so there is no difference in expressiveness and you might as well cache to save work. – luqui Jan 22 '17 at 01:05
  • @max Scala chose its default evaluation to be call-by-value, not call-by-name. In general, call-by-value is easier to reason about when you have side-effects. Since Scala wanted to interop with Java, side-effects are sort of necessary. – Alec Jan 22 '17 at 01:06
  • @luqui Yes. Very important catch there. And in the 5 minute window. :) – Alec Jan 22 '17 at 01:08
  • If you advocating against `lazy` objects, you should advocate against call-by-name too. It creates at least one extra object (a function). In many cases readability is much more important than GC troubles or a few extra cycles. – Victor Moroz Jan 22 '17 at 01:22
  • @VictorMoroz Yeah, but call by name is per call, unlike lazy evaluation. It makes is much easier to debug. And both cause GC trouble. – Alec Jan 22 '17 at 01:25
  • To add to your cons for lazy evaluation, wouldn't space be one of them? I mean, in a silly example, if you evaluate `a * n` using a recursive function `f(a, n) = f(a, n-1) * a if n > 0 else 1` (pseudocode), then lazy evaluation would require `O(n)` space while either call-by-value or call-by-name would only need O(1) space. I know this precise example should be taken care of by TCO, but I imagine sometimes TCO won't be able to optimize this extra space usage away? – max Jan 22 '17 at 03:21
  • @max Fair enough. Yes, these things are a big (if not too frequent) problem in Haskell. They are called space leaks. – Alec Jan 22 '17 at 03:27
  • While laziness is the cause of the stack overflow in some uses of `foldl`, laziness is also the reason some uses of `foldr` *don't* stack overflow. You can say eagerness is the cause of the stack overflow in `foldr` (in an eager language). – Derek Elkins left SE Jan 22 '17 at 05:43
  • AFAICS, call-by-name in Algol60 allowed more than what Scala does, e.g. [Jensen's device](https://en.wikipedia.org/wiki/Jensen's_Device). Thanks to the substitution-based semantics, a call like `f(i,a(i))` behaves in a subtle way when `f` assigns to `i`: when that happens `a(i)` changes according to the new value of `i`. Essentially, this is more than by-reference. Scala does not allow to assign to `i` (unless you wrap it in some object, I guess, and pass its reference instead). – chi Jan 22 '17 at 09:13
  • @chi Neat, and frightening to think about debugging! IIRC, the choice to have call by name be the default strategy in ALGOL60 was controversial - there is some evidence and finger pointing (something about a certain more influential member making this choice without consulting the committee). – Alec Jan 22 '17 at 09:18
4

I don't know why Scala doesn't haveTurns 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.

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. 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. 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:

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. 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. 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. 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.

Community
  • 1
  • 1
leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • But Scala _does_ have lazy evaluation... And IIRC it does under the hood something similar. And it _does_ cause massive headaches and GC. – Alec Jan 22 '17 at 01:05
  • @Alec Scala doesn't have _combinable_ lazy evaluation like Haskell. So I would consider original leftaroundabout statement true (no "proper" lazy evaluation). Simple wrapper is not enough to be considered "proper" in Haskell sense. – Victor Moroz Jan 22 '17 at 01:19
  • @VictorMoroz Interesting. What is "combinable" lazy evaluation? – Alec Jan 22 '17 at 01:22
  • @Alec In Scala `lazy val x = 1; f(x + x)` will evaluate `x` when calling `f` (unless `f` takes call-by-name parameters), in Haskell it will create a promise (thunk) to compute `f(x + x)` which is lazy too. It will be evaluated when you force it, resulting in "unwinding" a chain of thunks. You can try to create a chain of `lazy` declarations in Scala, but it's not very convenient. – Victor Moroz Jan 22 '17 at 01:35
  • 1
    @VictorMoroz: I don't know enough about Scala to be sure, but – wouldn't wrapping _everything_ in `lazy` give essentially the same, “combinable laziness” semantics as we have in Haskell (modulo a heck lot of overhead and unsafety)? – leftaroundabout Jan 22 '17 at 01:39
  • Also I would need to use only call-by-name parameters in methods, no functions (function parameters don't have call-by-name equivalent AFAIK), no libraries. I guess it's about the same as making Haskell strict. – Victor Moroz Jan 22 '17 at 01:53
3

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:

def foo(x: => Int) = {
  lazy val _x = x
  // make sure you only use _x below, not x
}
Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487