Conditional variable binding in Common Lisp

2.2k Views Asked by At

I want to execute a function with 2 local variables, but the values of these of these variables should depend on some condition. For example, let's say I have 2 variables x and y, and I want to swap them inside let if y > x. The swap should be temporary, I don't want to mutate state with rotatef. My code would look something like:

(setq x 2)
(setq y 1)
(let (if (> x y) ((x y) (y x)) ((x x) (y y)))
  (cons x y)) ; should return (1 . 2)

But the expression inside let is not valid Lisp. How do I conditionally assign values to local variables? The work around is to put the body in flet and call it with different arguments, but it look clumsy:

(flet ((body (x y) (cons x y)))
  (if (< x y)
      (body x y)
      (body y x)))
6

There are 6 best solutions below

3
On BEST ANSWER

Multiple-value-bind and values

There are lots of alternatives, some of which have already been pointed out in other answers. I think that the question in the title ("Conditional variable binding in Common Lisp") is a nice case for multiple-value-bind and values. I've used different variable names in the following just to make it clear where x and y are, and where the original values are coming from. The names can be the same, though; this just shadows them inside.

(let ((a 3)
      (b 2))
  (multiple-value-bind (x y)
      (if (< a b)
          (values a b)
          (values b a))
    (cons x y)))
;=> (2 . 3)

Then, using a bit of macrology, we can make this a bit cleaner, much like coredump did:

(defmacro if-let (test bindings &body body)
  "* Syntax:
let ({var | (var [then-form [else-form]])}*) declaration* form* => result*
* Description: 
Similar to LET, but each binding instead of an init-form can have a
then-form and and else-form.  Both are optional, and default to NIL.
The test is evaluated, then variables are bound to the results of the
then-forms or the else-forms, as by LET."
  (let ((bindings (mapcar #'(lambda (binding)
                              (destructuring-bind (variable &optional then else)
                                  (if (listp binding) binding (list binding))
                                (list variable then else)))
                          bindings)))
    `(multiple-value-bind ,(mapcar 'first bindings)
         (if ,test
             (values ,@(mapcar 'second bindings))
             (values ,@(mapcar 'third bindings)))
       ,@body)))

(pprint (macroexpand-1 '(if-let (< x y) ((x x y)
                                         (y y x))
                         (cons x y))))

; (MULTIPLE-VALUE-BIND (X Y)
;     (IF (< X Y)
;         (VALUES X Y)
;         (VALUES Y X))
;   (CONS X Y))

(let ((a 3) (b 2))
  (if-let (< a b)
      ((x a b)
       (y b a))
    (cons x y)))
;=> (2 . 3)

Comparison with progv

In terms of use, this has some similarities with sindikat's answer, but multiple-value-bind establishes bindings just like let does: lexical by default, but a global or local special declaration will make the bindings dynamic. On the other hand, progv establishes dynamic bindings. This means that if the bindings are entirely introduced by progv, you won't see much difference (except in trying to return closures), but that you can't shadow bindings. We can see this without having to do any conditional work at all. Here are two sample snippets. In the first, we see that the inner reference to x actually refers to the lexical binding, not the dynamic one established by progv. To refer to the one established by progv, you actually need to declare the inner reference to be special. progv doesn't accept declarations, but we can use locally.

(let ((x 1))
  (progv '(x) '(2)
    x))
;=> 1

(let ((x 1))
  (progv '(x) '(2)
    (locally (declare (special x))
      x)))
;=> 2

multiple-value-bind actually does the binding the way we'd expect:

(let ((x 1))
  (multiple-value-bind (x) (values 2)
    x))
;=> 2

It's probably better to use a binding construct like multiple-value-bind that establishes lexical bindings by default, just like let does.

3
On

One solution is to use progv instead of let, its first argument is a list of symbols to bind values to, second argument is a list of values, rest is body.

(progv '(x y) (if (< x y) (list x y) (list y x))
  (cons x y)) ; outputs (1 . 2)
3
On

Another alternative might be:

(let ((x (min x y))
      (y (max x y)))
  (cons x y))
3
On

My suggestion would be one of destructuring-bind or multiple-value-bind.

If you anticipate needing to do this a lot, I would suggest using a macro to generate the bindings. I've provided a possible macro (untested).

(defmacro cond-let (test-expr var-bindings &body body)
  "Execute BODY with the VAR-BINDINGS in place, with the bound values depending on 
   the trueness of TEST-EXPR.

   VAR-BINDINGS is a list of (<var> <true-value> <false-value>) with missing values 
   being replaced by NIL."

  (let ((var-list (mapcar #'car var-bindings))
        (then-values (mapcar #'(lambda (l)
                                 (when (cdr l) 
                                   (nth 1 l)))
                             var-bindings))
        (else-values (mapcar #'(lambda (l)
                                 (when (cddr l))
                                    (nth 2 l)))
                             var-bindings))
     `(destructuring-bind ,var-list
         (if ,test-expr
             (list ,@then-values)
           (list ,@else-values)))))
1
On

If you don't want to use progv, as mentioned by sindikat, you always can wtite something like that:

(defmacro let-if (if-condition then-bindings else-bindings &body body)
  `(if ,if-condition
     (let ,then-bindings
       ,@body)
     (let ,else-bindings
       ,@body)))

So expression like

(let-if (> x y) ((x y) (y x)) ((x x) (y y))
       (cons x y))

Will expand into:

(IF (> X Y)
(LET ((X Y) (Y X))
  (CONS X Y))
(LET ((X X) (Y Y))
  (CONS X Y)))
2
On

rotatef

How about:

CL-USER> (defvar x 2)
X
CL-USER> (defvar y 1)
Y
CL-USER> (let ((x x)    ; these variables shadow previously defined
               (y y))   ; X and Y in body of LET
           (when (> x y)
             (rotatef x y))
           (cons x y))
(1 . 2)
CL-USER> x              ; here the original variables are intact
2                       ; ^
CL-USER> y              ; ^
1                       ; ^

However, I think that in every such practical case there are lispier ways to solve problem without macros. Answer by msandiford is probably the best from functional point of view.

psetf

Although rotatef is really efficient method (it probably would be compiled to about three machine instructions swapping pointers in memory), it is not general.

Rainer Joswing posted just a great solution as a comment shortly after posting of the question. To my shame, I checked macro psetf only few minutes ago, and this should be very efficient and general solution.

Macro psetf first evaluates its even arguments, then assigns evaluated values to variables at odd positions just like setf does.

So we can write:

(let ((x x)
      (y y))
  (when (> x y)
    (psetf x y y x))
  ...)

And that's it, one can conditionally rebind anything to anything. I think it's way better than using macros. Because:

  • I don't think it's such a common situation;
  • Some macros in the posted answers repeat their body code, which may be really big: thus you get bigger compiled file (it's fair price for using macro, but not in this case);
  • Every custom macro does make code harder to understand for other people.