Defining new macros

Disclaimer: A general rule of thumb is to try and not define new macros if they are not needed, or if they do not serve a specific purpose, since, while they do make the code more readable in some cases, they also make it way harder to debug the code.

Syntax

CL-USER> (defmacro name-of-macro (parameter list) 
	       "Documentation"
	       (body of lisp expressions)) 

Macros are used to extend the syntax of standard LISP by generating/transforming code and adding new notations. One important thing to note is that the computation of the body is already done at compile time and that it is possible to even quote LISP expressions, which enables us to generate code through macro-expansion, which happens during compile-time. For example:

CL-USER> (defmacro forty-two () (* 2 21)) 
CL-USER> 42

Calling (forty-two) now is equivalent to using 42.

CL-USER> (defmacro forty-two () '(* 2 21)) 
CL-USER> (* 2 21)

Now (forty-two) is equivalent to (* 2 21), which is evaluated to 42 during runtime.

Parameter List

Similarly to functions, you can use &optional, &key, and &rest in parameter lists for macros, but additionally you can also use &body instead of &rest. This not only helps human readers, but also some development environments to interpret how the macro is intended to be used.

Backquote/Comma

To make it easier to read/write complex expressions, we use backquotes (`) and commas (,) as follows:

CL-USER> (defmacro measure-time (type &body body)
            "Macro to measure the time an action takes to finish, returns the returnvalue of the body"
            `(let ((start (get-universal-time))
                   (value (progn ,@body)))
               ,(ccase type
                  (:start  '(calculate-time start *total-time*))
                  (:init '(calculate-time start *total-init-time*))
                  (:movement  '(calculate-time start *total-movement-time*))
                  (:manipulation  '(calculate-time start *total-manipulation-time*))
                  (:detection  '(calculate-time start *total-detection-time*)))
               value))

The backquote (`) signals the start of an expression to LISP; inside of that expression, the comma (,) signals which subexpressions should be evaluated, while all other subexpressions are treated as if they are quoted ('). Note that in this example, we want the function calculate-time to be quoted in our code, since we told the macro to evaluate ccase during compile-time but want to evaluate calculate-time during runtime.

Splicing

If we need to remove the outermost parenthesis, for example, to put the code contained into a (progn …) block, then we can use ,@ to splice the list. You can find an example of how to use this in line 4 of the measure-time macro.

Example of an CRAM-macro

CL-USER> (defmacro with-simulated-robot (&body body)
          `(let ((results
                   (proj:with-projection-environment urdf-bullet-projection-environment
                     (cpl-impl::named-top-level (:name :top-level)
                       ,@body))))
             (car (cram-projection::projection-environment-result-result results))))

When analyzing this macro the first thing we can notice is that we only have (&body body) as our parameter list. This means, that this macro will be used as a prefix to code that we want to run on our robot. For example:

CL-USER> (with-simulated-robot
            code that does awesome stuff)

Here, “body” would now contain “(code that does awesome stuff)”. So now let's go through what the macro actually does. The first thing that is done, is to signal to LISP that a backquoted expression is about to start. Here, a variable “results” is created, which contains two nested, quoted functions/macros, as well as our spliced, evaluated body. The code contained by our variable “result” will look roughly like this:

'(proj:with-projection-environment ... 
   '(cpl-impl::named-top-level ...
      evaluation of our code that does awesome stuff))

Those two functions/macros will only be evaluated at runtime, as we can see from the quotes ('). After defining result, let's see roughly how the body of our macro will expand. Since non of the expressions have a comma (,) in front of them, every function/macro will be treated as if they were quoted ('):

'(car '(cram-projection::projection-environment-result-result ...))

To conclude: without any prior knowledge of what the other functions/macros are doing we can tell, that this macro will try to evaluate the body as far as it can during macro expansion. The results will be passed onto other functions/macros, which are all evaluated during runtime, which makes sense, given that the macro name is “with-simulated-robot”, which gives the impression that the code will be run while a robot is being simulated, which can obviously only happen during runtime, and not during macro expansion.