Sunday, October 23, 2011

A Few Nice Features Of Clojure, Part #1: Using Macros To Freeze Time

I thought it might be fun to show some of Clojure's features off by taking a look at how I use some of them in my own project EmailClojMatic.  I've got 4 features in mind and will show one feature each post.

Problem #1:

We want to unit test a function that depends on the current date.

Solution #1:

Fortunately we are using JodaTime, a great Java date/time library.  JodaTime has built in features for setting the 'now' time.  Let's use those to create a function to wrap our test code in, that will set the time at a given DateTime.

(defn do-at* [date-time f]
  (DateTimeUtils/setCurrentMillisFixed (.getMillis date-time))
  (try
    (f)
    (finally (DateTimeUtils/setCurrentMillisSystem))))

;; you might use this code like this:

(do-at* (DateMidnight. 2000 1 1)
  (fn [] (DateMidnight.))) ;;=> (DateMidnight. 2000 1 1)

If you are like me and you find yourself writing test code using this construct frequently you can use Clojure's macro functionality to essentially extend the Clojure language to have a new feature for evaluating an expression at a given DateTime.  This way we won't have to always wrap the expression in a function like we are doing in the do-at* function.

Let's see the macro version that I decided to call `do-at`, because it is just like the built in Clojure function `do` except it evaluates at the given time. [It's probably worth noting that xyz* function with xyz companion macro is a common Clojure idiom. -@darevay]

(defmacro do-at [date-time & body]
  "like clojure.core.do except evalautes the
  expression at the given time"
  `(do-at* ~date-time
    (fn [] ~@body)))

;; nice. Now we can see the actual code that was hiding
;; behind all of those parens and fn creation:

(do-at (DateMidnight. 2000 1 1)
  (DateMidnight.)) ;;=> (DateMidnight. 2000 1 1)

It's pretty powerful the way that macros give you the ability to shape your library so that it is free of boiler plate.

No comments:

Post a Comment