4

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)))
Mirzhan Irkegulov
  • 17,660
  • 12
  • 105
  • 166

6 Answers6

9

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.

Community
  • 1
  • 1
Joshua Taylor
  • 84,998
  • 9
  • 154
  • 353
  • Note that with lexical bindings enclosing a `progv` of the same variables you don't need the `funcall` and `lambda` to make the variables within the `progv` refer to the lexical bindings. The variables within the `progv` __always__ refer to the lexical bindings unless the variables have been declared special. In the case of `progv`, with great power comes great subtlety. – m-n Aug 06 '14 at 01:11
  • @m-n You're right! I was mimicking [sindikat's answer](http://stackoverflow.com/a/25115877/1281433), wherein there's *no* lexical binding, where the inner occurrences *do* get the dynamic bindings. E.g., `(progv '(x) '(1) x) => 1`. The point you make is very important, because it means that progv won't actually work at all the way that answer expects it to, since `(let ((x 'a)) (progv '(x) '(1) x)) => A`. – Joshua Taylor Aug 06 '14 at 01:37
  • 1
    @m-n I've updated my answer to incorporate the point you made (which is stronger than the one I made, and more easily demonstrated). Thank you for the valuable input! – Joshua Taylor Aug 06 '14 at 01:47
4

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)))
sheikh_anton
  • 3,422
  • 2
  • 15
  • 23
  • 1
    I like this, and provided a somewhat similar macro-based version on top of multiple-value-bind in [my answer](http://stackoverflow.com/a/25122218/1281433). Note that this has either a weakness or a feature: The weakness is that you may have to repeat the variable names (once in the then-list, and again in the else-list); the feature is that you can specify different variables to bind for the then and else cases (either a feature a or a bug :)). In my answer, I've shown an approach where the variable names only appear once, so it's impossible to forget any, but it removes some flexibility. – Joshua Taylor Aug 04 '14 at 16:10
3

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.
Community
  • 1
  • 1
Mark Karpov
  • 7,499
  • 2
  • 27
  • 62
  • To be clear, this isn't really **conditionally rebind anything to anything**; this is rebinding to the same value, and then conditionally assigning. This will certainly work in OP's case, but it's not quite the same. It's also not a good option if the original value is expensive or has side effects. That's a bit different from the OP's post, but consider if `(if-let test ((x (f1) (f2))) …)` got expanded to `(let ((x (f1))) (when test (setf x (f2))) …)`. If `test` is true, then *both* `f1` and `f2` get called. That's probably not a good thing. – Joshua Taylor Aug 05 '14 at 22:01
  • @Joshua, You're right, it's assigning. And while it is not always a good option, sometimes it's very efficient and simple way to get what you want. However, your answer is more correct. – Mark Karpov Aug 06 '14 at 06:33
2

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)
Mirzhan Irkegulov
  • 17,660
  • 12
  • 105
  • 166
  • Progv only establishes special (i.e., dynamically scoped) bindings, though. For this particular example, we won't see a difference, but if any closures are returned, or another function is called that uses special variables, the returns may be surprising. I've used a similar approach with multiple-value-bind (which binds lexically by default, like let does) and added a comparison to [my answer](http://stackoverflow.com/a/25122218/1281433). – Joshua Taylor Aug 04 '14 at 15:55
  • @sindikat `progv`'s advantage is that it can calculate what variables to bind at runtime. With that in mind, `(progv (quote ...) ...)` is a signal that you don't need it. A disadvantage of `progv` is that the compiler wont determine what variables it will bind. This has two consequences: the first, obvious, one is that you are likely to see unbound variable warnings under some uses. It's the hidden consequence that will bite, though: if `x` and `y` are lexical variables in your snippet then `(cons x y)` will refer to the lexical (outer) bindings, not the dynamic bindings introduced by `progv`! – m-n Aug 04 '14 at 20:22
  • 1
    `(let ((x 1)) (progv '(x) '(10) x))` -> `1`, not `10`, unless you have declared x globally special. – m-n Aug 04 '14 at 20:33
1

Another alternative might be:

(let ((x (min x y))
      (y (max x y)))
  (cons x y))
clstrfsck
  • 14,715
  • 4
  • 44
  • 59
  • 1
    This works in this particular case, but it doesn't really provide a general solution for the problem in the title, "Conditional variable binding in Common Lisp". It doesn't easily scale to more than two variables, and ends up performing essentially the same comparison twice. – Joshua Taylor Aug 04 '14 at 15:57
  • 1
    The extension to the general case is to replace `min` and `max` with the appropriate selectors for your case. My point was that it isn't always necessary to compute the entire binding list, just the values. – clstrfsck Aug 04 '14 at 22:11
  • That's not a bad approach if the selectors are orthogonal or independent, so to speak. If they're related, this (like in the min/max case) can still lead to redundant computation. E.g., if you did `(let ((x (first l)) (y (second l)) (z (third l))) …)` you'd end up traversing a `l` a bunch of times, whereas `(destructuring-bind (x y z) l …)` would (hopefully be better). As a contrived example, `(destructuring-bind (x y z) (sort l '<) …)` will be better than `(let ((x (first (sort l '<))) (y (second (sort l '<))) (z (third (sort l '<)))) …)`. – Joshua Taylor Aug 04 '14 at 22:48
0

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)))))
Vatine
  • 20,782
  • 4
  • 54
  • 70
  • I used a very similar approach in [my answer](http://stackoverflow.com/a/25122218/1281433). Note that you don't need to guard with `(when (cdr l) …)` and `(when (cddr l) …)`; Those functions return nil when the list isn't "big enough". (Same with first, second, and third, as in my answer.) Using destructuring-bind here is a bit dangerous, since it will go into sublists. Thus you could do, e.g., `(cond-let test ( ((a b &rest c) (list 1 2 3 4 5) (list 6 7 8 9 0))) (list a b c))` and get `(1 2 (3 4 5))` or `(6 7 (8 9 0))`, which might not be expected. – Joshua Taylor Aug 04 '14 at 18:09
  • This also doesn't allow a variable that's not in a list, like LET does. E.g., with LET you can do `(let (x (y 2)) …)` and have x bound to nil. – Joshua Taylor Aug 04 '14 at 18:10
  • @JoshuaTaylor There are issues with almost every approach. Actually, for the specific question, simply doing `(let ((x (min x y)) (y max x y))) ...)` would probably do the trick. – Vatine Aug 05 '14 at 09:27