Common Lisp - как вызвать / применить функцию с ключевыми аргументами?
Контекст
- С такими функциями, как
(lambda (List arg1 arg2 ... argn))
я могу использоватьfuncall
/apply
для вызова этих методов с исходными аргументами и, таким образом, изменения списка внутри лямбда. - С такими функциями, как
(lambda (arg1 arg2 ... argn &key List))
Я могу использовать толькоfuncall
/apply
с копиями аргументов, что означает, что я не могу изменять их внутри функций. - Как я могу использовать функции, подобные в 2. с той же функциональностью, что и в 1.?
Подробно о проблеме
1. Работающие функции
С (lambda (mem arg1 arg2 ... argn))
:
;; Can pass the original lists for modification inside the function:
(funcall #'fn program-memory args-list)
функции могут изменять эти списки.
2. Функции, которые теряют возможность изменять аргументы
С (lambda (arg1 arg2 ... argn &key mem))
, Я могу назвать это только копией исходных списков:
;; can only pass copies of the lists :(
(apply #'fn (concatenate 'list args (list :mem program-memory)))
Таким образом, я больше не могу изменять программную память.
3. Как сделать так, чтобы функции в 2. работали как в 1.?
Как заставить его работать? Т.е. вызовите функцию с исходным списком, а не с копией.
Пример с упрощенным старым кодом (как в 1.):
(defun mem/w (memory address value)
"Writes the value to memory at address. Returns nil."
(setf (elt memory address) value)
nil)
;; sum
(defun sum-op (mem a b o)
(mem/w mem o (+ a b)))
(let ((program (list 1 2 3 4 5 6 7 8))
(args (list 1 2 0)))
(apply #'sum-op
(cons program args))
(print program)) ;; shows modification --> good
Полный код находится на https://github.com/AlbertoEAF/advent_of_code_2019/blob/master/common-lisp/day5.lisp.
1 ответ
Кажется, возникло недопонимание того, что происходит, когда вы звоните:
(concatenate 'list args (list :mem program-memory))
Список аргументов args
а также (list :mem program-memory)
используются для создания нового списка. Здесь вы могли бы использоватьappend
, вот так: (append args (list :mem program-memory)
. В обоих случаях исходные списки не изменяются, но вы получаете новый список аргументов (который может использовать последний список, но это деталь).
Однако содержимое обоих списков входов и результирующего списка идентично, одни и те же объекты упоминаются в этих списках до и после конкатенации, неявная копия объектов отсутствует.
Посмотрим:
(defclass something () ())
(defvar *something* (make-instance 'something))
Когда я оцениваю *something*
, результирующий объект печатается как #<SOMETHING {10091B1973}>
(печатное представление может различаться в зависимости от реализации; идентификатор вашего объекта будет другим).
Если я внесу его в список и позвоню copy-list
, результирующий список по-прежнему имеет то же значение:
(let ((list (list *something*)))
(assert (eq (first list)
(first (copy-list list)))))
То же самое применимо, если вы храните списки внутри списка, они не будут копироваться рекурсивно без явного вызова копирования. Фактически, давайте попробуем тот же пример, который вы привели, но с ключевыми словами:
;; unrelated, but for this Advent of Code you are going to have
;; performance issues if you treat memory as a list, and not a vector.
;; Here I change it to a vector.
(defun mem/w (memory address value)
"Writes the value to memory at address"
(setf (svref memory address) value))
;; mem is now a keyword argument
(defun sum-op (a b o &key mem)
(mem/w mem o (+ a b)))
(let ((memory (vector 0 2 3 0 0 0 0 0))
(args (list 1 2 0)))
;; using the backquote/slice syntax for brevity
;; but the result is like concatenate/append
(apply #'sum-op `(,@args :mem ,memory))
memory)
Результирующее состояние памяти:
#(3 2 3 0 0 0 0 0)
NB. Неопределенным является изменение самого списка аргументов.
Редактировать:
Возможно, вы объединили саму память с аргументами, и в этом случае в вызываемой функции использовался новый список, представляющий память, но если да, то это ошибка, потому что объединение должно изменять только список аргументов, а не один аргумента.