An important difference between intern and make-symbol in GNU Emacs ELisp

September 14, 2023

Suppose, not hypothetically, that for some reason you're trying to create a GNU Emacs ELisp macro that defines a function, and for your sins you don't want to directly specify the name of your new function. In my case, I want to create a bunch of functions with names of the form 'mh-visit-<something>', which all use mh-visit-folder to visit the (N)MH folder '+inbox/<something>'. Ordinary people using macros to create functions probably give the macro the full name of the new function, but here it's less annoying if I can put it together in the macro.

(I will put the summary here: unless you know what you're doing, use intern instead of make-symbol in situations where you're making up symbols, like on the fly function definitions.)

If we passed in the name of our new function to be, I believe the resulting macro would be (without niceties like docstrings):

(defmacro make-visit-func (fname folder)
  `(defalias ',fname
     (lambda ()
       (mh-visit-folder ,(format "+inbox/%s" folder)))))

;; usage:
(make-visit-func mh-visit-fred "fred")

(You might think we would use defun, but defun is actually a macro itself and defalias is the direct thing, as I understand it; see Defining functions.)

The ` and , bits are Backquoting, which lets us create a quoted list of the code the macro will generate while splicing in some variables. The peculiar ',fname is (E)Lisp for the value of 'fname' spliced in to the list but quoted instead of evaluated; 'fname' itself is not a string but a symbol (also), which we have here because symbols are how functions get their (global) names.

(We don't have to quote 'mh-visit-fred' when we use the macro because macro arguments are effectively automatically quoted, by virtue of being unevaluated.)

If we're going to use the folder name to create both the full folder and the name of our new function, we need to turn a string into a symbol. If you quickly scan the documentation on creating symbols, there are two plausible ELisp functions to use, intern and make-symbol. The latter's name certainly sounds like what we want, and if you aren't an ELisp expert, you might rewrite our make-visit-func to use it, like so:

;; This doesn't actually work, don't copy it.
(defmacro make-visit-func (folder)
  `(defalias ',(make-symbol (format "mh-visit-%s" folder))
     (lambda ()
       (mh-visit-folder ,(format "+inbox/%s" folder)))))

;; usage:
(make-visit-func "fred")

If you do this, you will spend some time being rather confused. Your macro will execute without errors and debugging approaches like macroexpand will give you a result that works when evaluated. If you change to using intern, this will work, so you actually want:

;; This does actually work, you can copy it.
(defmacro make-visit-func (folder)
  `(defalias ',(intern (format "mh-visit-%s" folder))
     (lambda ()
       (mh-visit-folder ,(format "+inbox/%s" folder)))))

What I believe is happening is caused by the following innocent seeming bit in the description of make-symbol, emphasis mine:

This function returns a newly-allocated, uninterned symbol whose name is name (which must be a string).

Normally, the symbols that serve as the name of functions are interned, making them both visible for later use and kept around by ELisp garbage collection. However, what make-symbol gives us is an unreferenced symbol object (with a name assigned to it), so when defalias connects our lambda to it to make it a function, it is still not visible or retained. As a result it disappears into a puff of smoke afterward; even if it hasn't been literally garbage collected, there is no reference to it.

Evaluating the result of macroexpand worked, because it was the (more or less) list result and when evaluated back created a normal, interned symbol. Here it is:

(defalias 'mh-visit-fred
  (lambda nil
    (interactive) (mh-visit-folder "+inbox/fred")))

(I've split this on three lines for readability.)

Although 'mh-visit-fred' came from 'make-symbol', it is printed as text (well, its name is), and when read back to evaluate, it will be a normal interned string. This is one of the cases where the printed representation is not actually the read syntax for this value. I believe that the actual neurotically correct print syntax for 'make-symbol's result is '#:'. Although I believe Emacs accepts this as read syntax, I don't know if current versions ever print this on output unless you try very hard.

(Having gone through this once, I'm writing it down so that if I ever do this sort of thing again I won't have to re-learn this.)

PS: Since I am not an Emacs Lisp expert, it's quite possible that there are better ways to do all of this macro.

Comments on this page:

From at 2023-09-16 22:09:45:

Improve printing with:

(setq print-gensym t
      print-circle t)
Written on 14 September 2023.
« A user program doing intense IO can manifest as high system CPU time
Insuring that my URL server and client programs exit after problems »

Page tools: View Source, View Normal, Add Comment.
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Thu Sep 14 23:05:13 2023
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.