Higher-order Contextual Functions in Clojure

A lot of functions in Clojure take the form (f args1* f2 args2*), where (f args1) represents a context and (f2 args2) represents a computation to perform in that context. I’m going to call this Clojure’s “contextual function” pattern (since I’ve never seen it named before). This pattern is easy to overlook, but it’s powerful, usually easy to apply, and following it in your own code has a number of benefits.

Cleaner Invocation

Any time you see (send an-agent #(conj % 1)) or (swap! an-atom #(assoc % :key value)) you know the developer’s missed something. send and swap! are designed specifically so you can (send an-agent conj 1) and (swap! an-atom assoc :key value). In clojure.core, send, swap!, alter, commute, update-in and more work this way. The next time your final parameter is a function, consider whether the function you’re defining is, too, a contextual function. If so, consider changing your parameter list from [... f] to [... f & args] and change your invocation from (f x) to (apply f x args). That’s usually all it takes to join the contextual function club.

Chaining

One easy-to-miss strength of this approach is that it supports chaining. In a few places in our application, we wanted to send a computation to an agent and observe the state of the computation (e.g. to show and hide a spinner). We didn’t want to have to wire observation hooks or callbacks directly into the process, so we created a contextual function (defn with-process-callbacks [fire fun & args]). with-process-callbacks returns a function which calls fun with args, but calls fire whenever it’s about to start the process or the process finishes.

In Ruby, calling something like this might entail agent.send { with_callbacks { do_something 1, 2, 3}}. If with_callbacks didn’t follow the above pattern, an invocation would probably look like (send agent (fn [v] (with-process-callbacks callback #(f v 1 2 3)))) in Clojure. By forwarding arguments, however, we can instead write (send agent with-process-callbacks f 1 2 3).

More Flexible Macros

This pattern plays very well with macros that establish a lexical context. Following a pattern we noticed in Seesaw, whenever we wanted to introduce a macro (let’s call it mac) that executed code in a certain context, we first wrote a function mac* that took a function and optional arguments and executed that function in the context. The definition for the macro simply turns into a call to mac* with a fn wrapping the macro body.

For example, if with-open was written this way (it’s not), then (with-open [i my-reader] (foo i)) might simply turn into (with-open* my-reader (fn [i] (foo i))). with-open* would be a generally useful helper, enabling you to do things such as (with-open* my-reader copy out-file) instead of (with-open [r my-reader] (copy r out-file)). Whether this is an improvement in this case may be questionable, but having higher order variants of this type of macro can come in handy. We ended up using Seesaw’s b-do* (doc) function almost exclusively over b-do (doc) to ease unit testing where we were using Seesaw’s data binding functionality.

Not only do you get a higher order function equivalent to your macro almost for free, this almost always reduces the amount of code your macro generates – a readability and debuggability win.

Keep this pattern in mind the next time you find yourself writing Clojure. It helped us quite a bit.

Conversation
  • Did you know that the fellow who writes SeeSaw is in Ann Arbor, MI?

  • By the way if you have interest in Functional Programming, there are a few of us in Michigan. We’re trying to get a day of seminars and teaching on functional going. I think my e-mail should be available to you if you’re interested in hearing more about this as we get it going (or try to get it going :-) ) or contact me via the website.

  • Comments are closed.