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))))
Candid Moe
  • 131
  • 1
  • 9
  • 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? – coredump Dec 26 '20 at 19:13
  • 1
    @coredump. I edited my question to add a return as suggested. – Candid Moe Dec 26 '20 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. –  Dec 27 '20 at 11:05
  • @tfb. Just edited my question with your comments. I want to use 'return' to skip remaining conditions. – Candid Moe Dec 27 '20 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. –  Dec 27 '20 at 17:54
  • @tfb. I understand that, but it needs several not-yet-implemented functions in my minimal Lisp interpreter. – Candid Moe Dec 27 '20 at 18:50
  • @CandidMoe: it needs working macroexpansion, that's all. –  Dec 27 '20 at 19:27

3 Answers3

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))
coredump
  • 37,664
  • 5
  • 43
  • 77
  • 1
    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). –  Dec 26 '20 at 18:49
  • @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. – coredump Dec 26 '20 at 19:01
  • @tfb I don't expect error handling in the answer. That can be added later. – Candid Moe Dec 26 '20 at 21:41
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).

verdammelt
  • 922
  • 10
  • 22
  • 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. – Candid Moe Dec 26 '20 at 14:59
  • 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. – verdammelt Dec 26 '20 at 15:42
  • You answer give the hint to solve the problem. I'm learning Lisp and want to solve it with the most basic tools. – Candid Moe Dec 26 '20 at 15:53
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).

Gwang-Jin Kim
  • 9,303
  • 17
  • 30