Another Useful If Macro

I’m working on a project now that requires a lot of lines like,

(let [c (get-coords :New-York)]
  (if (seq c)
    (connect-coords c)
    nil))

Unfortunately, I couldn’t really finagle the current if- macros, if-let and if-some, to work the way I need. I really liked the built in binding you get with those macros though, it’s very easy to read. So like any engineer, instead of just living with it, I dedicated a ridiculous amount of my time to learn how to fix it. It was abolutely worth it.

Given a collection c, I need something with the following behavior.

       If a is a non-empty collection of any sort (seq, vector, set, map, etc…) evaluate then,        otherwise, evaluate else.

I came up with this.

(defmacro if-seq
  "bindings => binding-form test

  If test is a seq, evaluates then with binding-form bound to the value of
  test, if not, yields else"
  ([bindings then]
   `(if-seq ~bindings ~then nil))
  ([bindings then else & oldform]
   (assert-args
    (vector? bindings) "a vector for its binding"
    (nil? oldform) "1 or 2 forms after binding vector"
    (= 2 (count bindings)) "exactly 2 forms in binding vector")
   (let [form (bindings 0) tst (bindings 1)]
     `(let [temp# ~tst]
        (if (seq temp#)
          (let [~form temp#]
            ~then)
          ~else)))))

If you dig through the Clojure core namespace, this will look pretty familiar. Why? It’s almost the exact same code as if-let. The only difference is in (if (seq temp#) ... near the bottom. In if-let, this line is (if temp# .... The simliarity is painful almost so lets expand it.

(defmacro if-pred
  ([pred bindings then]
   `(if-pred ~pred ~bindings ~then nil))
  ([pred bindings then else & oldform]
   (assert-args
    (vector? bindings) "a vector for its binding"
    (nil? oldform) "1 or 2 forms after binding vector"
    (= 2 (count bindings)) "exactly 2 forms in binding vector")
   (let [form (bindings 0) tst (bindings 1)]
     `(let [temp# ~tst]
        (if (~pred temp#) ;; <--
          (let [~form temp#]
            ~then)
          ~else)))))

Check out that microscopic change. What’s it do? It allows you to do all of this fun stuff,

(if-pred even? [a 2] true false)
;;=> true

(if-pred #(= 4 %) [a 2] true false)
;;=> false

(if-pred (fn [[a b]] (< a b)) [pair [2 4]] true false)
;;=> true

(if-pred (fn [[a b]] (< a b)) [pair [12 4]] true false)
;;=> false

;; And going back to the original intent ...

(if-pred seq [a [1 2 3]] a [])
;;=> [1 2 3]

(if-pred seq [a nil] a [])
;;=> []

(if-pred seq [a []] a [])
;;=> []

P.S. I’m using another macro in there called assert-args. This is a private macro in the clojure.core namespace. The macros will work fine without those lines in them if you don’t want to add the macro. If you do want to add the macro so that you have the error checks/logging, here you go!

(defmacro ^{:private true} assert-args
  [& pairs]
  `(do (when-not ~(first pairs)
         (throw (IllegalArgumentException.
                 (str (first ~'&form) " requires " ~(second pairs) " in " ~'*ns* ":" (:line (meta ~'&form))))))
       ~(let [more (nnext pairs)]
          (when more
            (list* `assert-args more)))))
Written on June 22, 2017