1

I'm learning (common) Lisp, and as exercise, I want to implement 'xond', a cond macro, that transform this silly example:

(xond (= n 1) (setq x 2) (= n 2) (setq x 1))

into a if-else chain:

(if (= n 1) (setq x 2) (if (= n 2) (setq x 1)))

Currently, I have this macro:

(defmacro xond (&rest x) (if x (list 'progn (list 'if (pop x) (pop x)))))

that just expand the first two items in x:

(macroexpand '(xond (= x 1) (setq y 2)))

produce

(PROGN (IF (= X 1) (SETQ Y 2))) ;

Now I want to process all items in x, so I add a loop to produce a if-serie (a step toward if-else-version):

(defmacro xond (&rest x)
  (loop (if x
           (list 'progn (list 'if (pop x) (pop x)))
           (return t))))

but then macro seems to stop working:

(macroexpand '(xond (= x 1) (setq y 2)))
T ;

What I'm missing here?

Edition

verdammelt's answer put me in the right track, and coredump's made me change my approach to an iterative one.

Now I'll implement (xond test1 exp1 test2 exp2) as:

(block nil
   test1 (return exp1)
   test2 (return exp2)
)

which can be done by iteration.

I'm writing this for my minimal Lisp interpreter; I have only implemented the most basic functions.

This is what I wrote. I'm using la to accumulate the parts of the output.

(defmacro xond (&rest x) 
   (let ((la '())) 
      (loop 
         (if x (push (list 'if (pop x) (list 'return (pop x))) la) 
               (progn (push 'nil la)
                      (push 'block la)
                      (return la)
                )))))

with

(macroexpand '(xond (= x 1) (setq y 2) (= X 2) (setq y 1)))

result:

(BLOCK NIL 
    (IF (= X 2) (RETURN (SETQ Y 1)))
    (IF (= X 1) (RETURN (SETQ Y 2)))
) ;

Second edition

Add a label to block and change return to return-from, to avoid conflict with other return inside arguments. Also changed push for append to generate code in the same orden as the parameters.

(defmacro xond (&rest x) 
    (let ((label (gensym)) (la '()) (condition nil) (expresion nil)) 
        (setq la (append la (list 'block label)))
        (loop 
            (if x   
                (setq la (append la (list 
                   (list 'if (pop x) (list 'return-from label (pop x))))))
                 (return la)))))

So

(macroexpand '(xond (= x 1) (setq y 2) (= X 2) (setq y 1)))

now gives

(BLOCK #:G3187 (IF (= X 1) (RETURN-FROM #:G3187 (SETQ Y 2))) (IF (= X 2) (RETURN-FROM #:G3187 (SETQ Y 1))))
7
  • The edit introduces a different expansion which has a different semantics, ie. If multiple tests are true. Maybe you want to add a return clause? Commented Dec 26, 2020 at 19:13
  • 1
    @coredump. I edited my question to add a return as suggested. Commented Dec 26, 2020 at 19:20
  • 1
    Your (block nil ... (return ...)) approach is not hygienic: consider (loop ... (xond ... (return 3)) ...). You definitely don't need to use return / return-from in a macro like this, but if you do, do it hygienically by using a gensym for the block name. Commented Dec 27, 2020 at 11:05
  • @tfb. Just edited my question with your comments. I want to use 'return' to skip remaining conditions. Commented Dec 27, 2020 at 13:47
  • You don't need to do that if you take the obvious approach (as recommended in coredump's answer) of generating (if c1 r1 (if c2 r2 ...)). That's hugely more natural in an expression language like Lisp than some sucky imperative thing with explicit control transfers all over the place. Commented Dec 27, 2020 at 17:54

3 Answers 3

3

Some remarks

  • You do not need a progn when you only expand into a single if
  • The use of pop might be confusing for the reader (and the programmer too) since it mutates a place, maybe you want to start with a less imperative approach

Also, in that case I don't think a loop approach is helpful, because you need to nest the expressions that come after in the body inside a previously built form, and even though it can be done, it is a bit more complex to do that simply a recursive function or a "recursive" macro.

Here I explain both approach, starting with "recursive" macro (the quote here is because the macro does not call itself, but expands as call to itself).

Macro expansion fixpoint

If I had to implement xond, I would write a macro that expands into other calls to xond, until macroexpansion reaches a base case where there are no more xond:

(defmacro xond (&rest body)
  (if (rest body)
      (destructuring-bind (test if-action . rest) body
        `(if ,test ,if-action (xond ,@rest)))
      (first body)))

For example, this expression:

(xond (= n 1) (setq x 2) (= n 2) (setq x 1))

First macroexpands into:

(if (= n 1)
    (setq x 2)
    (xond (= n 2) (setq x 1)))

And eventually reaches a fixpoint with:

(if (= n 1)
    (setq x 2)
    (if (= n 2)
        (setq x 1)
        nil))

Be careful, you cannot directly use xond inside the definition of xond, what happens is that the macro expands as a call to xond, which Lisp then expands again. If you are not careful, you may end up with an infinite macroexpansion, that's why you need a base case where the macro does not expand into xond.

Macro calling a recursive function

Alternatively, you can call a recursive function inside your macro, and expand all the inner forms at once.

With LABELS, you bind xond-expand to a recursive function. Here this is an actual recursive approach:

(labels ((xond-expand (body)
           (if body
               (list 'if
                     (pop body)
                     (pop body)
                     (xond-expand body))
               nil)))
  (xond-expand '((= n 1) (setq x 2) (= n 2) (setq x 1))))

 ; => (IF (= N 1)
 ;    (SETQ X 2)
 ;    (IF (= N 2)
 ;        (SETQ X 1)
 ;        NIL))
Sign up to request clarification or add additional context in comments.

3 Comments

Depending on what xond is meant to do there might need to be an error case for an arglist length of 1 I think (it should be zero or 2 or more).
@tfb in the macro destructuring-bind would error but I agree there should be better error handling. In the labels I will rewrite to use pop too, so that it will error too.
@tfb I don't expect error handling in the answer. That can be added later.
2

Your xond macro ends with (return t) so it evaluates to t rather than your accumulated if expressions.

You could use loop's collect clause to accumulate the code you wish to return. For example: (loop for x in '(1 2 3) collect (* 2 x)) would evaluate to (2 4 6).

3 Comments

I see that now. I understand the problem; in Python I would accumulate strings in a list and return a join an the end, but here, I'm lost. Code will be appreciated.
If I get time to spend on this problem I will see if I can produce something to help you. For now I've added a suggestion about the collect clause of loop which would probably be useful here.
You answer give the hint to solve the problem. I'm learning Lisp and want to solve it with the most basic tools.
0

How about

(ql:quickload :alexandria)

(defun as-last (l1 l2)
  `(,@l1 ,l2))

(defmacro xond (&rest args)
  (reduce #'as-last
          (loop for (condition . branch) in (alexandria:plist-alist args)
                collect `(if ,condition ,branch))
          :from-end t))

(macroexpand-1 '(xond c1 b1 c2 b2 c3 b3))
;; (IF C1 B1 (IF C2 B2 (IF C3 B3))) ;
;; T

alexandria's plist-alist was used to pair the arguments, the intrinsic destructuring in loop used to extract conditions and branches.

The helper function as-last stacks lists together in the kind of (a b c) (d e f) => (a b c (d e f)).

(reduce ... :from-end t) right-folds the sequence of the collected (if condition branch) clauses stacking them into each other using #'as-last.

Without any dependencies

('though, does alexandria even count as a dependency? ;) )

(defun pairs (l &key (acc '()) (fill-with-nil-p nil))
  (cond ((null l) (nreverse acc))
        ((null (cdr l)) (pairs (cdr l) 
                               :acc (cons (if fill-with-nil-p
                                              (list (car l) nil)
                                              l) 
                                          acc) 
                               :fill-with-nil-p fill-with-nil-p))
        (t (pairs (cdr (cdr l)) 
                  :acc (cons (list (car l) (cadr l)) acc) 
                  :fill-with-nil-p fill-with-nil-p))))

(defun as-last (l1 l2)
  `(,@l1 ,l2))

(defmacro xond (&rest args)
  (reduce #'as-last
          (loop for (condition branch) in (pairs args)
                         collect `(if ,condition ,branch))
          :from-end t))

(macroexpand-1 '(xond c1 b1 c2 b2 c3 b3))
;; (IF C1 B1 (IF C2 B2 (IF C3 B3))) ;
;; T

The helper function pairs makes out of (a b c d e f) => ((a b) (c d) (e f)).

(:fill-with-nil-p determines in case of odd number of list elements, whether the last element would be listed (last-el) or (last-el nil) - in the latter case filled with nil).

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.