RSS Feed

Lisp Project of the Day

macrodynamics

You can support this project by donating at:

Donate using PatreonDonate using Liberapay

Or see the list of project sponsors.

macrodynamicsmacro

Documentation🤨
Docstrings🥺
Tests 😀
Examples😀
RepositoryActivity🥺
CI 🥺

Found a useful library for writing macro and using dynamic variables during expansion. I didn't try to figure out how does it work (yet) but decided to make a more visual example than the code from library's README.

Let's pretend we tried to write the code like this, manually placing h1, h2, h3, for headers:

POFTHEDAY> (spinneret:with-html-string
             (:h1 "Hello")
             (:h2 "World")
             (:p "This is an example"))
"<h1>Hello</h1>
 <h2>World</h2>
 <p>This is an example"

I'd like to write more semantic code separated into the sections:

POFTHEDAY> (spinneret:with-html-string
             (section "Hello"
               (section "World"
                 (:p "This is an example"))))
"<h1>Hello</h1>
 <h2>World</h2>
 <p>This is an example"

First, let's try to implement the section macro using Lisp's dynamic variable:

POFTHEDAY> (defmacro section (title &body body)
             (let* ((*level* (1+ *level*))
                    (tag-name (format nil "H~A" *level*))
                    (tag (alexandria:make-keyword tag-name)))
               `(spinneret:with-html
                  (,tag ,title)
                  ,@body)))

POFTHEDAY> (spinneret:with-html-string
             (section "Hello"
               (section "World"
                 (:p "This is an example"))))
"<h1>Hello</h1>
 <h1>World</h1>
 <p>This is an example"

This does not work.

Why did this happened? Let's add some logging to our macro function:

POFTHEDAY> (defmacro section (title &body body)
             (let* ((*level* (1+ *level*))
                    (tag-name (format nil "H~A" *level*))
                    (tag (alexandria:make-keyword tag-name)))
               (format t "Expanding section with level=~A~%"
                       *level*)
               `(spinneret:with-html
                  (,tag ,title)
                  ,@body)))

POFTHEDAY> (spinneret:with-html-string
             (section "Hello"
               (section "World"
                 (:p "This is an example"))))
Expanding section with level=1
Expanding section with level=1
Expanding section with level=1
Expanding section with level=1
Expanding section with level=1
Expanding section with level=1
"<h1>Hello</h1>
 <h1>World</h1>
 <p>This is an example"

Don't know why does it output 6 times instead of 2. But definitely, there is something wrong with our dynamic variable's value, isn't it?

Now let's use macrodynamics to define the variable, macro and the binding:

POFTHEDAY> (macrodynamics:def-dynenv-var *level* 0)

POFTHEDAY> (macrodynamics:def-dynenv-macro section (title &body body)
             (macrodynamics:ct-let ((*level* (1+ *level*)))
               (let* ((tag-name (format nil "H~A" *level*))
                      (tag (alexandria:make-keyword tag-name)))
                 (format t "Expanding section with level=~A~%"
                         *level*)
                 `(spinneret:with-html
                    (,tag ,title)
                    ,@body))))

POFTHEDAY> (spinneret:with-html-string
             (section "Hello"
               (section "World"
                 (:p "This is an example"))))
Expanding section with level=1
Expanding section with level=1
Expanding section with level=1
Expanding section with level=2
Expanding section with level=2
"<h1>Hello</h1>
 <h2>World</h2>
 <p>This is an example"

Wonderful! Now it works!

Update

@PuercoPop asked on Twitter if macrolet will be enough in the above example. I've tested this hypothesis and no, it is not suitable replacement for macrodynamics:

POFTHEDAY> (let ((level 0))
             (macrolet ((section (title &body body)
                         (let* ((level (1+ level))
                                (tag-name (format nil "H~A" level))
                                (tag (alexandria:make-keyword tag-name)))
                           (format t "Expanding section with level=~A~%"
                                   level)
                           `(spinneret:with-html
                              (,tag ,title)
                              ,@body))))
                (spinneret:with-html-string
                  (section "Hello"
                    (section "World"
                      (:p "This is an example"))))))

;; It ends with this error:

Execution of a form compiled with errors.
Form:
  (SECTION "Hello"
  (SECTION "World"
    (SPINNERET::WITH-TAG (:P)
      "This is an example")))
Compile-time error:
  during macroexpansion of
(SECTION "Hello"
  (SECTION "World"
    #)).
Use *BREAK-ON-SIGNALS* to intercept.

 The variable LEVEL is unbound.
 It is a local variable not available at compile-time.

Brought to you by 40Ants under Creative Commons License