Я начинающий лингвист и решил написать бота для Telegram в качестве тестового проекта. Здесь я столкнулся с неожиданным поведением let
. Код представлен в виде системы .asd с одним файлом, содержащим следующий код. Важная часть, на которую следует обратить внимание:
long-poll-updates
функция по сути представляет собой цикл.
Вызов read-updates
в long-poll-updates
функции
Вызов check-integrity
в функции read-updates
.
checks
переменная в произвольной форме.
На каждой итерации основного цикла я делаю (prin1 checks)
и вижу что-то, что мне трудно найти или понять. На первой итерации цикла он печатает именно то значение, которое я ему присвоил. Каждая новая итерация не переназначает значения nil
обратно, а использует те же точные значения, которые остались после операций предыдущей итерации.
(defun long-poll-updates ()
"Main loop. Repeatedly sends requests to get updates."
; Sets a variable for storing the last processed updates's ID.
(let ((offset 0)) (loop
(let* ((api-answer (get-updates-request offset))
(parsed-plist (jonathan:parse
(flexi-streams:octets-to-string api-answer))))
;; Read response and modify offset parameter to get next updates.
(let ((response-data (read-updates parsed-plist)))
(when (getf response-data :has-results)
(setf offset
(1+ (getf response-data :last-update-id)))))))))
(defun read-updates (response-plist)
"Reads the incoming long poll response:
checks for response validity/errors,
proceeds to an appropriate action."
(let ((response-data (check-integrity response-plist)))
(cond ((getf response-data :has-ok)
(cond ((getf response-data :is-ok)
;; Evaluate updates on successful poll
(cond
((getf response-data :has-results)
(setf (getf response-data :last-update-id)
(eval-updates response-plist)))
(t
(log-data "No results received."))))
(t (log-errors response-plist))))
(t (log-data "Received malformed JSON-response while long polling.")))
response-data))
(defun check-integrity (response-plist)
"Runs checks for valid JSON received, success/faliure and presence of new updates.
Returns a plist of checks passed/failed."
(let ((checks '(:has-ok nil
:is-ok nil
:has-results nil)))
(prin1 checks)
(loop :for (indicator value) on response-plist by #'cddr
;; If successful response:
:when (eql indicator :|ok|)
:do (progn
(setf (getf checks :has-ok) t)
(when (eql value t)
(setf (getf checks :is-ok) t)))
;; If any results:
:when (and (eql indicator :|result|)
(listp value)
(< 0 (length value)))
:do (setf (getf checks :has-results) t))
checks))
Полагаю, я ожидал, что код функции будет оцениваться заново каждый раз, когда она вызывается, даже не осознавая этого. Тем не менее, похоже, что компилятор оценивает данные какому-то объекту, который никогда не покидает память, прежде чем код когда-либо будет запущен. Может ли кто-нибудь указать мне на более великую идею, с которой я здесь имею дело?
Я вижу, что подход lisp на самом деле «сначала теория», и я редко встречаю людей, которые действительно документируют проблемы, с которыми они сталкиваются, подходя к нему в блочной и буквальной манере (или как бы это ни выглядело, когда вы новичок в lisp, но уже имеете опыт программирования). . Мне это кажется невероятно странным, но дело не в этом.
«Почему переменные «позволяют» сохранять изменения, внесенные в них в новых итерациях?» -> вы не меняете переменную let, вы меняете данные, которые привязаны к переменной let. Эти данные представляют собой константный литерал кавычки. Данные оцениваются сами по себе каждый раз. Таким образом, переменная let каждый раз привязывается к одной и той же ячейке cons. Затем вы изменяете данные, а не привязку let.
Еще один совет: не размещайте код с табуляцией вместо пробелов для отступов. В этом случае Stackoverflow может неправильно отображать отступы.
Проблема в том, что опубликованный код изменяет литеральный объект, т. е. checks
является литералом списка, а setf
используется для его изменения. Но согласно HyperSpec:
Последствия неопределенны, если литеральные объекты будут деструктивно изменены.
В этом случае кажется, что хранилище для checks
используется повторно, что является совершенно законной оптимизацией, поскольку вы не должны изменять хранилище для литерала списка, привязанного к checks
.
Один из простых способов решить проблему — просто использовать (list :has-ok nil :is-ok nil :has-results nil)
, поскольку list
создает новый список. Другое решение — использовать дерево копирования, чтобы создать новую копию списка, например (copy-tree '(:has-ok nil :is-ok nil :has-results nil))
. Это может быть особенно полезно, если вы работаете с переменной, которая может быть привязана к литералу списка, например, в функции, которая принимает аргумент списка, для которого вызывающая сторона может предоставить список в кавычках.
Кстати, это не редкость для других языков. Например, в C попытка изменить строковый литерал приводит к неопределенному поведению. В общем, вам всегда следует выяснить, как используемый вами язык обрабатывает объекты, обозначаемые литералами, прежде чем пытаться их изменить или рисковать подобными ошибками.
Это часто задаваемый вопрос. Ваш код изменяет константный литеральный объект.