Endless Parentheses

Ramblings on productivity and technical subjects.

profile for Malabarba on Stack Exchange

What's a defconst and why you should use it

Any Emacs package developer worth their salt knows the difference between a defvar and defcustom. These two comprise the vast majority of variable definitions in Elisp code, but there's a third child, the defconst. While regular variables and customizable variables only really differ when it comes to Emacs' customize system, constants differ in loading logic in a subtle but important way.

When a defvar is evaluated, the value specified is assigned to the variable only if the previous value was void (if the variable wasn't defined yet). Evaluating a defconst, on the other hand, always sets the symbol's value to the (new) given value. This becomes most relevant when a package is upgraded after having been loaded in Emacs. Any variables defined by the package will retain their old values until Emacs is restarted, whereas constants will be updated to their new values.

In a sense, somewhat ironically, Elisp constants are the least constant of all variables. Rest assured though, they were given this name for a reason. This behaviour is indeed suited for storing values that should be constant. Here's a simple use-case which arose for us in sx.el.

The StackExchange API requires that we provide filters to specify which information we want to receive. Of course, we were storing these filters in variables and then using that information while printing the question.

(defvar sx-browse-filter
  '(question.title question.owner))

(defun sx-question--print-info (question-data)
  (let-alist question-data
    .title "\nAsked by:" .owner))

The problem with this approach would only be seen after an upgrade. Lets say we decide to also display a question's score. Without a doubt, we'd change the above to something like this.

(defvar sx-browse-filter
  '(question.score question.title question.owner))

(defun sx-question--print-info (question-data)
  (let-alist question-data
    (number-to-string .score) " "
    .title "\nAsked by:" .owner))

Now the user upgrades sx.el, tries to run it again, and gets hit on the face with an error!

sx-browse-filter is still using the old value, so the question score is not returned by the API. That means .score is nil and number-to-string throws an error. Changing the defvar to defconst was all we needed to prevent this problem.

If you're wondering why you've never run into such an issue, that's because package.el used to have another bug, which would never load new package versions upon upgrade. That has been fixed in 25.1—packages are now reloaded on upgrade—so you should ensure your package won't run into new problems by making proper use of constants.

Simply put, you should defconst whenever there are functions or macros in a package which rely on the variable having that specific value (kind of like a proper constant). This way you'll know you never have old values hanging around.

comments powered by Disqus