How do I loop over a longer list with map that depends on a shorter list wrapping it to apply some function to the longer list in Common Lisp?

Viewed 66

Basically, I'd like the equivalent of this code in Common Lisp, but in a preferably more convenient way.

(defun circular (items) 
  (setf (cdr (last items)) items))
(map 'list #'(lambda (x y) (+ x y)) 
     (circular '(1 2 3)) '(2 3 4 5))
;; ==> (3 5 7 6)
2 Answers

I think your code is almost good enough. (The only problem is that you should not modify a quoted list).

All you need to do is to restore the list you made circular:

(defun map-circular (result-type function governing-sequence &rest reused-lists)
  "Apply `function` to successive sets of arguments in which one argument is obtained from each sequence.
The result has the length of `governing-sequence`."
  (let ((last-cells (mapcar (lambda (list)
                              (let ((cell (last list)))
                                (setf (cdr cell) list)
                                cell))
                            reused-lists)))
    (unwind-protect
         (apply #'map result-type function governing-sequence reused-lists)
      (dolist (cell last-cells)
        (setf (cdr cell) nil)))))

Test:

(defparameter l1 (list 1 2))
(defparameter l2 (list 1 2 3))
(map-circular 'list #'+ '(1 2 3 4) l1 l2)
==> (3 6 7 7)
l1
==> (1 2)
l2
==> (1 2 3)

If you use the SERIES package, you can use the SERIES function which:

Creates an infinite series that endlessly repeats the given items in the order given.

This works if the items are known statically, ie. not from a list of values given at runtime.

Here below, the code scans a list and, at the same time, iterates over the infinite serie 1 2 3 .... Both series are iterated in parallel with mapping, as s and n, and mapping generates a series of (cons s n). The result is collected as a list:

(collect 'list
  (mapping ((s (scan 'list '(a b c d e f g h)))
            (n (series 1 2 3)))
    (cons s n)))

The result is:

((A . 1) (B . 2) (C . 3) (D . 1) (E . 2) (F . 3) (G . 1) (H . 2))

Series are subject to stream-fusion, and the expanded code is:

(let* ((#:out-823 (list 1 2 3)))
  (let (#:elements-816
        (#:listptr-817 '(a b c d e f g h))
        #:items-821
        (#:lst-822 (copy-list #:out-823))
        #:items-826
        (#:lastcons-813 (list nil))
        #:lst-814)
    (declare (type list #:listptr-817)
             (type list #:lst-822)
             (type cons #:lastcons-813)
             (type list #:lst-814))
    (setq #:lst-822 (nconc #:lst-822 #:lst-822))
    (setq #:lst-814 #:lastcons-813)
    (tagbody
     #:ll-827
      (if (endp #:listptr-817)
          (go series::end))
      (setq #:elements-816 (car #:listptr-817))
      (setq #:listptr-817 (cdr #:listptr-817))
      (setq #:items-821 (car #:lst-822))
      (setq #:lst-822 (cdr #:lst-822))
      (setq #:items-826 ((lambda (s n) (cons s n)) #:elements-816 #:items-821))
      (setq #:lastcons-813 (setf (cdr #:lastcons-813) (cons #:items-826 nil)))
      (go #:ll-827)
     series::end)
    (cdr #:lst-814)))

If you need to pass an arbitrary list to the function, you can make your own circular list by calling (nconc list list), which is simpler to write. Since you probably don't want to mutate the input list, you can write this instead:

(defun circularize (list)
  (let ((list (copy-list list)))
    (nconc list list)))
Related