I use alexandria's
when-let macro more than any other common utility. After that, the next most commonly used export is
if-let. It seems to me that there is a natural extension of these two macros to a
cond-let. Before getting into it, however, here is an example of
if-let in action:
(if-let (x (car some-list)) (print x) (print "the list was empty"))
In the above
if-let behaves just like
if in the case that
(car some-list) is
NIL. But in the case that
some-list has a
car, that value is bound to the variable
X, which can then be used.
when-let is just like
when, but again, it lets you bind a variable as the result of the tested condition in case you want to use it later.
But where oh where is
cond-let? Shouldn't there be one? I have found myself writing a version of
cond-let a number of times in the past, usually stashed in my project's utility module. Today we'll explore an implementation that relies on the imperative macro.
But first …
Why COND-LET ?
Because sometimes you want to do something like this:
(cond ((cadr xs) (let ((x (cadr xs))) (do-stuff-with x))) ((car xs) (let ((x (car xs))) (do-stuff-with x))) (t (do-stuff-with NIL)))
The example, however contrived, illustrates an extension of
if-let to the case of a
cond pattern. The concept is that you often want to use the result a predicate's evaluation (here, more like a semipredicate) in the execution of code that is run on condition that predicate returned non-nil.
Here is a version of
cond-let macro that I'm calling
(defun do-list-stuff (xs) (imperative-cond-let ((:= x (cadr xs)) (list :cadr x)) ((:= x (car xs)) (list :car x)) (t "it's NIL!"))) ;; calling do-list-stuff > (do-list-stuff (list 1 2)) (:CADR 2) > (do-list-stuff (list 1)) (:CAR 1) > (do-list-stuff nil) "it's NIL!"
If you include several variables in those binding forms then the clause will execute in the case that each variable has a non-nil value:
(defun add-first-two (xs) (imperative-cond-let ((:= x (cadr xs) y (car xs)) (+ x y)) ((:= x (car xs)) x) (t 0))) ;; calling add-first-two > (add-first-two (list 1 2)) 3 > (add-first-two (list 1)) 1 > (add-first-two (list )) 0
(defmacro imperative-cond-let (&body clauses) (let ((imperative-body (loop for (bindings . body) in clauses for vars = (unless (eq t bindings) (loop for var in (rest bindings) by #'cddr collect var)) collect bindings collect `(when (and ,@vars) (return (progn ,@body)))))) `(imperative ,@imperative-body)))
I like this implementation because of its parsimony. For each clause, the variables bound using the convensions of the
imperative macro are extracted. They are then check to all be be non-nil via
(and ,@vars) and, when they are, the clause body is run and returned. In the case of a clause beginning with
t, no variables are collected so that
(and) is called, which returns
Try it out yourself! Use
macroexpand to see what it expands into.