Two Guys Arguing

clojure refs are functions of their current value

Posted in clojure by youngnh on 09.24.10

I was cleaning up some code that looked like this:

(get-nested (:mappings engine) db-name mapping-name)

Where get-nested was a one-off utility function that returned the value two keys deep in a doubly nested map, like so:

(get-nested {:key1 {:key2 "value"}} :key1 :key2) ;; => "value"

There is a Clojure function that does exactly this, only with slightly different syntax, get-in:

(get-in {:key1 {:key2 "value"}} [:key1 :key2]) ;; => "value"

Imagine my surprise when replacing the original code with

(get-in engine [:mappings db-name mapping-name])

blew up in my face. I checked the definition of get-nested to see if it was doing anything extra I wasn’t aware of:

(defn get-nested [map key1 key2] 
  (if-let [map2 (map key1)] 
    (map2 key2))) 

Nope. Looks pretty standard. It was at this point that my coworker informed me that (:mappings engine) returned a ref holding a map. Now I was really confused. This explained why my get-in form blew up, you can’t get a value from a map if it’s in a ref, you have to deref it first, but the get-nested code had been working in our software in heavy use for weeks.

Turns out, get-nested was very fortuitously written in a style that takes advantage of a little corner of Clojure I hadn’t previously known about. When a ref is invoked as a function, it returns the deref’d value of itself.

It’s not uncommon to write a form like:

({:key1 "value"} :key1)

The map is a function of its keys. Passing it a key returns the corresponding value in the map. This is a form I’m used to seeing. Turns out, this works equally well:

(def narwal (ref {:key1 "value"}))
(narwal :key1)

Since a ref is a function of it’s current value, it gives Clojure whatever object it’s currently holding, and Clojure tries to call invoke on that. It is equivalent to writing:

(@narwal :key1)

But where the first form will always return “value”, refs are updateable and over the course of your program, :key1 could have any value or may not exist at all. The above form is not referentially transparent.

You can see that refs are IFns by:

(supers (class (ref {}))) ;; => #{java.lang.Object java.lang.Runnable clojure.lang.IRef clojure.lang.IDeref clojure.lang.IFn clojure.lang.ARef java.util.concurrent.Callable clojure.lang.IReference clojure.lang.IMeta clojure.lang.AReference java.lang.Comparable}

or dig into the Clojure source code on github and look at the implementation yourself.

Tagged with: , ,

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s