Как setf работает под капотом?
В настоящее время изучаю общий язык, следуя Практическому общему лиспу Питера Сейбела (я в главе 11, о коллекциях), мне трудно понять, как setf
работает за капотом.
Учитывая это выражение:
(setf a 10)
Я полностью понял, как интерпретатор может (1) получить переменную с именем a
и (2) изменить значение, на которое оно указывает 10
,
Теперь, в случае определенных коллекций, например списков, векторов или хеш-таблиц, setf также может использоваться для изменения значений, содержащихся в коллекции. Например, с вектором:
(defparameter *x* '(a b c d))
(setf (elt *x* 1) bb)
Это вызывает у меня подозрение setf
потому что он в конечном итоге находит нетривиально доступную информацию или создает чёрную магию. Я вижу множество возможностей.
1. setf является функцией
(elt *x* 1)
выражение возвращается 'b
, так setf
практически работает с (setf b bb)
, Я тогда не понимаю, как setf
можно сделать вывод, какой объект (здесь, список *x*
) он должен измениться, не имея возвращаемого значения elt
содержит как указание на то, что оно происходит из коллекции, так и указатель на указанную коллекцию. Кажется сложным.
2. setf - это макрос
Идея в том, как setf
это макрос, он работает напрямую с (setf (elt *x* 1) bb)
и, следовательно, может извлечь elt *x* 1
часть, чтобы определить, какой объект / коллекция используется, и, следовательно, должен быть изменен.
Это не кажется очень эффективным, ни надежным, ни устойчивым к сложным операциям. Тем не менее, так как я не могу запустить этот код:
(funcall (find-symbol (concatenate 'string "E" "LT")) *x* 1) ; -> B
(setf (funcall (find-symbol (concatenate 'string "E" "LT")) *x* 1) 'bb) ; -> ERROR : (SETF FUNCALL) is only defined for functions of the form #'symbol
Это заставляет меня думать, что setf
является макросом, реализующим очень простую эвристику для извлечения вызываемой функции и всей другой необходимой информации. Кажется сложным.
3. setf является частным случаем интерпретации
Другой путь может заключаться в том, чтобы интерпретатор по-разному обрабатывал setf, имея дело с какой-то черной магией для правильной реализации ожидаемого поведения. Кажется сложным.
4. есть что-то, чего я не знаю о лиспе
Вероятно, реальный ответ. Что я упустил?
Дополнительный вопрос: зависит ли метод реализации от реализации интерпретатора lisp? (или, проще говоря, что именно стандартный стандарт lisp определяет для реализации setf) В настоящее время я использую clisp, но приветствуется понимание других реализаций.
2 ответа
SETF
это макрос, который устанавливает значение для места. Место означает форму с расширением setf. Существуют различные виды встроенных мест, и вы можете определить больше (см., Например, DEFSETF
а также DEFINE-SETF-EXPANDER
, формы вызова функций как места и макросы формы как места).
Вы можете получить расширение setf для формы, используя GET-SETF-EXPANSION
, Возвращает пять значений. Например,
(get-setf-expansion '(elt *x* 1))
;=> (#:*X*660)
; (*X*)
; (#:NEW1)
; (SB-KERNEL:%SETELT #:*X*660 1 #:NEW1)
; (ELT #:*X*660 1)
Пятое значение - это форма получения, которая при оценке возвращает текущее значение места. Четвертый - это установочная форма, которая при оценке устанавливает новое значение для места. Здесь вы можете увидеть, что SBCL использует SB-KERNEL:%SETELT
установить значение.
Первое значение представляет собой список имен переменных, которые должны быть связаны со значениями, возвращаемыми формами во втором значении при оценке форм метода установки / получения. Третье значение представляет собой список переменных хранилища, которые должны быть связаны с новыми значениями, которые будут сохранены установщиком.
С их помощью мы можем определить простой MY-SETF
-macro.
(defmacro my-setf (place values-form &environment env)
(multiple-value-bind (vars vals stores setter)
(get-setf-expansion place env)
`(let* ,(mapcar #'list vars vals)
(multiple-value-bind ,stores ,values-form
,setter))))
Все, что нам нужно сделать, это связать переменные и оценить установщик. Обратите внимание, что среда должна быть передана GET-SETF-EXPANSION
, Мы игнорируем пятое значение (получатель), поскольку оно нам не нужно. MULTIPLE-VALUE-BIND
используется для привязки переменных хранилища, поскольку их может быть несколько.
(let ((list (list 1 2 3 4)))
(my-setf (elt list 2) 100)
list)
;=> (1 2 100 4)
(let ((a 10) (b 20) (c 30))
(my-setf (values a b c) (values 100 200 300))
(list a b c))
;=> (100 200 300)
Существуют различные способы определения своих мест. Самые простые способы - это использовать DEFSETF
или просто определите setf-функцию с помощью DEFUN
, Например:
(defun eleventh (list)
(nth 10 list))
(defun set-eleventh (list new-val)
(setf (nth 10 list) new-val))
(defsetf eleventh set-eleventh)
(let ((l (list 1 2 3 4 5 6 7 8 9 10 11 12 13)))
(setf (eleventh l) :foo)
l)
;=> (1 2 3 4 5 6 7 8 9 10 :FOO 12 13)
(get-setf-expansion '(eleventh l))
;=> (#:L662)
; (L)
; (#:NEW1)
; (SET-ELEVENTH #:L662 #:NEW1)
; (ELEVENTH #:L662)
(defun twelfth (list)
(nth 11 list))
(defun (setf twelfth) (new-val list)
(setf (nth 11 list) new-val))
(let ((l (list 1 2 3 4 5 6 7 8 9 10 11 12 13)))
(setf (twelfth l) :foo)
l)
;=> (1 2 3 4 5 6 7 8 9 10 11 :FOO 13)
(get-setf-expansion '(twelfth l))
;=> (#:L661)
; (L)
; (#:NEW1)
; (FUNCALL #'(SETF TWELFTH) #:NEW1 #:L661)
; (TWELFTH #:L661)
setf
это макрос.
Вы можете прочитать о Generalized Reference во всех подробностях, но, в основном, это работает так:
(setf symbol expression)
такой же как setq
В противном случае мы имеем (setq (symbol arguments) expression)
,
Если есть функция с именем (setf symbol)
(да, вы правильно прочитали, функция с именем со списком длиной 2!), затем она вызывается.
В противном случае setf
расширение генерируется ", используя определение из defsetf
или же define-setf-expander
,