After my previous post I continued to dig deeper into the Emacs skeleton system. Not all of it was beneficial for my sanity, so strap in and let me explain some more cursed parts of it!
Just as before, all the examples should be ready to evaluate within
Emacs. Consider reading this post in eww so that C-x C-e
is
readily available.
As explained in the previous post, within a skeleton str
represents
the value taken from the skeleton interactor, usually an
interactive prompt. A quick example:
(with-temp-buffer
(skeleton-insert
'("Name: "
"Hello, my name is " str ". Nice to meet you!"))
(buffer-string))
It can be used more than once and seemingly even as an argument of a function (because why not!).
(with-temp-buffer
(skeleton-insert
'("Name: "
"Hello, my name is " str ". Nice to meet you! "
"Yours truly, " (upcase str) "."))
(buffer-string))
As it turns out, this isn’t entirely true. Let’s try removing the
first mention of str
from the previous example.
(with-temp-buffer
(skeleton-insert
'("Name: "
"Hello, nice to meet you! "
"Yours truly, " (upcase str) "."))
(buffer-string))
This is where the cursed nature of str
shows its face. Once str
is set (i.e. it’s been used at least once already within that
skeleton), it’s effectively a regular variable1. Before that, it
expands to roughly the following list:
(setq str (skeleton-read "Name: "))
Let me stress that this is a list, not code that gets evaluated.
Or rather not yet. This utilizes the fact that when skeleton receives
a list as its element, it evaluates it2 and uses the result as the
actual skeleton element. If str
is used as a top-level element of
a skeleton, this behavior makes it work. But if it’s inside
a function call, the first occurrence of str
won’t work as expected
as it’s literally just this unevaluated list.
As a result, in our example upcase
gets passed a list, not a string.
How do we fix it? We eval it ourselves!
(with-temp-buffer
(skeleton-insert
'("Name: "
"Hello, nice to meet you! "
"Yours truly, " (upcase (eval str)) "."))
(buffer-string))
This can be also written like this, so all the str
can be freely
reordered without worrying about the special case of the first one:
(with-temp-buffer
(skeleton-insert
'("Name: "
'(eval str)
"Hello, nice to meet you! "
"Yours truly, " (upcase str) "."))
(buffer-string))
This last skeleton evaluates the form that sets str
for all its
future references, and discards the result.
I’m still processing how cursed this is but I’m glad I have finally
understood the weird str
semantics I already encountered before.
Thank you for joining me on this short journey!