It seems lazy evaluation of expressions can cause a programmer to lose control over the order in which their code is executed. I am having trouble understanding why this would be acceptable or desired by a programmer.
How can this paradigm be used to build predictable software that works as intended, when we have no guarantee when and where an expression will be evaluated?
9
A lot of the answers are going into things like infinite lists and performance gains from unevaluated parts of the computation, but this is missing the larger motivation for laziness: modularity.
The classic argument is laid out in the much-cited paper “Why Functional Programming Matters” (PDF link) by John Hughes. The key example in that paper (Section 5) is playing Tic-Tac-Toe using the alpha-beta search algorithm. The key point is (p. 9):
[Lazy evaluation] makes it practical to modularize a program as a generator that constructs a large number of possible answers, and a selector that chooses the appropriate one.
The Tic-Tac-Toe program can be written as a function that generates the whole game tree starting at a given position, and a separate function that consumes it. At runtime this does not intrinsically generate the whole game tree, only those subparts that the consumer actually needs. We can change the order and combination in which alternatives are produced by changing the consumer; no need to change the generator at all.
In an eager language, you can’t write it this way because you would probably spend too much time and memory generating the tree. So you end up either:
- Combining the generation and consumption into the same function;
- Writing a producer that works optimally only for certain consumers;
- Implementing your own version of laziness.
7
How can this paradigm be used to build predictable software that works as intended, when we have no guarantee when and where an expression will be evaluated?
When an expression is side-effect free, the order in which the expressions are evaluated does not affect their value, so the behavior of the program is not affected by the order. So the behavior is perfectly predictable.
Now side effects are a different matter. If side effects could occur in any order, the behavior of the program would indeed be unpredictable. But this is not actually the case. Lazy languages like Haskell make it a point to be referentially transparent, i.e. making sure that the order in which expressions are evaluated will never affect their result. In Haskell this is achieved by forcing all operations with user-visible side effects to occur inside the IO monad. This makes sure that all side-effects occur exactly in the order you’d expect.
8
If you are familiar with databases, a very frequent way to process data is:
- Ask a question like
select * from foobar
- While there is more data, do: get next row of results and process it
You do not control how the result is generated and in which way (indexes? Full table scans?), or when (should all the data be generated at once or incrementally when being asked for?). All you know is: if there is more data, you will get it when you ask for it.
Lazy evaluation is pretty close to the same thing. Say you have an infinite list defined as ie. the Fibonacci sequence – if you need five numbers, you get five numbers calculated; if you need 1000 you get 1000. The trick is that the runtime knows what to provide where and when. It is very, very handy.
(Java programmers can emulate this behavior with Iterators – other languages may have something similar)
3
Consider asking your database for a list of the first 2000 users whose names start with “Ab” and are older than 20 years. Also they must be male.
Here’s a little diagram.
You Program Processor
------------------------------------------------------------------------------
Get the first 2000 users ---------->---------- OK!
--------------------- So I'll go get those records...
WAIT! Also, they have to ---------->---------- Gotcha!
start with "Ab"
--------------------- NOW I'll get them...
WAIT! Make sure they're ---------->---------- Good idea Boss!
over 20!
--------------------- Let's go then...
And one more thing! Make ---------->---------- Anything else? Ugh!
sure they're male!
No that is all. :( ---------->---------- FINE! Getting records!
--------------------- Here you go.
Thanks Postgres, you're ---------->---------- ...
my only friend.
As you can see by this terrible terrible interaction, the “database” isn’t actually doing anything until it’s ready to handle all the conditions. It’s lazy-loading results at each step and applying new conditions each time.
As opposed to getting the first 2000 users, returning them, filtering them for “Ab”, returning them, filtering them for over 20, returning them, and filtering for male and finally returning them.
Lazy loading in a nutshell.
3
Lazy evaluation of expressions will cause the designer of a given
piece of code lose control over the sequence their code is executed.
The designer shouldn’t care about the order in which expressions are evaluated provided the result is the same. By deferring evaluation, it may be possible to avoid evaluating some expressions altogether, which saves time.
You can see the same idea at work at a lower level: many microprocessors are able to execute instructions out of order, which lets them use their various execution units more efficiently. The key is that they look at dependencies between instructions and avoid reordering where it would change the result.
There are several arguments for lazy evaluation I think are compelling
-
Modularity With lazy evaluation you can break code up into parts. For example, suppose you have the problem to “find the first ten reciprocals of elements in a list list such that the reciprocals are less than 1.” In something like Haskell you can write
take 10 . filter (<1) . map (1/)
but this is just incorrect in a strict language, since if you give it
[2,3,4,5,6,7,8,9,10,11,12,0]
you will be dividing by zero. See sacundim’s answer for why this is awesome in practice -
More things work Strictly (pun intended) more programs terminate with non strict evaluation than with strict evaluation. If your program terminates with an “eager” evaluation strategy it will terminate with a “lazy” one, but the oposite is not true. You get stuff like infinite data structures (which are really only kinda cool) as specific examples of this phenomenon. More programs work in lazy languages.
-
Optimality Call-by-need evaluation is asymptotically optimal with respect to time. Although the major lazy languages (that essentially being Haskell and Haskell) don’t promise call-by-need, you can more or less expect an optimal cost model. Strictness analysers (and speculative evaluation) keep the overhead down in practice. Space is a more complicated matter.
-
Forces Purity using lazy evaluation makes dealing with side effects in an undisciplined way a total pain, because as you put it, the programmer loses control. This is a good thing. Referential transparency makes programming, refractoring, and reasoning about programs so much easier. Strict languages just inevitably cave to the pressure of having impure bits–something Haskell and Clean have resisted beautifully. This is not to say that side effects are always evil, but controlling them is so useful that this reason alone is enough to use lazy languages.
Suppose you have a lot of expensive calculations on offer, but don’t know which ones will actually be needed, or in which order. You could add a complicated mother-may-i protocol
to force the consumer to figure out what’s available and trigger calculations that are not yet done. Or you could just provide an interface that acts as though the calculations were
all done.
Also, suppose you have an infinite result. The set of all primes for example. It’s obvious that you can’t calculate the set in advance, so any operation in the domain of primes has to be lazy.
with lazy evaluation you do not lose control about code execution, it is still absolutely deterministic. It is hard to get used with it, though.
lazy evaluation is useful because it is a way of reduction of lambda-term that will terminate in some cases, where eager evaluation will fail, but not vise versa. This includes
1) when you need to link to computation result before you actually execute computation, for example, when you constructs cyclic graph structure, but want to do it in functional style
2) when you define infinite data structure, but function this structure feed to use only part of datastructure.