Как 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,

Другие вопросы по тегам