Where's COND-LET?

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 …


Because sometimes you want to do something like this:

  ((cadr xs)
   (let ((x (cadr xs)))
     (do-stuff-with x)))
  ((car xs)
   (let ((x (car xs)))
     (do-stuff-with x)))
   (do-stuff-with NIL)))

The example, however contrived, illustrates an extension of when-let and 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 imperative-cond-let.

(defun do-list-stuff (xs) 
   ((:= x (cadr xs)) 
    (list :cadr x))
   ((:= x (car xs)) 
    (list :car x)) 
    "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) 
    ((:= x (cadr xs) 
         y (car xs))
     (+ x y))
    ((:= x (car xs)) x)

;; calling add-first-two
> (add-first-two (list 1 2))
> (add-first-two (list 1))
> (add-first-two (list ))


(defmacro imperative-cond-let (&body clauses)
  (let ((imperative-body 
            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 t.

Try it out yourself! Use macroexpand to see what it expands into.