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 …
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 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.
Seeing COND-LET
Here is a version of cond-let
macro that I'm calling imperative-cond-let
.
(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
Defining IMPERATIVE-COND-LET
(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 t
.
Try it out yourself! Use macroexpand
to see what it expands into.
#commonlisp