Being Lazy in Clojure
Foreword
This Tutorial explains Clojure's laziness and gives a real world example where laziness saves us from tons of calculations.
What Laziness is
In the most basic way possible, laziness is the ability to define something and only calculate it when it's actually needed.
Example:
(def nums (map inc [1 2 3]))
What does the above code really do? It goes through the sequence [1 2 3] and increases every element, so nums is [2 3 4], or doesn't it?
The surprising truth is that the above code does not calculate anything. Only when we actually use nums then the calculations are done:
nums ; => [2 3 4]
; (it's only now that inc was mapped on [1 2 3])
Yea right, anyone can say that...
Okay let's write our own inc function that first prints something and then increases a number. This way we will know the exact time when it was actually called:
(defn pinc [n]
(prn ".")
(inc n))
And now we use it to define nums again:
(def nums (map pinc [1 2 3]))
If we look at the REPL or the Console or whatever it is that prints out our Clojure messages, we do see nothing. Nothing happened, pinc was never called even though we just mapped it onto a sequence.
If we directly ask for the value of nums however:
nums ; => prints "..." and gives (2 3 4)
Then everything is calculated and "..." is printed.
This is called lazy evaluation. Things are calculated not when they are defined, but when they are actually needed.
How Lazy is Clojure?
Many functional programming languages are lazy. For example, the Haskell language is entirely lazy. Clojure however is not entirely lazy, only the majority of sequence operations like map, reduce, filter or repeatedly are lazy evaluated.
For example, the following code gets evaluated immediately because it just defines a number, no sequence is involved:
(def n (pinc 0)) ; => immediately prints "."
So, why the witchcraft?
Infinite Lists
The most common use of laziness are infinite lists or streams. For example, we could define a list of all prime numbers. In case you didn't know, that's a lot of prime numbers (infinitely many).
If we would define such list in a language like C++ or Python then the language would try to calculate all prime numbers immediately, which would run literally forever.
If we define such list in Haskell or Clojure, then nothing is calculated just yet. As a matter of fact we could happily print out the first 1000 prime numbers from that list without running into a problem. Again, because lazy evaluation only calculates what is really needed, and nothing more.
A lot of Theory
Lazy evaluation is obviously a really powerful concept, and everyone seems to love it. The irony is that whenever we read about laziness, it's about infinite lists. But let's face it, the majority of us never really needs an infinite list of prime numbers.
A Game Server Example
Let's say we want to make a game server. Our game has monsters, a lot of them. And each monster has an inventory with randomly generated items:
(defn gen-item []
{:name "sword"
:attack (rand-int 100)})
(def monster {:name "wolf"
:level 3
:inventory (repeatedly 10 gen-item)})
Okay so let's say our game is really huge and we will spawn around 1.000 monsters per second which all have 10 randomly generated items.
In non-lazy languages we would generate exactly 10.000 random items per second, which can be quite expensive.
Now here is where we save tons of calculations thanks to Clojure's laziness. In the above code we used a sequence to generate the items:
:inventory (repeatedly 10 gen-item)
And this sequence is lazy! It is not calculated when it's defined!
So if we spawn 1.000 monsters, we generate 0 items yet. The random items are only generated if a player decides to look into a dead monster's inventory:
(:inventory monsters) ; generates items (not earlier)
Now if 100% of all the dead monsters are looted, then we obviously didn't win anything because all items will be generated. If however only 50% of the monsters are actually looted (which is much more likely), then 50% of the calculations never actually happen thanks to laziness.
Now obviously the monster inventory is just one little part of our game, but the laziness applies any time we use a sequence. And any time we don't end up needing a certain sequence, we will save computations, and often a whole lot of them.
This is why lazy evaluation is so unbelievably powerful.