A piece of intertube about the Clojure programming language, algorithms and artificial intelligence.

Tuesday, December 7, 2010

when-let maybe?

In this post we will see how to incrementally develop a macro similar to when-let but more flexible.


When-let is useful to both bind one variable and do a test on the bind value in one operation. One common usage is:

(when-let [something (get-something arg1 arg2)]
   (do-stuff something))


This is equivalent to this code:

(let [something (get-something arg1 arg2)]
   (when something
      (do-stuff something)))

but with a more concise form.

When-let only executes the code after the binding if the value assigned in the let form is logically true, that is only if the value is not nil or false. In our example, this means that if get-something returns false then do-stuff will not be executed. Sometimes this is not enough and we want to execute the code even for false values. For instance if we are getting our data from a database and false values are acceptable but nil values are not, or if our functions return nil on error.

We could define a when-nlet macro which does what when-let does but executes the body whenever the binded value is not nil. By spying the code from clojure.core we obtain:

(defmacro when-nlet [bindings & body]
  (when (not= (count bindings) 2)
    (throw (IllegalArgumentException.
            "when-nlet requires an even number of forms in binding vector")))
  (let [form (bindings 0)
        tst (bindings 1)]
    `(let [temp# ~tst]
        (when (not (nil? temp#))
          (let [~form temp#]
            ~@body)))))

This is fine. But what if we need multiple values to be bound and multiple checks on them?

We could write:

(when-nlet [val1 (get-something arg1 arg2)]
  (when-nlet [val2 (get-something2 val1)]
     (when-nlet [val3 (get-something3 val2]
        (do-stuff val1 val2 val3))))

This is not satisfying. What about writing a when-nlet* macro that does multiple binds?

We could call it like that:

(when-nlet* [val1 (get-something arg1 arg2)
             val2 (get-something2 val1)
             val3 (get-something3 val2)]
          (do-stuff val1 val2 val3))

and it would produce multiple calls to when-nlet.

Here it is:

(defmacro when-nlet* [bindings & body]
  (when (not (even? (count bindings)))
    (throw (IllegalArgumentException.
            "when-nlet* requires an even number of forms in binding vector")))
  (let [whenlets (reduce (fn [sexpr bind]
                           (let [form (first bind)
                                 tst (second bind)]
                             (conj sexpr `(when-nlet [~form ~tst]))))
                         ()
                         (partition 2 bindings))
        body (cons 'do body)]
    `(->> ~body ~@whenlets)))

After the reduce the whenlets variable is assigned this list (we can see it by playing with macroexpand, macroexpand-1 and prn at the REPL): 

((when-nlet [val3 (get-something3 val2)]) (when-nlet [val2 (get-something2 val1)]) (when-nlet [val1 (get-something arg1 arg2)]))

We then thread the body inside the when-nlets forms with the powerful ->> macro. We obtain:

(when-nlet [val1 (get-something arg1 arg2)]
  (when-nlet [val2 (get-something2 val1)]
    (when-nlet [val3 (get-something3 val2)]
      (do (do-stuff val1 val2 val3)))))

So basically what we have done is creating a macro that does multiple binds, stops after the first bind returning a nil value and executes its body if no nil value has been encountered. It is nice and it shows us how much powerful Clojure is but... if we read this tutorial on how to use monads in Clojure we quickly see that there is a simpler way to do the same thing with the maybe monad!

(domonad maybe-m [val1 (get-something "a" "b")
                  val2 (get-something2 val1)
                  val3 (get-somnil val2)]
               (do-stuff val1 val2 val3))

This code is equivalent to our usage of when-nlet*. Thus, if we still feel the need for our when-nlet* macro, we could write it simply like that (after :using clojure.contrib.monads):

(defmacro when-nlet* [binding & body]
  (let [body (cons 'do body)]
   `(domonad maybe-m ~binding ~body)))

Is that not much better?

Conclusion: we shall not write macros before learning more about monads ;-) !

Followers