“We will encourage you to develop the three great virtues of a programmer: laziness, impatience, and hubris”
Save article ToRead Archive Delete · Log in Log out
11 min read · View original · raganwald.com
larry wall
laziness and eagerness
In computing, “laziness” is a broad term, generally referring to not doing any work unless you need it. Whereas its opposite is “eagerness,” doing as much work as possible in case you need it later.
Consider this JavaScript:
Now, here’s the question: Does JavaScript evaluate 2+3
? You probably know the answer: Yes it does. When it comes to passing arguments to a function invocation, JavaScript is eager, it evaluates all of the expressions, and it does so whether the value of the expression is used or not.1
If JavaScript was lazy, it would not evaluate 2+3
in the expression ifThen(1 === 0, 2 + 3)
. So is JavaScript an “eager” language? Mostly. But not always! If we write: 1 === 0 ? 2 + 3 : undefined
, JavaScript does not evaluate 2+3
. Operators like ?:
and &&
and ||
, along with program control structures like if
, are lazy. You just have to know in your head what is eager and what is lazy.
And if you want something to be lazy that isn’t naturally lazy, you have to work around JavaScript’s eagerness. For example:
JavaScript eagerly evaluates () => 2 + 3
, which is a function. But it doesn’t evaluate the expression in the body of the function until it is invoked. And it is not invoked, so 2+3
is not evaluated.
Wrapping expressions in functions to delay evaluation is a longstanding technique in programming. They are colloquially called thunks, and there are lots of interesting applications for them.
generating laziness
The bodies of functions are a kind of lazy thing: They aren’t evaluated until you invoke the function. This is related to if
statements, and every other kind of control flow construct: JavaScript does not evaluate statements unless the code actually encounters the statement.
Consider this code:
You are doubtless chuckling at its naïveté. Imagine this list was the numbers from one to a billion–e.g. [1, 2, 3, ..., 999999998, 999999999, 1000000000]
–and we invoke:
We get the correct result, but we iterate over every one of our billion numbers first. Awful! Small children and the otherwise febrile know that you can return
from anywhere in a JavaScript function, and the rest of its evaluation is abandoned. So we can write this:
This version of the function is lazier than the first: It only does the minimum needed to determine whether a particular list contains a particular value.
From containing
, we can make a similar function, findWith
:
findWith
applies a predicate function to lazily find the first value that evaluates truthily. Unfortunately, while findWith
is lazy, its argument is evaluated eagerly, as we mentioned above. So let’s say we want to find the first number in a list that is greater than 99
and is a palindrome:
It’s the same principle as before, of course, we iterate through our billion numbers and stop as soon as we get to 101
, which is greater than 99
and palindromic.
But JavaScript eagerly evaluates the arguments to findWith
. So it evaluates isPalindromic, gt(99))
and binds it to predicate
, then it eagerly evaluates billion
and binds it to list
.
Binding one value to another is cheap. But what if we had to generate a billion numbers?
NumbersUpTo(1000000000)
is eager, so it makes a list of all billion numbers, even though we only need the first 101
. This is the problem with laziness: We need to be lazy all the way through a computation.
Luckily, we just finished working with generators2 and we know exactly how to make a lazy list of numbers:
Generators yield values lazily. And findWith
searches lazily, so we can find 101
without first generating an infinite array of numbers. JavaScript still evaluates Numbers()
eagerly and binds it to list
, but now it’s binding an iterator, not an array. And the for (const element of list) { ... }
statement lazily takes values from the iterator just as it did from the billion
array.
the sieve of eratosthenes
Here is the Sieve of Eratosthenes, written in eager style:
Let’s take a pass at writing the Sieve of Eratosthenes in lazy style. First off, a few handy things we’ve already seen in this blog, and in JavaScript Allongé:
With those in hand, we can write a generator that maps an iterable to a sequence of values with every nth
element changed to null
:3
That’s the core of the “sieving” behaviour: take the front element of the list of numbers, call it n
, and sieve every nth
element afterwards.
Now we can apply nullEveryNth
recursively: Take the first unsieved number from the front of the list, sieve its multiples out, and yield the results of sieving what remains:
With sieve
in hand, we can use Numbers
to get a list of numbers from 1
, and the rest
operation to give us all but the head… In other words, the numbers from 2
. Then we compact
the result to filter out all the nulls
, and what is left are the primes:
Besides performance, did you spot the full-on bug? Try running it yourself, it won’t work! The problem is that at the last step, we called compact
, and compact
is an eager function, not a lazy one. So we end up trying to build an infinite list of primes before filtering out the nulls.
We need to write a lazy version of compact
:
And now it works!
When we write things in lazy style, we need lazy versions of all of our usual operations. For example, here’s an eager implementation of unique
:
Naturally, we’d need a lazy implementation if we wanted to find the unique values of lazy iterators:
And so it goes with all of our existing operations that we use with lists: We need lazy versions we can use with iterables, and we have to use the lazy operations throughout: We can’t mix them.
it comes down to types
This brings us to an unexpected revelation.
Generators and laziness can be wonderful. Exciting things are happening with using generators to emulate synchronized code with asynchronous operations, for example. But as we’ve seen, if we want to write lazy code, we have to be careful to be consistently lazy. If we accidentally mix lazy and eager code, we have problems.
This is a symmetry problem. And at a deeper level, it exposes a problem with the “duck typing” mindset: There is a general idea that as long as objects handle the correct interface–as long as they respond to the right methods–they are interchangeable.
But this is not always the case. The eager and lazy versions of compact
both quack like ducks that operate on lists, but one is lazy and the other is not. “Duck typing” does not and cannot capture difference between a function that assures laziness and another that assures eagerness.
Many other things work this way, for example escaped and unescaped strings. Or obfuscated and native IDs. To distinguish between things that have the same interfaces, but also have semantic or other contractural differences, we need types.
We need to ensure that our programs work with each of the types, using the correct operations, even if the incorrect operations are also “duck compatible” and appear to work at first glance.
(discuss on hacker news)
notes
-
A few people have pointed out that a sufficiently smart compiler can notice that
2+3
involves two constants and a fixed operator, and therefore it can be compiled to5
in advance. JavaScript does not necessarily perform this optimization, but if it did, we could substitute something likex + y
and get to the same place in the essay. ↩ -
“Programs must be written for people to read, and only incidentally for machines to execute” ↩
-
This is the simplest and most naïve implementation that is recognizably identical to the written description: We start with a table of numbers (e.g., 2, 3, 4, 5, . . . ) and progressively cross off numbers in the table until the only numbers left are primes. Specifically, we begin with the first number, p, in the table, and: 1. Declare p to be prime, and cross off all the multiples of that number in the table, then 2. Find the next number in the table after p that is not yet crossed off and set p to that number; and then repeat from step 1. In The Genuine Sieve of Eratosthenes, Melissa E. O’Neill describes how to write a lazy functional sieve that is much faster than this implementation, although it abstracts away the notion of crossing off multiples from a list. ↩