2

I'm trying to write a function that will destructively remove N elements from a list and return them. The code I came up with (see below) looks fine, except the SETF is not working the way I intended.

(defun pick (n from)
  "Deletes (destructively) n random items from FROM list and returns them"
  (loop with removed = nil
     for i below (min n (length from)) do
       (let ((to-delete (alexandria:random-elt from)))
         (setf from (delete to-delete from :count 1 :test #'equal)
               removed (nconc removed (list to-delete))))
     finally (return removed)))

For most cases, this works just fine:

CL-USER> (defparameter foo (loop for i below 10 collect i))
CL-USER> (pick 3 foo)
(1 3 6)
CL-USER> foo
(0 2 4 5 7 8 9)
CL-USER> (pick 3 foo)
(8 7 0)
CL-USER> foo
(0 2 4 5 9)

As you can see, PICK works just fine (on SBCL) unless the element being picked happens to be the first on the list. In that case, it doesn't get deleted. That's because the only reassignment happening is the one that goes on inside DELETE. The SETF doesn't work properly (i.e. if I use REMOVE instead, FOO does not change at all).

Is there any scoping rule going on that I'm not aware of?

Guilherme
  • 608
  • 1
  • 6
  • 13
  • So, I just found [this other question](http://stackoverflow.com/questions/20074462/setf-in-a-function-does-not-work?rq=1) in which there seems to be a similar problem. I'm not using quoted data, though. Is there anything I'm missing here? – Guilherme Oct 29 '15 at 19:50
  • `below (min n (length from))` isnt' going make a lot of sense if you're altering the length of the list. – Joshua Taylor Oct 29 '15 at 20:01
  • 1
    You can't write a function that *"Deletes (destructively) n random items from FROM list and returns them"*. What happens if you want to delete one random item from `(42)`. The you can't turn a cons cell into an empty list. – Joshua Taylor Oct 29 '15 at 20:07
  • I know I'm altering the length of the list. I just want to make sure the altering happens no more times than the length of the list itself, hence the `below` clause. The `loop` throws an error if I delete it. Not that it matters, really, I'm just removing tens of items from a list that's hundreds of items-long. – Guilherme Oct 29 '15 at 20:12
  • No, that's not modifying a list. That's updating the value of a variable to be a different value. The kind of destruction that DELETE can do is changing the CAR and CAR of the cells in the list. E.g., if you do `(let* ((a (list 1 2))) (delete 1 a))` will return `(2)`, and the value of a will be exactly the same cons cell that it was before, but the CAR and CDR of that cons cell may be different. – Joshua Taylor Oct 29 '15 at 20:21
  • 1
    You might the question and answers in [DELETE is destructive — but not always?](http://stackoverflow.com/questions/19339030/delete-is-destructive-but-not-always) helpful. – Joshua Taylor Oct 29 '15 at 20:24

3 Answers3

5

A (proper) list consists of cons cells that each hold a reference to the next cell. So, it is actually a chain of references, and your variable has a reference to the first cell. To make this clear, I rename the binding outside of your function to var:

var ---> [a|]--->[b|]--->[c|nil]

When you pass the value of the variable to your function, the parameter gets bound to the same reference.

var ---> [a|]--->[b|]--->[c|nil]
        /
from --'

You can update the references in the chain, for example eliminate b:

var ---> [a|]--->[c|nil]
        /
from --'

This has an effect on the list that var sees outside.

If you change the first reference, for example eliminating a, this is just the one originating from from:

var ---> [a|]--->[b|]--->[c|nil]
                /
        from --'

This has obviously no effect on what var sees.

You need to actually update the variable binding in question. You can do that by setting it to a value returned by function. Since you already return a different value, this would then be an additional return value.

(defun pick (n list)
  (;; … separate picked and rest, then
    (values picked rest)))

Which you then use like this, for example:

(let ((var (list 1 2 3)))
  (multiple-value-bind (picked rest) (pick 2 var)
    (setf var rest)
    (do-something-with picked var)))

Now to the separation: unless the list is prohibitively long, I'd stick to non-destructive operations. I also would not use random-elt, because it needs to traverse O(m) elements each time (m being the size of the list), resulting in a runtime of O(n·m).

You can get O(m) overall runtime by determining the current chance of picking the current item while linearly running over the list. You then collect the item into either the picked or rest list.

(defun pick (n list)
  (loop :for e :in list
        :and l :downfrom (length list)
        :when (or (zerop n)
                  (>= (random 1.0) (/ n l)))
            :collect e :into rest
          :else
            :collect e :into picked
            :and :do (decf n)
        :finally (return (values picked rest))))
Svante
  • 50,694
  • 11
  • 78
  • 122
  • "You can get O(m) overall runtime by determining the current chance of picking the current item while linearly running over the list. You then collect the item into either the picked or rest list." This won't work quite right if the list contains duplicate elements. – Joshua Taylor Oct 30 '15 at 02:57
  • @JoshuaTaylor: I did not understand the requirements to specify that all duplicates of a randomly chosen element should be removed (even though the erroneous attempt seems to imply that effect). You are right to question it, and I should perhaps have asked for clarification, but my bet is that the removal of duplicates is actually not intended here. – Svante Oct 30 '15 at 08:51
  • 1
    @scanned actually, rereading op'so initial attempt, with `(delete to-delete from :count 1 :test #'equal)` *does* makes it like yours; duplicate elements are considered distinct. So your linear O(m) should be fine. – Joshua Taylor Oct 30 '15 at 12:09
3

Delete isn't required to modify any structure, it's just permitted to. In fact, you can't always do a destructive delete. If you wanted to delete 42 from (42), you'd need to return the empty list (), which is the symbol NIL, but there's no way that you can turn the list (42), which is a cons cell (42 . NIL) into a different type of object (the symbol NIL). As such, you'll probably need to return both the updated list, as well as the elements that were removed. You can do that with something like this, which returns multiple values:

(defun pick (n from)
  (do ((elements '()))
      ((or (endp from) (zerop n))
       (values elements from))
    (let ((element (alexandria:random-elt from)))
      (setf from (delete element from)
            elements (list* element elements))
      (decf n))))

CL-USER> (pick 3 (list 1 2 3 2 3 4 4 5 6))
(2 6 4)
(1 3 3 5)
CL-USER> (pick 3 (list 1 2 3 4 5 6 7))
(2 7 5)
(1 3 4 6)
CL-USER> (pick 2 (list 1 2 3))
(2 3)
(1)
CL-USER> (pick 2 (list 1))
(1)
NIL

On the receiving end, you'll want to use something like multiple-value-bind or multiple-value-setq:

(let ((from (list 1 2 3 4 5 6 7)))
  (multiple-value-bind (removed from)
      (pick 2 from)
    (format t "removed: ~a, from: ~a" removed from)))
; removed: (7 4), from: (1 2 3 5 6)

(let ((from (list 1 2 3 4 5 6 7))
      (removed '()))
  (multiple-value-setq (removed from) (pick 2 from))
  (format t "removed: ~a, from: ~a" removed from))
; removed: (3 5), from: (1 2 4 6 7)
Joshua Taylor
  • 84,998
  • 9
  • 154
  • 353
2

delete does not necessarily modify its sequence argument. As the hyperspec says:

delete, delete-if, and delete-if-not return a sequence of the same type as sequence that has the same elements except that those in the subsequence bounded by start and end and satisfying the test have been deleted. Sequence may be destroyed and used to construct the result; however, the result might or might not be identical to sequence.

For instance, in SBCL:

* (defvar f (loop for i below 10 collect i))

F
* (defvar g (delete 0 f :count 1 :test #'equal))

G
* g

(1 2 3 4 5 6 7 8 9)

* f

(0 1 2 3 4 5 6 7 8 9)
*

Note that in your function setf modifies the local variable from, and since delete in the case of first element does not modify the original list, at the end of the function the variable foo maintains the old values.

Renzo
  • 26,848
  • 5
  • 49
  • 61
  • And this is a very easy case; if you just need to remove the first element from a list, the easiest thing to do is just return the *rest* of the list. – Joshua Taylor Oct 29 '15 at 20:03
  • And delete *can't* be destructive if you try to delete in such a way that there'd be no elements left. E.g., if you have `(1)` and you try to delete `1`, you can't return that same cons cell, you have to return `nil`. There's no way to make `(1)` empty. – Joshua Taylor Oct 29 '15 at 20:06
  • I know I can't count on `DELETE` to change the values of my list. That's why I'm trying to `SETF` it inside the function. The thing is that the `SETF` seems to have no effect outside the function. – Guilherme Oct 29 '15 at 20:23
  • Just saw your edit. So `FROM`'s a variable local to my function. Got it! – Guilherme Oct 29 '15 at 20:23