Неразрушающий набор?
Common Lisp, похоже, идет на все, чтобы предоставить как неразрушающие функции (например, subst & remove), так и деструктивные функции и модифицировать макросы (например, delete & rotatef) для общего использования. Предположительно это делается для эффективной поддержки как функциональных, так и нефункциональных стилей программирования. Но также, кажется, есть определенный уклон к нефункциональному стилю в дизайне вездесущего setf
, setf
макрос, включающий обобщенную ссылку, по-видимому, достаточно гибок, чтобы изменять любое определяемое место (за исключением, возможно, неизменяемых целых чисел и символов). Эта сила, вероятно, объясняет его широкое использование, несмотря на его нефункциональное / разрушительное поведение.
Вопрос в том, почему нет соответствующего "функционального стиля" неразрушающего оператора, созданного по образцу setf
(назовите это положено, так как set
уже занято), аналогично другим парам неразрушающего / деструктивного оператора lisp. Такой оператор, вероятно, будет принимать те же аргументы, место и значение, но будет возвращать копию объекта, в который место было встроено, вместо нового значения в этом месте. Это также, вероятно, потребовало бы универсального копира некоторого вида, со стандартным setf
просто изменив копию перед возвратом. Тогда неразрушающий оператор может быть использован вместо setf
для большинства заданий, с setf
зарезервированы для действительно больших объектов. Является ли такой оператор осуществимым (или даже возможным) с учетом (предполагаемого) требования к универсальному копировальному устройству и необходимости извлечения объекта из произвольного места, встроенного в него?
2 ответа
Также не существует универсального установщика, но с SETF вы должны предоставить его при использовании DEFINE-SETF-EXPANDER. Я полагаю, вы могли бы определить эквивалент линз / функциональных ссылок. Для другого подхода Clojure определяет функцию обновления (заимствованную из других языков, я знаю, что она существует в Prolog), которая также не является универсальной. Библиотека FSET предоставляет некоторые функциональные структуры, в частности ассоциативные карты, которые работают так же, как в Clojure.
Предположим, вы ограничиваете себя структурами:
(defstruct foo a b c)
(defstruct bar x y z)
(defparameter *test*
(make-foo :a (make-bar :x 0 :y 0 :z 0)
:b 100
:c "string"))
;; *test* is now
;; #S(FOO :A #S(BAR :X 0 :Y 0 :Z 0) :B 100 :C "string")
Следующий подход делает копию, она опирается на copy-structure
а также slot-value
, который, хотя и не является стандартным, хорошо поддерживается:
(defun update (structure keys value)
(if (endp keys)
value
(destructuring-bind (key &rest keys) keys
(let ((copy (copy-structure structure)))
(setf (slot-value copy key)
(update (slot-value copy key)
keys
value))
copy))))
Вы можете обновить *test*
следующее:
(update *test* '(a z) 10)
Вот след:
0: (UPDATE #S(FOO :A #S(BAR :X 0 :Y 0 :Z 0) :B 100 :C "string") (A Z) 10)
1: (UPDATE #S(BAR :X 0 :Y 0 :Z 0) (Z) 10)
2: (UPDATE 0 NIL 10)
2: UPDATE returned 10
1: UPDATE returned #S(BAR :X 0 :Y 0 :Z 10)
0: UPDATE returned #S(FOO :A #S(BAR :X 0 :Y 0 :Z 10) :B 100 :C "string")
Чтобы обобщить, вы можете принять функцию вместо значения, чтобы вы могли реализовать эквивалент incf
частично применяя функцию обновления с #'1+
(результирующее закрытие примет список ключей).
Кроме того, вам необходимо обобщить операцию копирования, что возможно с помощью универсальных функций. Кроме того, вы можете использовать другие виды аксессуаров и заменить slot-value
с общим access
функция (для которой есть (setf access)
операция). Однако этот общий подход copy/setf может быть расточительным, если вы хотите поделиться некоторыми данными. Например, вам нужно всего лишь скопировать часть списка, которая ведет к вашим данным, а не те ячейки, которые следуют за ними.
Вы можете определить некоторые средства для определения пользовательских обновлений:
(defmethod perform-update (new (list list) position)
(nconc (subseq list 0 position)
(list new)
(nthcdr (1+ position) list)))
Common Lisp не имеет универсального копира по той же причине, по которой он не имеет встроенного печатаемого представления объектов CLOS (см., Например, Сохранение объектов CLOS): мощь MOP.
В частности, создание объекта может иметь произвольные побочные эффекты, репликацию которых трудно гарантировать. Например, определить initialize-instance
для вашего класса, чтобы изменить слоты на основе чего-то плавного (например, приливы или просто random
). Тогда результат make-instance
Вызванный с одинаковыми аргументами дважды может отличаться. Вы можете утверждать, что это аргумент в пользу memcpy
стиль универсального копира. Тем не менее, представьте себе класс, который выполняет учет экземпляров (:allocation :class
слот, который содержит хеш-таблицу, сопоставляющую уникальный идентификатор экземпляру). Теперь в оба конца от скопированного объекта через таблицу будет получен другой объект (оригинал, а не копия) - серьезное нарушение договора. И эти примеры - лишь вершина айсберга.
Однако я бы не согласился с тем, что Common Lisp поощряет императивный стиль больше, чем функциональный стиль. Это просто обескураживает стиль, который вы описываете (копировать + setf
). Подумайте об этом так: от функционального POV копия неотличима от оригинала (потому что все неизменно).
Есть также "маленькие подсказки", показывающие явное предпочтение функциональному стилю. Обратите внимание, что "естественные" имена (например, append
) зарезервированы для неразрушающих функций; разрушительные версии (например, nconc
) даны неизвестные имена.
Кроме того, имена путей, символы и числа (включая такие составные ratio
а также complex
) являются неизменяемыми, т. е. все функции pathname и number создают новые объекты (функциональный стиль).
Таким образом, IMO, Common Lisp поощряет программистов использовать функциональный стиль, в то же время делая императивный стиль выполнимым, в соответствии с лозунгом " простые вещи должны быть легкими, а сложные должны быть возможными ".
Смотрите также рецензию Кента Питмана.