I discovered an interesting trick today which I thought worth sharing – you can add a side effect to the end of a sequence in Clojure which will only get called after the sequence is fully consumed for the first time.
(defmacro on-consumed [seq code] `(lazy-cat ~seq (do ~code nil))) (def coll (on-consumed [1 2 3] (println "DONE"))) (take 2 coll) ;; not fully consumed => (1 2) (take 3 coll) ;; not fully consumed (not gone past third item) => (1 2 3) (take 4 coll) ;; fully consumed past end of sequence => (1 2 3) DONE ;; printed to *out* (take 4 coll) ;; fully consumed for second time, no side effect => (1 2 3)
So how does this work?
The trick is that lazy-cat is a macro that builds a lazy sequence from one or more collections.
Because it is a macro, it is able to change evaluation order – in particular the code to create each collection is called lazily when each collection is needed.
So we come to the clever bit – the (do ~code nil) which implements the side effect. Because it is called lazily by lazy-cat, it only gets called once all the previous items in the sequence have been consumed. And because it returns nil (an empty sequence), it signals the end of the list. But in the process of producing the nil at the end of the list, it calls our side effect code as desired.
Finally, because lazy-cat caches the results of the sequence, our side effect code only gets called once.
While this is a neat trick I still think it should be used with caution. Side effects are not in general a good idea in functional code, and embedding them in sequences could cause all kinds of tricky bugs. Normally it is best in Clojure to keep most of your code purely functional, and restrict the side effects to carefully controlled areas where they are needed.
Nevertheless, I can think of a few cases where this would be useful:
- When debugging – ever wondered whether your lazy sequence is getting realised or not?
- If you are using a sequence of impure actions then you’ve already moved outside the pure FP world, and might as well use this if you want to append a side effect on the end.
- Potentially closing a resource after all input has been consumed. Though I would be careful with this one – there is a risk that if your lazy sequence isn’t consumed then the resource may be left open…