I'm a beginner lisper and I've decided to write a Telegram bot as a test project. Here I have faced some unexpected behaviour of let
. The code is in a form of .asd system with one file containing the following code. The important part of it to pay attention to:
long-poll-updates
function is basically a loop.
A call to read-updates
in long-poll-updates
function
A call to check-integrity
in read-updates
function.
checks
variable in let form.
On each iteration of the main loop I do (prin1 checks)
and see something that I struggle to look up or understand. On the first iteration of the loop it prints exactly the value that I've assigned to it. Each new iteration does not reassign nil
values back, but uses the same exact values left behind by the previous iteration's operations.
(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))
I suppose I expected function's code to evaluate anew each time it is called, without even realizing it. Yet it seems that compiler evaluates data to some entity that never leaves memory, before the code is ever run. Could anyone point my mind at a greater idea that I'm dealing with here?
I see that lisp's way is really "theory first" and I rarely find people actually documenting issues they face when approaching it in a blocky and litteral manner (or whatever it looks like when you're new to lisp, but had prior coding experience). I find it incredibly strange, but that's not the point.
The problem is that the posted code is modifying a literal object, i.e., checks
is a list literal and setf
is used to modify it. But according to the HyperSpec:
The consequences are undefined if literal objects are destructively modified.
In this case it seems that the storage for checks
is reused, which is a perfectly legal optimization since you aren't supposed to mutate the storage for the list literal bound to checks
.
One easy way to fix the problem is to just use (list :has-ok nil :is-ok nil :has-results nil)
since list
creates a fresh list. Another solution is to use copy-tree
to make a fresh copy of the list, e.g., (copy-tree '(:has-ok nil :is-ok nil :has-results nil))
. This can be especially useful if you are working with a variable which may be bound to a list literal, for example in a function that accepts a list argument for which the caller might provide a quoted list.
As an aside, this is not an uncommon situation in other languages. In C, for example, attempting to modify a string literal causes undefined behavior. In general you should always find out how the language you are using treats the objects denoted by literals before attempting to modify them or risk similar bugs.