Возврат новой структуры с измененными полями

Я ищу простой способ вернуть новую структуру, которая является копией существующей с некоторыми измененными полями, без изменения оригинала.

Я понимаю, что вы можете использовать setf для изменения данных в одном из полей, например:

[1]> (defstruct foo bar)
FOO
[1]> (defvar quux (make-foo :bar 12))
QUUX
[1]> (foo-bar quux)
12
[1]> (setf (foo-bar quux) 15)
15
[1]> (foo-bar quux)
15

Но, как я уже сказал, это по существу уничтожает исходные данные, а это не то, к чему я стремлюсь.

Я мог бы, конечно, сделать что-то вроде этого:

(defstruct foo bar baz) ; define structure
(defvar quux (make-foo :bar 12 :baz 200)) ; make new instance
(defvar ping (copy-foo quux)) ; copy instance
(setf (foo-bar ping) 15) ; change the bar field in ping

Но это больше похоже на обходной путь, чем на что-либо еще.

В Erlang вы можете сделать что-то вроде этого:

-record(foo, {bar, baz}). % defines the record

example() ->
  Quux = #foo{bar = 12, baz = 200}, % creates an instance of the record
  Ping = Quux#foo{bar = 15}. % creates a copy with only bar changed

Данные не изменены.

PS Да, я знаю, что Common Lisp — это не Erlang; но Erlang сделал удобной неизменяемую работу с записями/структурами, и, поскольку в Common Lisp поощряется функциональный стиль, было бы неплохо, если бы была доступна аналогичная опция.


person Electric Coffee    schedule 17.07.2016    source источник
comment
«Поскольку в Common Lisp поощряется функциональный стиль»: на самом деле я думаю, что мы можем сказать, что Common Lisp не зависит от функционального стиля, и, в частности, CLOS (объектно-ориентированная часть Common Lisp) полностью основана на побочных эффектах.   -  person Renzo    schedule 17.07.2016
comment
Можно сделать ООП неизменно   -  person Electric Coffee    schedule 17.07.2016
comment
Конечно, это возможно. Но в Common Lisp есть набор операторов, которые позволяют изменять работающие системы (например, изменения в классе автоматически изменяют уже созданные экземпляры), и это всегда считалось «бонусом» Common Lisp по сравнению с другими языками. Я не хочу сказать, что функциональный стиль невозможен и не является хорошим стилем: это отличный стиль программирования. Я только замечу, что я никогда не видел в Common Lisp чего-то, что «поощряло бы» функциональный стиль больше, чем императивный: по этой причине я думаю, что CL агностик по отношению к этому.   -  person Renzo    schedule 17.07.2016
comment
Я в основном основываю эту информацию на стране lisp, в одной из первых глав которой говорится, что функциональный стиль предпочтителен при написании кода на lisp.   -  person Electric Coffee    schedule 17.07.2016


Ответы (1)


записи в Erlang аналогичны структурам Prolog. Известный мне Пролог реализует это как предикат с именем update_struct/4, который допускает макрорасширение: он принимает параметр типа и расширяется до двух объединений. Такая же обработка выполняется в Erlang, согласно документации. В Common Lisp нам не нужно явно передавать тип, как показано в следующей функции update-struct:

(defun update-struct (struct &rest bindings)
  (loop
    with copy = (copy-structure struct)
    for (slot value) on bindings by #'cddr
    do (setf (slot-value copy slot) value)
    finally (return copy)))

Пример

CL-USER> (defstruct foo bar baz)
FOO
CL-USER> (defparameter *first* (make-foo :bar 3))
*FIRST*
CL-USER> (defparameter *second* (update-struct *first* 'baz 2))
*SECOND*
CL-USER> (values *first* *second*)
#S(FOO :BAR 3 :BAZ NIL)
#S(FOO :BAR 3 :BAZ 2)

Технические характеристики

Райнер Йосвиг любезно заметил, что:

Есть одна вещь, которая не определена стандартом: использование SLOT-VALUE на объектах структуры не определено. Но это должно работать в большинстве реализаций, поскольку они предоставляют эту функцию. Единственная реализация, в которой это не работает, — это GCL.

Действительно, HyperSpec говорит о SLOT-VALUE:

Обратите внимание, что поведение для условий и структур не указано.

Реализация может вести себя иначе, если структура поддерживается списком или вектором (см. параметр ̀:TYPE). В любом случае, если вам нужна переносимость, вам лучше использовать классы. Эта тема также была подробно объяснена Райнером в common lisp: slot-value для структур defstruct .

Другие неизменяемые структуры данных

Подумайте также об использовании списков свойств, которые прекрасно сочетаются с неизменяемым подходом.

Скажем, ваш первоначальный список x:

(:a 10 :b 30)

Тогда (list* :b 0 x) это следующий список:

(:b 0 :a 10 :b 30) 

... где самое последнее значение, связанное с :b, затмевает последующие (см. GETF ).

Детали петли

Список bindings представляет собой список свойств с чередованием ключей и значений (как параметры ключевых слов). В приведенном выше выражении LOOP я перебираю список привязок используя ON, что означает, что я рассматриваю каждый подсписок вместо каждого элемента. Другими словами, (loop for x on '(a b c d)) последовательно связывает x с (a b c d), (b c d), (c d) и, наконец, (c).

Однако, поскольку я предоставляю пользовательский аргумент BY, следующий элемент вычисляется с использованием CDDR< /a>, вместо CDR по умолчанию (таким образом, мы продвигаемся вперед на две ячейки вместо одной). Это позволяет мне рассматривать каждую пару элементов ключ/значение в списке и связывать их с slot и value благодаря синтаксису деструктурирования.

person coredump    schedule 17.07.2016
comment
Не могли бы вы объяснить, что именно вы делаете в этом цикле? Я не могу его расшифровать - person Electric Coffee; 17.07.2016
comment
Супер умное решение с объяснением. Даже не подозревал, что существуют универсальные структурные функции! Спасибо! - person Electric Coffee; 17.07.2016
comment
Есть одна вещь, которая не определена стандартом: использование SLOT-VALUE в объектах структуры не определено. Но это должно работать в большинстве реализаций, поскольку они предоставляют эту функцию. Единственная реализация, в которой он не работает, — это GCL. - person Rainer Joswig; 17.07.2016
comment
@RainerJoswig Большое спасибо. - person coredump; 17.07.2016