Or see the list of project sponsors.
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!
@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.