0

Suppose I have a function that returns two values inside a loop macro. Is there an elegant way to collect the first values into one list and second values into another list?

As a silly example, consider the following function:

(defun name-and-phone (id)
  (case id
    (0 (values "Peter" 1234))
    (1 (values "Alice" 5678))
    (2 (values "Bobby" 8910))))

Now I would like to collect the names into one list and phone numbers into another one. I could do something like

(loop for i upto 2
      collect (nth-value 0 (name-and-phone i))
        into names
      collect (nth-value 1 (name-and-phone i))
        into phones
      finally (return (values names phones)))

but that means I have to call name-and-phone twice for each i, which seems inefficient.

I could also use a temporary dotted list

(loop for i upto 2
      collect (multiple-value-bind (name phone)
                  (name-and-phone i)
                (cons name phone))
        into names-and-phones
      finally (return (values (mapcar #'car names-and-phones)
                              (mapcar #'cdr names-and-phones))))

but that does not feel very elegant.

Is there a way I could use loop's collect inside the scope of multiple-value-bind?

Googling around I could not find much; the closest is this question, where OP is collecting a variable of loop (and not from a nested scope).

Dominik Mokriš
  • 1,118
  • 1
  • 8
  • 29

3 Answers3

4
(loop with name and number
      for i from 0 upto 2
      do (setf (values name number)
               (name-and-phone i))
      collect name into names
      collect number into numbers
      finally (return (values names numbers)))

Alternative: there is the slightly more powerful ITERATE macro as a library.

Rainer Joswig
  • 136,269
  • 10
  • 221
  • 346
  • 2
    I should have thought of `with name and number` but the `(setf (values name number) (name-and-phone i))` bit is new to me. Once again impressed by the Setter Guild! – Dominik Mokriš Oct 10 '21 at 17:06
3

Use Destructuring:

(loop for n from 0 to 10
      for (f r) = (multiple-value-list (floor n 3))
      collect f into fs
      collect r into rs
      finally (return (values fs rs)))
==> (0 0 0 1 1 1 2 2 2 3 3)
    (0 1 2 0 1 2 0 1 2 0 1)

A sufficiently smart compiler should be able to avoid consing up a list in multiple-value-list.

See also values function in Common Lisp.

sds
  • 58,617
  • 29
  • 161
  • 278
1

One of the problems with loop is that, while its very good at the things it does, if you want to do things it doesn't do you're immediately going to have a mass of not-very-easy-to-understand code. I think that's what you're seeing here, and the other answers are as good as any I think: it's just a bit painful.

I think the fashionable answer to making it less painful is either to use an extensible loop, of which there is at least one, or some alternative comprehensive iteration macro.

Having been down those rabbit holes further than anyone should be allowed to go, I've recently decided that the answer is not ever-more-fancy iteration macros, but decomposing the problem into, among other things, much less fancy macros which iterate and much less fancy macros which collect objects which then can be naturally composed together.

So in your case you could have:

(with-collectors (name phone)
  (dotimes (i 2)
    (multiple-value-bind (n p) (name-and-phone i)
      (name n) (phone p))))

or, because supporting multiple values naturally is quite useful, you could write this:

(defmacro collecting-values ((&rest collectors) &body forms)
  (let ((varnames (mapcar (lambda (c)
                            (make-symbol (symbol-name c)))
                          collectors)))
    `(multiple-value-bind ,varnames (progn ,@forms)
       ,@(mapcar #'list collectors varnames))))

giving you

(with-collectors (name phone)
  (dotimes (i 2)
    (collecting-values (name phone)
      (name-and-phone i))))

and then

> (with-collectors (name phone)
    (dotimes (i 2)
      (collecting-values (name phone)
        (name-and-phone i))))
("Peter" "Alice")
(1234 5678)

(collecting / with-collectors was written by a friend of mine, and can be found here.)

ignis volens
  • 7,040
  • 2
  • 12