macroscommon-lispcl-who

Undefined function after macroexpansion


I'm studying Common Lisp and want to play with lisp and web development. My current problem comes from a simple idea to iterate over all javascript files i want to include. I use SBCL and Quicklisp for fast startup. The problem could be related to the cl-who package I'm using.

So I've declared my package and started like this:

(defpackage :0xcb0
  (:use :cl :cl-who :hunchentoot :parenscript))
(in-package :0xcb0)

To keep it simple I reduced my problem function. So I have this page function:

(defun page (test)
  (with-html-output-to-string
    (*standard-output* nil :prologue nil :indent t)
    (:script
     (:script :type "text/javascript" :href test))))

This will produce the desired output

*(0xcb0::page "foo")

<script>
   <script type='text/javascript' href='foo'></script>
</script>

Now my I've created a macro which produces :script tags.

(defmacro js-source-file (filename)
  `(:script :type "text/javascript" :href ,filename)))

This works as expected:

*(macroexpand-1 '(0XCB0::js-source-file "foo"))

(:SCRIPT :TYPE "text/javascript" :HREF "foo")

However if I include this into my page function:

(defun page (test)
  (with-html-output-to-string
    (*standard-output* nil :prologue nil :indent t)
    (:script
     (js-source-file "foo"))))

...it will give me a style warning (undefined function: :SCRIPT) when defining the new page function. Also, the page function produces this error when executed:

*(0xcb0::page "foo")

The function :SCRIPT is undefined.
   [Condition of type UNDEFINED-FUNCTION]

Why does the embedded macro js-source-file works as expected, in that it produces the desired output, but fails when is called within another function?

P.S. I know the topic of macros in Lisp can be quite exhausting for a beginner like me. But currently I can't wrap my head around the fact that this should work but doesn't!


Solution

  • The problem is that macros are expanded a bit counter intuitively in order from outmost to inmost. For example:

    (defmacro foobar (quux)
      (format t "Foo: ~s~%" quux))
    
    (defmacro do-twice (form)
      `(progn
         ,form
         ,form))
    
    (foobar (do-twice (format t "qwerty")))
    

    The output will be

    Foo: (DO-TWICE (FORMAT T "qwerty"))
    

    foobar never sees the expansion of do-twice. You could avoid the problem by calling macroexpand yourself in foobar:

    (defmacro foobar (quux)
      (format t "Foo: ~s~%" (macroexpand quux)))
    
    (foobar (do-twice (format t "qwerty")))
    ; => Foo: (PROGN (FORMAT T "qwerty") (FORMAT T "qwerty"))
    

    Since you're using a third party macro, that probably isn't a good solution. I think the best option is to generate the markup yourself in js-source-file. I'm not familiar with cl-who, but this seemed to work in my quick test:

    (defun js-source-file (filename stream)
      (with-html-output (stream nil :prologue nil :indent t)
        (:script :type "text/javascript" :href filename))))
    
    (defun page (test)
      (with-output-to-string (str)
        (with-html-output (str nil :prologue nil :indent t)
          (:script
           (js-source-file test str)))))