为什么我不能在迭代列表时删除当前元素?

时间:2012-02-27 17:16:42

标签: iteration common-lisp destruction

我想迭代列表,使用元素执行操作,并根据某些条件,我想摆脱活动元素。但是,当使用下面的函数时,我最终会进入无限循环。

 (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

它不可能因为它指向自己吗?最后,elt当然是(car list)

这是正确的评估吗?我怎么能有效地解决这个问题?

3 个答案:

答案 0 :(得分:3)

循环是无限的,因为你没有迭代任何东西,你重复应用action,但如果它没有改变元素,因为pprint显然没有,那么如果{ {1}}结果是否定的,那么它将保持不变,即使删除工作正常,列表也不会为空。

DELETE是一种破坏性的功能。在Common Lisp中,允许破坏性操作销毁他们的参数。您应该放弃对参数的任何引用,并仅使用返回值。操作完成后,无法保证参数的状态。特别是,可能没有效果,因为实现也被允许与非破坏性对应物相同地行动,但通常序列的组成部分将以某种难以预测的方式重新组装。你也在你的例子中摧毁一个文字,它有不明确的行为,应该避免。

通常最好将Common Lisp中的列表视为不可变和破坏性操作作为微观化,只有在您确定它们不会破坏任何内容时才能使用。对于此问题,您可能希望使用LOOP在条件test上汇总结果列表来遍历列表。请参阅LOOP chapter of PCL

答案 1 :(得分:1)

实际上,您可以在迭代时更改列表的状态。除了rplacd之外,您还必须使用delete,并控制列表中的进度,而不是在迭代子句中,而是在do体内:

 (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)))))  

如果你不想破坏参数列表,你应该通过copy-list调用它。

如果您要从列表中删除并非所有通过测试的elt元素,而是所有通过测试的元素,那么delete调用将需要作为test参数传递:test函数。

编辑),甚至更简单直接,就像这样(非破坏性)版本:

(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)))))

答案 2 :(得分:0)

我对lisp有点新意,所以也许我在你的问题中遗漏了一些东西。不过,我认为我理解你在问什么,我想知道为什么你没有使用现有的结构......即remove-if-not(或remove-if如果我有事情倒退)和mapcar ......

(mapcar #'pprint (remove-if-not #'oddp '(1 2 3 4))

以上打印13(并返回(nil nil),但可能您可以忽略它......或者您可以执行上述操作并以{{结尾1}})。 (如果您想要平价,请将(values)更改为remove-if-not。)

这让我觉得这可能是一种更明智的做事方式,除非你出于教学原因这样做或者我错过了一些东西......我承认其中任何一种都是可能的。 :)

P.S。 Hyperspec info on remove-if, remove-if-not, etc.