Skip to content

Instantly share code, notes, and snippets.

@NicMcPhee
Last active December 14, 2015 01:19
Show Gist options
  • Save NicMcPhee/5005197 to your computer and use it in GitHub Desktop.
Save NicMcPhee/5005197 to your computer and use it in GitHub Desktop.
An example of why using map instead of doseq to perform a side-effecting operation on items in a collection can fail in subtle and difficult to debug ways.
;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; This logging code is taken straight from Chapter 4 of
;; Programming Clojure by Emerick, et al. If you're having
;; trouble finding your console output in Eclipse using
;; counterclockwise, add the file output from the book and
;; use that instead.
(def console (agent *out*))
(defn write
[^java.io.Writer w & content]
(doseq [x (interpose "" content)]
(.write w (str x)))
(doto w
(.write "\n")
.flush))
(defn log-reference
[reference & writer-agents]
(add-watch reference :log
(fn [_ reference old new]
(doseq [writer-agent writer-agents]
(send-off writer-agent write new)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; The idea here is to create a collection of atoms that
;; each encapsulate a different integer. We then
;; arrange for logging on each one, and increment
;; all of them, hoping to see logging output for
;; each increment.
;; The following definition, however, generates *no*
;; output other than our print statement text. The
;; problem is that map is all about creating a collection
;; of *values* rather than generating side-effects. Because
;; of that, map is lazy and only applies the function to its
;; arguments if and when the result is actually needed.
;; Here, we never actually use or look at or print the collection
;; of atoms, so none of the map calls is ever forced to actually
;; evaluate its function calls. This means that in fact none of
;; the atoms are created, no logging is arranged, no incrementing
;; is done, and no logging actually happens on the console.
;; These problems can be particularly tricky to debug, because
;; sometimes they behave differently from the REPL than they do
;; from your code. If you remove the println from the end of this,
;; for example, and call it from the REPL then instead of returning
;; nil (which is what the println returns) it will return the result
;; of the second map. That will force the creation of the atoms
;; (which are the inputs to the second map) and force the increments.
;; It will *not* however, force any logging to happen, because the
;; the result of the first map is never used anywhere. Worse, if you
;; call make-logged-atoms-too-lazy in a sequence of calls in a do
;; list in some other function, *nothing* will be forced (even
;; without the println) because the do list never uses the results
;; of that last map. That means it is never forced and nothing
;; actually happens! So a function like this can look like it works
;; or partially works from the REPL, but doesn't work at all in
;; your program.
;; How confusing is that?!?
(defn make-logged-atoms-too-lazy [num-atoms]
(let [atoms (map atom (range num-atoms))]
; Arrange to log each atom
(map #(log-reference % console) atoms)
; Increment each atom, which should generate a log message
(map #(swap! % inc) atoms)
(println "We're done!")
))
;; The solution is to use doseq, which is specifically intended
;; for use in cases where the point is side effects instead of
;; values. We still use map to create the atoms because we
;; need the values (the atoms) that result from mapping our
;; function across the range. The next two, however, need to
;; be doseqs since they're just there for the side effects
;; (attaching a logger, and incrementing the values in the
;; atoms).
(defn make-logged-atoms [num-atoms]
(let [atoms (map atom (range num-atoms))]
; Arrange to log each atom
(doseq [a atoms] (log-reference a console))
; Increment each atom, which should generate a log message
(doseq [a atoms] (swap! a inc))
(println "We're done!")
))
@NicMcPhee
Copy link
Author

It's worth noting that for also uses lazy evaluation and would (I think) generate problems similar to map if what you're after is side effects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment