Обработка коллекций

Обработка коллекций это отдельная тема для статьи. Во-первых я сам не сразу понял фишку, во-вторых кроме этой фишки там полная путаница в обработке этих коллекций. Всё дело в том, что lisp немного функциональный, а немного нет. Сейчас разложить всё по полочкам, чтобы сразу всё стало ясным и больше к этому не возвращаться.

Виды функций обработки коллекций

Функции обработки коллекций делятся на несколько видов на основе своего поведения с исходными данными:

Обработка коллекций

Эта схема не является классическим делением, поэтому ссылаться на неё не стоит. Сейчас поясню что здесь к чему. Операции бывают копирующие и разрушающие. Про копирующие всё ясно — отправили вы список в аргументах функции, а в результате вернулся другой список. Так вот в случае копирующих операций эти два списка независимы и можно спокойно менять один не боясь за то, что с другим что-то случится. Пример копирующей операции — reverse:

(defvar *a* (list 1 2 3))
(defvar *b* (reverse *a*))
(setf (elt *b* 1) 4)

(format t "~{~d~^ ~}~%" *a*)
(format t "~{~d~^ ~}~%" *b*)

;; Вывод:
; 1 2 3
; 3 4 1

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

(defparameter *list-1* (list 1 2 3))
(defparameter *list-2* (reverse *list-1*))
(defparameter *list-3* (append *list-1* *list-2*))

(setf (elt *list-3* 5) 5)

(format t "~{~d~^ ~}~%" *list-1*) ; 1 2 3
(format t "~{~d~^ ~}~%" *list-2*) ; 3 5 1
(format t "~{~d~^ ~}~%" *list-3*) ; 1 2 3 3 5 4

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

Утилизирующие операции

Обработка коллекций была бы не полной, а lisp не был бы функциональным без утилизирующих операций. Смысл таких операций в том, что аргументы, переданные в такую функцию нам скорее всего не понадобятся, поэтому с ними можно делать всё что угодно. Например, из таких переменных можно построить возвращаемое значение. Эти операции оптимизированы для скорости и меньшего выделения памяти. Последнее означает, что и сборщик мусора будет меньше работать. Данные операции делятся на 2 типа — операции без чётких побочных действий и с чёткими побочными действиями. Данное деление довольно условно. Сейчас поясню его на примере:

(setf *list* (nreverse *list*)) ;;вызов нечёткой утилизирующей функции
(defparameter *x* (list 1 2 3))
(nconc *x* (list 4 5 6)) ;;вызов нечёткой утилизирующей функции
;; *x* ==> (1 2 3 4 5 6)

Видно, что вся разница здесь в том, что в случае nreverse мы присваиваем результат функции переменной, а в случае nconc нет. Хотя если записать так:

(setf *x* (nconc *x* (list 4 5 6)))

То результат не изменится. Думаю замедление так же не будет значительным. Поэтому данную схему можно упростить откинув классификацию утилизирующих операций.

Итоги

Что в итоге мы имеем? В lisp операции делятся на 3 типа — копирующие, разрушающие (не копирующие), утилизирующие. Первые создают копию данных, поэтому можно не бояться за их сохранность, примером таких функций могут служить: reverse.

Примером разрушающих: append;

Примером утилизирующих: nconc, DELETE, DELETE-IF, DELETE-IF-NOT, DELETE-DUPLICATED, NSUBSTITUTE.

В общем случае утилизирующие функции имеют такое же имя, как их недеструктивные двойники с подставленной первой буквой N.

Разумеется есть ещё и совсем не входящие в такую классификацию функции, которые только читают данные, не меняя их. Например count или length.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.