Home > clojure > Clojure Update Literacy

Clojure Update Literacy

October 4th, 2011

I want to write briefly (ok, not that briefly) about my understanding of Clojure‘s various “update” functions, in other words, functions that take a function argument, apply it to some value and do something with the result. These include swap!, alter, commute, send, send-off, alter-var-root, update-in, alter-meta!, vary-meta, and probably several more. From here on out, I’ll call a function in this family an update function and the function passed to it a transform function.

Note that update functions are most commonly encountered when using Clojure’s reference types, i.e. vars, refs, atoms, and agents. Their signatures are all very consistent:

    (<update-fn> old-state <tranform-fn> & args)
    ;=> new-state

That they’re so consistent is a testament to the thought that has gone into the design of Clojure’s core API.

Nonetheless, a newcomer to Clojure, overwhelmed by all the other new concepts they’re learning, might find update functions a little confusing. They might find themselves calling reset! a lot because swap! is scary. Of course, if the value passed to reset! (or ref-set) depends on the previous value of the ref, you’ve thrown away all the nice concurrency guarantees that atoms and refs are supposed to give you. For example, the prototypical id generator:

  ; BADBAD Don't do this!
  (defn id-generator []
    (let [id (atom 0)]
     (fn []
       (reset! id (inc @id)))))

No No No No No

Of course, everyone knows to use (swap! id inc) for a canonical example like this, but in the thick of a larger app, feeling like you’re in over your head, it’s easier to make mistakes.


So, learning to read (and write) an update function can take you a long way along the road toward writing more idiomatic Clojure. For someone with an OO background, it might be easier if we mentally re-wrote the signature above like this:

   state = transform_fn(state, arg0, arg1, ...);

that is, we’re applying some transformation to the current value of state and storing the value back in state. See, swap! and friends are just a function call in disguise. Their signatures are completely consistent with Clojure’s argument order conventions, but they slightly obscure what’s going on because the state and transform function are switched. This makes sense since state is the important argument, but it took me a little while to realize this and make the mental adjustment.

Once I reached this conclusion, I found writing in a functional style with state transitions much more straightforward.

Note that I find this a useful way to *read* update functions. It’s not a replacement for thinking hard about the semantics of the update function you’re using


Here are some general guidlines that I find helpful:

  • Always write pure functions that represent state transitions or transformations. They take in the current state and maybe some additional arguments and calculate a new state. This is obvious, but sometimes I have to keep repeating these things to myself. Kind of like in Tcl, “everything’s a string, everything’s a string, everything’s a string, …”
  • reset! (and its cousin ref-set) is for exactly what its name says: “reset this atom back to some initial state dammit!” Only use it if you’re resetting your app, or if the new value truly doesn’t depend on the old value. In the latter case, randomly generated values or user input come to mind.
  • Always give your transform functions a name. Passing an anonymous function to swap! means you just wrote a function that’s harder to test or experiment with at the repl. Besides, a descriptive name is more readable than most anonymous functions.
  • Limit the number of call sites for update functions in your app. A system that’s sprinkled liberally with (dosync) blocks and calls to swap! will be more difficult to reason about than one where the state transitions are localized.
  • Don’t forget the args! I’ve often fallen into the trap of passing an anonymous function when I needed to pass args to my state transition function.
  • Don’t forget that most things are functions and, therefore, candidates for transform functions!
        ; A contrived example
        (def signal (atom :red))
        (def transitions { :red :green, :green :yellow, :yellow :red })
        (swap! signal #(get transitions %)) ; <- NO
        (swap! signal transitions) ; <- YES
    

and finally, as a wise man once said “functions and data!”


In conclusion, I hope this sheds some light on an area of Clojure that I’ve personally found to require a great deal of mental deprogramming. As I’ve gained confidence with these concepts, my code has been easier to read, easier to test, and easier to reason about. Happy Clojuring.

clojure

  1. tim
    October 10th, 2011 at 21:38 | #1

    Thank you. It really is helpful. I use Clojure for about a year, but still have the exact same feeling that it is over the head. Constantly tinkering emacs config, libraries change, code which worked in 1.2 does work in 1.3, no good books yet :), the life of wondering from blog to blog.

  1. No trackbacks yet.