DELETE разрушительно, но не всегда?

Меня немного смущает деструктивная функция DELETE в Common Lisp. Кажется, он работает так, как ожидалось, за исключением того, что элемент является первым элементом в списке:

CL-USER> (defvar *test* (list 1 2 3))
*TEST*
CL-USER> (delete 1 *test*)
(2 3)
CL-USER> *test*
(1 2 3)
CL-USER> (delete 2 *test*)
(1 3)
CL-USER> *test*
(1 3)

person mck    schedule 12.10.2013    source источник


Ответы (3)


«Разрушительный» не означает «действующий на месте». Это означает, что структура обрабатываемого значения может быть изменена произвольным и часто неопределенным образом. В некоторых случаях реализации и случаи могут иметь эффект, как если бы он был «на месте». Однако на это, как правило, нельзя полагаться.

Если вы используете деструктивный оператор, вы сообщаете компилятору, что вас не интересует предыдущее значение переменной после завершения операции. Вы должны предположить, что это значение впоследствии искажено до неузнаваемости, и больше не использовать его. Вместо этого следует использовать возвращаемое значение операции.

(let ((a (list 1 2 3)))
  (let ((b (delete 2 a)))
    (frob b))
  a)

=> You were eaten by a grue.

Если вы не уверены в безопасности деструктивных операций, используйте их недеструктивные аналоги (в данном случае remove).

(let ((a (list 1 2 3)))
  (let ((b (remove 2 a)))
    (frob b))
  a)

=> (1 2 3)

Если вы действительно хотите изменить содержимое переменной, установите для них возвращаемое значение операции:

(let ((a (list 1 2 3)))
  (setf a (delete 2 a))
  a)

=> (1 3)
person Svante    schedule 12.10.2013
comment
Ну, не столько предыдущее значение переменной, сколько структура значения, которое содержит хотя бы эта переменная. В конце концов, совместное использование структуры не редкость. - person Vatine; 13.10.2013
comment
И это не ограничивается значением переменной [a]. Вы можете сделать, например, (delete 1 (rest some-list)). Ему просто нужен список в качестве аргумента; это не макрос, и ему не нужно место. - person Joshua Taylor; 13.10.2013
comment
@mck Обратите внимание, что результаты в первом случае не так непредсказуемы, как если бы вас съела гадость. Хотя car и cdr любой cons-ячейки в списке могут быть изменены, они все равно будут cons-ячейками. В первом случае a всегда будет cons-ячейкой после вызова (delete 2 a), даже если впоследствии car и cdr будут другими. Что еще более важно, это все еще будет та же cons-ячейка, что и раньше. Например, см. этот пример кода. - person Joshua Taylor; 29.10.2013

Имейте в виду, что DELETE должен работать со списком, а не с переменной. DELETE передается список (указатель на первый минус), а не переменная.

Поскольку delete не имеет доступа к переменной *test*, он не может ее изменить. «Это» здесь означает его привязки. *test* будет указывать на ту же ячейку cons, что и раньше. Единственное, что можно изменить, — это содержимое cons-ячейки или содержимого других cons-ячеек, на которые указывает первая cons-ячейка.

Одно можно сказать наверняка: независимо от того, что делает DELETE, *test* всегда будет указывать на одну и ту же cons-ячейку.

Что из этого следует? Если вы хотите, чтобы *test* указывало на результат операции удаления, вы должны явно указать это:

(setq *test* (delete 1 *test*))
person Rainer Joswig    schedule 12.10.2013

DELETE работает, изменяя CDR предыдущей cons-ячейки списка так, чтобы она указывала на ячейку, расположенную после удаляемых элементов. Но если вы удаляете первый элемент списка, предыдущая ячейка cons для изменения отсутствует.

Хотя эта реализация на самом деле не указана в стандарте языка, так работает практически каждая реализация.

Поскольку при удалении первого элемента списка нет предыдущей ячейки cons, которую нужно изменить, он просто возвращает вторую ячейку cons. Таким образом, несмотря на то, что DELETE разрешено изменять список, вы все равно должны присвоить результат своей переменной, чтобы обработать этот случай. Кроме того, следует подчеркнуть, что деструктивное поведение только разрешено по стандарту, а не требуется. Таким образом, существует отдаленная вероятность того, что какая-то реализация вообще не реализует его деструктивно, и вы также должны допустить это.

И даже если DELETE сработало, изменив CAR вместо CDR, все же есть случай, когда он не может сделать деструктивно: удаление всех элементов списка, например.

(setq *test* (list 'a 'a))
(delete 'a *test*)

Это приводит к пустому списку, т.е. NIL. Но *test* по-прежнему содержит ячейку cons, которая была головой исходного списка, и DELETE никак не может это изменить. Итак, вы должны сделать:

(setq *test* (delete 'a *test*))

чтобы установить *test* на NIL.

person Barmar    schedule 12.10.2013
comment
Это может быть обычная реализация, но это не указано. Delete может собрать все неудаленные элементы и вернуть новый список (т. е. быть полностью неразрушающим) или может скопировать n неудаленные элементы в cars первого n cons в списке и установите для cdr ячейки n значение nil. Конкретная реализация не продиктована стандартом. Ваше описание также не будет охватывать случай, например, (delete 1 (list 1)) и (delete 1 (list 1 2)), где ничего не нужно изменять; delete может просто вернуть cdr аргумента списка. - person Joshua Taylor; 13.10.2013