Preface: I don’t know how long it takes to learn ELISP

Chapter 1: Library

The previous chapter implemented newbie.el, a library that defines only one function. In fact, this function could be defined as a macro rather than a function, and would make the calling code execute marginally more efficiently. Because calling a function is like going to the bus stop, and calling a macro is like riding in your own car. It’s not a very accurate metaphor, so it’s just a metaphor.

Define the macro

Let’s define a macro that doesn’t do anything,

(defmacro foo ())

Formally, defining a macro looks like defining a function, except defun has been replaced with defmacro.

Calling a macro is also similar to calling a function, such as calling foo, the macro defined above that does nothing.

(foo)

For this macro call, Elisp evaluates to nil. Why is it nil? Because the ELISP interpreter encounters a macro call statement, it replaces it with the definition of the macro, which is the expansion of the macro. The above (foo) statement will be replaced with

It’s nothing. Nothing, just nil.

If you want the definition of foo to be something, for example

(defmacro foo ()
  t)

The result of the expansion of the macro call statement is T.

Macros can also have arguments like functions, for example

(defmacro foo (x)
  x)

The macro call (foo “Hello world!”) The result is “Hello World!” .

Constructing a program as if it were data

The definition of macros illustrates an important feature of Lisp: the ability to construct programs as if they were data. For example,

(defmacro foo ()
  (list '+ 1 2 3))

The Elisp interpreter evaluates the expression in the macro definition. (list ‘+ 1, 2, 3) (list ‘+ 1, 2, 3) Thus, the macro call statement (foo) is expanded by the Elisp interpreter to (+ 1 2 3), and the Elisp interpreter continues to evaluate the result of the macro expansion, so (foo) evaluates to 6. Using the Elisp interpreter’s macro definition and call handling, you can construct a program in the same way as you construct data in a program.

Since (list ‘+ 1 2 3) is nearly equivalent to ‘(+ 1 2 3), the above macro definition can be simplified to

(defmacro foo ()
  '(+ 1 2 3))

When using a quote constructor in a macro definition, note that quotes shield the Elisp interpreter from processing parameters. For example,

(defmacro foo (x y z)
  '(+ x y z))

The definition of this macro is legal, but it is called as follows

(foo 1 2 3)

It’s not going to be expanded as (+ 1, 2, 3), it’s going to be expanded as (+ x, y, z). Because when Elisp evaluates the macro definition, he thinks that ‘(+ x, y, z) in the macro definition is just a literal list, where x, y, and z are not the macro’s parameter values. Therefore, in the definition of macros, you need to be clear about which are literal data and which are variables or function calls. For the above example, we need to use list back, i.e

(defmacro foo (x y z)
  (list '+ x y z))

Thus, (foo 1, 2, 3) will be expanded as

(+ 1 2 3)

The quotation marks

Macro definition

(defmacro foo (x y z)
  (list '+ x y z))

with

(defmacro foo (x y z)
  `(+ ,x ,y ,z))

Synonymous.

Quotes’ can make a list of whole literal sense of the list, but the quotes (usually with ~ in the same rows) on the keyboard can also make a list of the literal sense of the list, but if the front by the modified symbols, such as the parameters of the macro, Elisp interpreter will no longer see it as a literal symbols.

In a backquoted list,@ promotes an element from a list to an outer list, for example

`(1 ,@(list 2 3) 4)

and

` (1, @ '(2, 3) 4)

As well as

` (1, @ ` (2, 3) 4)

Is (1, 2, 3, 4).

Using these strange symbols, it is much easier to construct programs like constructs in macro definitions.

Princ \ n macros

The following code defines the macro

(defmacro princ\n (x)
  `(progn
     (princ ,x)
     (princ "\n")))

Instead of printc \’ in newbie.el, for example

(princ\n "Hello world!" )

I will always be using the printc \n macro.

Variable to capture

Sometimes you need to use local variables in a macro definition. For example,

(defmacro bar (x y a)
  `(let (z)
     (if (< ,x ,y)
         (setq z ,x)
       (setq z ,y))
     (+ ,a z)))

This macro adds the smaller of its arguments x and y to a. For example,

(bar 2 3 1)

And the answer is 3.

If the call to bar occurs in coincidental circumstances, for example

(let ((z 1))
  (bar 2 3 z))

It evaluates to 4, not 3. This unexpected result occurs because the above macro call statement is expanded to

(let ((z 1))
  (let (z)
    (if (< 2 3)
        (setq z 2)
      (setq z 3))
    (+ z z)))

This expansion occurs because the Elisp interpreter does not evaluate the macro parameters, but instead passes them into the macro’s definition and replaces them with the macro’s parameters. The third argument to (bar 2 3 z) is z. After the Elisp interpreter passes this argument into the definition of bar, the parameter a is changed to z, but the definition of bar has a local variable z. In the final (+ z z) expression, The first z should have been the parameter I passed to BAR, but the ELISP interpreter in this case would have considered it to be a local variable of BAR, and the result would not have been what I expected.

Health macro

Macros that ensure that local variables in a macro definition are not confused with external variables in the macro expansion environment are called sanitary macros. Elisp’s macros are unhygienic. The Scheme language, also a Lisp dialect, provides hygiene macros. In recent years, the emerging Rust language has also supported health macros. However, ELISP can simulate health macros using Uninterned symbols.

The Elisp interpreter maintains tables of symbols that are bound to data, functions, or macros during the execution of the program’s interpretation. Symbols that appear in these tables are Interned. Symbols that do not appear in this table are Interned. Use the Elisp function make-symbol to create symbols outside the system. For example,

(setq z 3)
(setq other-z (make-symbol "z"))

The Z in the first expression is the symbol bound to the number 3, which is institutional, and the symbol that make-symbol creates is also called Z, but it’s external, so I use an institutional symbol other -Z to bind this external symbol, which is also called Z. Using the exoteric Z symbol of this other-z binding, we can make the macro bar defined in the previous section hygienic, i.e

(defmacro bar (x y a) (let ((other-z (make-symbol "z"))) `(progn (if (< ,x ,y) (setq ,other-z ,x) (setq ,other-z ,y)) (+  ,a ,other-z))))

The new definition of BAR is no longer afraid of variable capture. Try it.

(let ((other-z 1))
  (bar 2 3 other-z))

In the above call to bar, although the third argument has the same name as the local variable Other -z in the bar definition, no variable capture will occur, so the value of the above code is 3.

How does the redefined BAR avoid variable capture? To understand all this, you need to have a deep understanding of how Elisp evaluates the definition of macros. First, the Elisp interpreter evaluates any expression in the macro definition. If you want to prevent it from evaluating an expression, you need to use quotes. Expressions that are enclosed in quotes are treated as constants by the Elisp interpreter. However, by using back quotes and commas, you can open up some variation in expressions that Elisp considers constant. The latter is the key to the redefinition of BAR to avoid variable capture, because Elisp does not evaluate the constant part of the macro definition, but evaluates the variable parts of the constant. This is equivalent to leaving a section of code “at rest” in a macro definition, where parts of the code can be modified by the Elisp interpreter to the desired effect.

The statement in the definition of bar that would have occurred in the original capture of variables is

(+ ,a ,other-z)

Since the other-z is already bound to an external symbol z at the beginning of the let expression, the Elisp interpreter evaluates the macro definition by assuming that all the other-z is (or evaluates to) the external symbol z, That is, by the time the BAR call statement is expanded by ELISP, the symbol Other-Z is no longer Other-Z, but the exoteric Z. In the definition of bar, there is no chance that the local variable Other-z will be confused with the external variable of the same name. This is how the ELISP language constructs hygiene macros.

In fact, in the above definition of bar, there is no need for me to use Other-z at all. I could have defined bar as follows:

(defmacro bar (x y a)
  (let ((z (make-symbol "z")))
    `(progn
       (if (< ,x ,y)
           (setq ,z ,x)
         (setq ,z ,y))
       (+ ,a ,z))))

In the let expression of the above code, the in-system symbol z is bound to the out-of-system symbol z, and then in subsequent code,z is evaluated by the ELISP interpreter as the out-of-system symbol z. In this way, the macro calls the following statement

(let ((z 1))
  (bar 2 3 z))

The result was as expected, 3.

Those outside the system are conducive to health construction.

conclusion

This chapter has only scratched the surface of Elisp macros; their real use is in defining a new syntax for the Elisp language (often called metaprogramming), not in defining something like princ\n that you can easily do with functions.

Next chapter: Dynamic Modules