4 Answers
4

In the case of (let [r (range 1e9)] [(first r) (last r)]), it grabs the first element (0), then generates a billion - 2 elements, throwing them out as it goes, and then grabs the last element (999,999,999). It never has any need to keep any part of the sequence around.

In the case of (let [r (range 1e9)] [(last r) (first r)]), it generates a billion elements in order to be able to evaluate (last r), but it also has to hold on to the beginning of the list it's generating in order to later evaluate (first r). So it's not able to throw anything away as it goes, and (I presume) runs out of memory.

In the second code, isn't Clojure supposed to execute (last r) and then trying to execute (first r)? I mean according to your answer, Clojure looks like it is peeking to the next form in order to decide whether to drop the head or not.
–
ChironApr 25 '11 at 16:24

See Rafał Dowgird's answer. In both cases, Clojure sees that there are future references to r and so it will have to keep it around.
–
dfanApr 25 '11 at 16:31

To elaborate on dfan and Rafał's answers, I've taken the time to run both expressions with the YourKit profiler.

It's fascinating to see the JVM at work. The first program is so GC-friendly that the JVM really shines at managing its memory.

I drew some charts.

GC friendly: (let [r (range 1e9)] [(first r) (last r)])

This program runs very low on memory; overall, less than 6 megabytes. As stated earlier, it is very GC friendly, it makes a lot of collections, but uses very little CPU for that.

Head holder: (let [r (range 1e9)] [(last r) (first r)])

This one is very memory hungry. It goes up to 300 MB of RAM, but that's not enough and the program does not finish (the JVM dies less than one minute later). The GC takes up to 90% of CPU time, which indicates it desperately tries to free any memory it can, but cannot find any (the objects collected are very little to none).

Edit The second program ran out of memory, which triggered a heap dump. An analysis of this dump shows that 70% of the memory is java.lang.Integer objects, which could not be collected. Here's another screenshot:

What really holds the head here is the binding of the sequence to r (not the already-evaluated (first r), since you cannot evaluate the whole sequence from its value.)

In the first case the binding no longer exists when (last r) is evaluated, since there are no more expressions with r to evaluate. In the second case, the existence of the not-yet-evaluated (first r) means that the evaluator needs to keep the binding to r.