Search code examples
iterationcommon-lispdestruction

Why can't I delete the current element when iterating over a list?


I want to iterate over a list, perform an action with the elements and based on some criteria, I want to get rid of the active element. However, when using the function below I end up in an infinite loop.

 (defun foo (list action test)
   (do ((elt (car list) (car list)))
       ((null list))
     (funcall action elt)
     (when (funcall test elt)
       (delete elt list))))

(setq list '(1 2 3 4))
(foo list #'pprint #'oddp)
-> infinite loop

Is it not possible as it points to itself? In the end, elt is (car list) of course.

Is this a correct assessment? And how could I solve this efficiently?


Solution

  • Actually you can alter the state of your list while iterating over it. You will just have to use rplacd in addition to delete, and control the advancement along the list not in the iteration clause, but inside the do body:

     (defun nfoo (lst action test)
       (do* ((list (cons 1 lst))
             (elt (cadr list) (cadr list)))
            ((null (cdr list))
             (if (funcall test (car lst)) (cdr lst) lst))
         (funcall action elt)
         (if (funcall test elt)
           (rplacd list (delete elt (cddr list)))
           (setf list (cdr list)))))  
    

    You should call it via copy-list if you don't want it to destroy the argument list.

    If you want to remove from your list not all elements equal to elt that passed the test, but rather all such that will pass the test, then the delete call will need to be passed the test function as the :test argument.

    (edit:) and even much simpler and straightforward, like this (non-destructive) version:

    (defun foo (list action test)
       (do* ((elt (car list) (car list)))
            ((null list))
         (funcall action elt)
         (if (funcall test elt)
           (setf list (delete elt list))
           (setf list (cdr list)))))