A few days ago I was writing a pretty complex Elisp function which didn’t work correctly. My usual tool to use in such cases if of course Edebug – but in this case, it didn’t help much. One of the things my function did was messing around with buffers and windows, and this interferes with Edebug insisting on showing the source code in a specific window. In such cases, I miss the old-school “printf debugging”. In Elisp, you can say (message “...”)
anywhere in your code – in fact, message
accepts more arguments and works a bit like format – but it still is not the most useful thing. The echo area is small, and when you have more messages, you need to go to the *Messages*
buffer or use tricks like my show-some-last-messages command.
I figured that it would be very useful if I could display something (like with message
) but then wait for a keypress, so that I’d have enough time to read what was displayed. Well, a bit like what alert
does in a browser.
Well, easy enough.
;; Note: a better version of this function comes later in the post (defun alert (&rest message-args) "Like `message', but waiting for a keypress." (read-key (concat (apply #'format message-args) "\n[press any key to continue]")))
Believe it or not, but this made my life so much nicer!
After using this for a while, I noticed a pattern: my most often usage of alert
was like this.
(alert "var1: %s, var2: %s" var1 var2)
So, why not create a proper abstraction for that, that is, a function which would accept a list of symbols and use alert
to show their names and values?
(defvar alert-vars-format "%s: %s") ;; Note: a better version of this comes later in the post (defun alert-vars (&rest vars) "Use `alert' to show the values of VARS. VARS should be a list of symbols." (funcall #'alert "%s" (mapconcat (lambda (var) (format alert-vars-format (symbol-name var) (symbol-value var))) vars ", ")))
(Of course, that defvar
should really be a defcustom – again, if this were a proper package, I would definitely do it that way. One day I’ll learn how to publish my code on Melpa…)
Now I can say (alert-vars 'var1 'var2)
and see my variables alert
ed to me. How cool is that?
I started using that and almost immediately ran into a problem. Can you see it? It turns out that symbol-value only works for dynamic variables, and I was using lexical bindings in my code. Apparently there is no function similar to symbol-value
for lexical bindings (and I think there can’t be because of the nature of lexical scope), so the situation is hopeless, right?
Of course not.
After a short while I realized that the solution to my problem is to make alert-vars
a macro and not a function. Of course, macros are more difficult to write than functions, and honestly, I’m not 100% sure my macro doesn’t have some well-hidden bug (and I’m almost 100% sure it could be written in a more elegant way), but it seems to work for me.
(defmacro alert-vars (&rest vars) "Use `alert' to show the values of VARS. VARS should be a list of symbols." `(alert ,(mapconcat (lambda (var) alert-vars-format) vars ", ") ,@(reverse (seq-reduce (lambda (acc var) (cons var (cons (symbol-name var) acc))) vars '()))))
The mapconcat
is a simple way to construct a format string consisting of the correct number of instances of alert-vars-format
joined by comma-space separators. Then I used seq-reduce
to create the suitable list out of vars
(which is a list of symbols). Of course, mapcar
wouldn’t have worked – I need to generate a list which is twice as long as vars
, and I considered using flatten-list
to be less elegant than a seq-reduce
. The double cons
is a bit ugly, but lets me construct the list one step at a time in an efficient way, that is, adding to the beginning and not the end – hence the need for reverse
.
I am pretty sure most (or all) of the magic could have been done by two (or maybe even one, who knows) applications of cl-loop
. Well, maybe I’ll learn cl-loop
one day.
Anyway, this is how you use the alert-vars
macro:
(alert-vars major-mode default-directory)
(Since it is a macro, you don’t need to quote anything – it takes care of things like that itself.) And now you can use it inside a lexically binding let
, too!
Once I started using this macro, I noticed one more issue with it. It turns out (unsurprisingly) that read-key
accepts C-g
and just returns 7 (which is its ASCII code). I would prefer, however, if C-g
interrupted the function I’m alert
-debugging. One way to do that is to use the higher-level read-char
function. This has a (minor) drawback that it requires the user to type an actual character on the keyboard, and clicking a mouse button (or even pressing a function key) causes an error. Hence I decided to use read-key
and just implement quitting myself.
(defun alert (&rest message-args) "Like `message', but waiting for a keypress." (when (eq (read-key (concat (apply #'format message-args) "\n[press C-g to quit or any other key to continue]")) ?\C-g) (keyboard-quit)))
One last drawback I can see is that alert
has no meaningful return value. Currently I cannot imagine a scenario where a return value would be useful, but last week I encountered a situation when I used the return value from message
, so you never know. For now I decided that I’m going to apply the YAGNI principle and leave it as it is.