Frequent Flyers of the util-namespace

eval

eval

Tags: #Clojure, #macros

In my Clojure projects, the util-namespace is typically a drawer where helper-functions and macros end up that are A) very project-specific or that B) I like to think of as generically applicable.
That italicised part is important as not a lot of these helpers actually transfer to other projects.
But those that do (the "frequent flyers"), are the ones that I miss the instant I start a new project with a blank util-slate.

This article describes two such helpers that I keep copying from project to project: when-seq and whenp.
Let's dive in!


some or none

One pattern I often use is how to evaluate some code only when dealing with a collection containing at least one item.

Consider the case where we want to generate some html-snippet with 'Recent articles'.
Sure enough hiccup is our friend:

(let [articles (find-articles {:max-age 60})]
  [:div
    [:h3 Recent articles]
    [:ul
      (for [{title :title} articles]
        [:li title])]])

As showing some html with only a header and no actual article titles is not very informative, we want to evaluate the let-body if and only if there's at least one article.

Wrapping the let-body in a (when (seq articles) [:div ,,,]) seems the obvious solution, as seq is Clojure's recommended idiom to check if a collection contains something.
One step further and you'll realise that "there's a macro for that" and you can combine let and when to (when-let [articles (seq (find-articles ,,,))] ,,,,).
And while it works for this case, there's an alternative for seq that I recommend you to use here, namely not-empty:

(when-let [articles (not-empty (find-articles {:max-age 60}))]
  ;; at least one article here
  )

not-emptynot-empty-examples yields the collection we pass it if and only if that collection is not empty (and nil otherwise).
It's better than using seq in that we can keep working with whatever the wrapped function (e.g. find-articles) returns, be it a vector, set, string or list.

enter Macro Club

Having used this pattern quite a lot, it was great to see Borkdude capturing this exact pattern in a macro when-seq and propose it to be included in core.
It would make our example even more concise and descriptive:

(when-seq [articles (find-articles {:max-age 60})]
  ;; at least one article here
  )

It's in line with other when-*-macros like when-let, when-some and when-first. And naming-wise this re-introduces the notion of seq, which is better known than not-empty and improves readability as it gets away from the negated wording of the latter.

Now, upvoting questions on Ask Clojure might help proposals like this to be considered by the Clojure core-team1. But in the mean time: why not ignore the first rule of Macro Club 2 and add it to the util-namespace to see if it sticks!

It could look something like this3:

(defmacro when-seq
  "Accepts a single binding => binding-form seqable?, e.g. [roles #{}].

  The same as (when-let [roles (not-empty (find-roles))] body)."
  [binding & body]
  `(when-let [~(binding 0) (not-empty ~(binding 1))]
    (do ~@body)))
You could add some asserts, like in other when-* macros.

If This Then...This!

Back to not-empty. I'd like to think of it as a function that 'rounds down' empty collections to something falsy, making it useful in when-let as we saw above.
It's like a filter-predicate, but instead of applying it to a collection, it just 'filters' one value.

whenp

So what if we generalise this 'filtering'? Sure there are other things worth checking besides emptyness!

Enter: whenp.

Some example uses:

(whenp 1 odd?)  ;;=> 1
(whenp 2 odd?) ;;=> nil

;; nil skips any test
(whenp nil odd?) ;;=> nil

;; find first
(some #(whenp % even?) '(1 2 3)) ;; => 2

;; multiple predicates that should all hold
(whenp 1 odd? pos?)  ;;=> 1
(whenp -1 odd? pos?)  ;;=> nil

Back to our html-example: using whenp we can come up with more specific conditions that should all hold, and that combined with when-let determine if the html-snippet gets created:

(when-let [articles (whenp (find-articles {:max-age 60})
                           #(->> % count (<= 3))
                           #(some recent-article? %))]
  ;; at least 3 articles and a recently published one
  )

;; To improve readability and testability, let's have a dedicated
;; when-function:
(defn when-highlightable
  "Yields `articles` when at least 3 articles and it contains a recent
  article, else `nil`."
  [articles]
  (whenp articles #(->> % count (<= 3)) #(some recent-article? %)))

;; The `when-`-prefix should indicate that this function yields the
;; collection as-is or nil, and it's not e.g. filtering the
;; collection.
(when-let [articles (when-highlightable (fetch-articles {:max-age 60}))]
  ,,,
  )

I've had this function in util-namespaces in various forms and names before I found an issue on Ask Clojure from some years ago.
The discussion there certainly helped coming to the current name and implementation that I use:

(defn whenp
  "Yield `v` when `v` is not nil and passes all `preds`. `nil` otherwise.
  E.g. `(whenp 1 odd? pos?) ;;=> 1`."
  [v & preds]
  (when (and (some? v) ((apply every-pred preds) v))
    v))

Fin!


Tags: #Clojure, #macros