Common Lisp Macros

Sven-Olof Nyström
Advanced functional programming, fall -10
Information technology
Uppsala University

Sequential execution.

;* Preliminaries

Most Common Lisp control constructs (for example: defun, cond, let, labels) allow more than one expression in the body. These are then executed in sequence, and the result of the final one is returned.

Example

(defun hello()
  (print "hello")
  (print "how")
  (print "are you?"))

CL-USER> (hello)

"hello"
"how"
"are you?"
"are you?"
CL-USER>

Dolist

A convenient contruct for iteration

CL-USER> (dolist (i '(a b c))
           (print i))

A
B
C
NIL
CL-USER>

Dotimes

Another convenient contruct for iteration

CL-USER> (dotimes (i 3)
           (print i))

0
1
2
NIL
CL-USER>

Rest function argument

(defun sum (&rest l)
  (reduce #'+ l :initial-value 0))

CL-USER> (sum 1 2 3 4 5)
15

Keyword function argument

(defun seq (&key start end step)
  (list start end step))

(seq :start 10 :end 1 :step -1)

If you have trouble remembering the order of the arguments in your function, or want a function with an arbitrary number of arguments

Support for writing macros, gensym

Gensym creates an uninterned symbol. Looks strange when printed, since it is not interned and belongs to no package.

CL-USER> (gensym)
#:G844
CL-USER> (gensym "X")
#:X845

Backquote

Macros build Lisp programs. Since the resulting expressions may be complex, Common Lisp has a special syntax for building complex nested list structures.

`(,x b)
might expand to
(list x 'b)
and evaluate to
(42 b)
if x is 42.

Generally speaking, anything preceeded by a comma (,)is evaluated as a Lisp expression and the value inserted.

Anything else is inserted as it is.

Backquote, examples

`(a (+ 1 2) c)
`(a ,(+ 1 2) c)
`(a (list 1 2) c)
`(a ,(list 1 2) c)
`(a ,@(list 1 2) c)

Expressions preceeded by ,@ must evaluate to a list. The elements of the list are inserted.

Macro evaluation

Basic idea: The macro is evaluated (without first evaluating the arguments). Then the result of the macro evaluation is evaluated as a Lisp expression.

When does the first evaluation (macroexpansion) occur?

This means that we do not have access to run-time information when expanding the macros. No point in trying to evaluate expressions etc.

Macros, One complication

SBCL compiles all functions in one file at one go. Suppose a file contains

Now, the macro cannot use the function before it has been compiled. Thus, the use of the macro cannot be compiled...

Possible solutions

  1. Put macros (with help functions) and their uses in different files and make sure that the macro definitions are compiled before their uses (manually or using asdf).
  2. Define help functions as local functions using labels.

Examples.

Now, let's look at some examples

Common Lisp already defines an if, but let's define one anyway:

(defmacro my-if (condition then else)
  `(cond (,condition ,then)
         (t ,else)))

(defun fak (n)
  (my-if (= n 0) 1
         (* n (fak (- n 1)))))


A test run and closer examination

CL-USER> (fak 10)
3628800

CL-USER> (macroexpand-1 '(my-if (= n 0) 1
                          (* n (fak (- n 1)))))
(COND
  ((= N 0) 1)
  (T (* N (FAK (- N 1)))))
T

Next, try macroexpand, which repeats macroexpansion.

Sequential execution

(defmacro seq (&body body)
  `(let ()
     ,@body))

CL-USER> (seq (print "foo") (print "bar") 32)

"foo"
"bar"
32

A simple iteration

(do-interval (x 0 10)
   (print x))

CL-USER> (do-interval (x 0 3) (print x))

0
1
2

Simple iteration, solution

I need to introduce temporaries (use gensym)

General looping: Use block + loop + return-from

(block foo
  (loop
    ...
    (when ... (return-from foo value))
    ...))

We could also use labels.

Simple iteration, code.

(defmacro do-interval (var-a-b &body body)
  (let ((blck (gensym "BLOCK"))
        (a1 (gensym "A"))
        (b1 (gensym "B"))
        (var (car var-a-b)))
    `(let* ((,a1 ,(cadr var-a-b))
            (,b1 ,(caddr var-a-b))
            (,var ,a1))
       (block ,blck
         (loop
            (unless (< ,var ,b1)
              (return-from ,blck nil))
            ,@body
            (incf ,var))))))

A shorthand

destructuring-bind simulates Common Lisp's argument passing.

Example:

(destructuring-bind (a b c) '(1 2 3)
    (format t "[~w ~w ~w]" a b c))
Nice, but too long!

Introduce a shorthand!

(defmacro dbind (alist value &rest body)
  `(destructuring-bind ,alist ,value ,@body))

Destructuring case

First, lisps ecase.

Like switch of other languages..

(defun foo (x)
  (ecase x
    (42 "fortytwo")
    (foo "bar")))

CL-USER> (foo 42)
"fortytwo"
CL-USER> (foo 'foo)
"bar"
CL-USER> (foo 'something-else)
; Evaluation aborted.

Desructuring case (cont)

I like the pattern matching of Erlang and SML.

Wouldn't it be cool to be able to write something like this?

(defun foo (x)
  (dcase x
    ((leaf data)
     (format t "A leaf with data ~a~%" data))
    ((node left right)
     (format t "A node with left ~a and right ~a~%" left right))))

Destructuring case (cont)

(defmacro dcase (index &rest clause-list)
  (let ((var (gensym)))
    (labels
	((translate-clause (clause)
	   (let ((arg-list (cdar clause)))
	     `((,(caar clause))
	       (destructuring-bind ,arg-list (cdr ,var)
		 ,@(cdr clause))))))
      `(let ((,var ,index))
	(ecase (car ,var)
	  ,@(map 'list #'translate-clause clause-list))))))

Iterating over vectors

Why dolist but no do-vector?

(defmacro do-vector (var-seq &body body)
  (destructuring-bind (var seq) var-seq
    (let ((vector (gensym "VECTOR"))
	  (index (gensym "INDEX")))
      `(let ((,vector ,seq))
	(dotimes (,index (length ,vector))
	  (let ((,var (aref ,vector ,index)))
	    ,@body))))))

Try

(do-vector (x "foo bar") (print x))

Defmap

It's is sometimes useful to put literal data into a table.

(defmacro defmap (name options &rest entries)
  (declare (ignore options))
  (let ((table (make-hash-table)))
    (dolist (entry entries)
      (setf (gethash (car entry) table) (cadr entry)))
    `(defvar ,name ',table)))

; Example

(defmap *a* ()
  (azure 42)
  (bar 99)
  (clojure 123)
  (data 987)
  (email 1009))

do-hash-table

But iterating over a hash table is tiresome...

(defmacro do-hash-table (key-value-ht &rest body)
  (destructuring-bind (key value ht) key-value-ht
    (let ((iterator (gensym "ITERATOR"))
	  (next (gensym "NEXT")))
    `(with-hash-table-iterator (,iterator ,ht)
      (loop
       (multiple-value-bind (,next ,key ,value) (,iterator)
	 (unless ,next (return nil))
	 ,@body))))))

Do-hash, test run

CL-USER> (do-hash-table (key var *a*) (format t "[~w ~w ]~%" key var))

[EMAIL 1009 ]
[DATA 987 ]
[CLOJURE 123 ]
[BAR 99 ]
[AZURE 42 ]

List comprehensions in Lisp

(collect.lisp)

Macros, general comments and advice

Problems with macros

Macros, Advice:

   (def... <identifier> ...

   (with-... (...) <body>)

   (do-... <body>   )

What will macros give you?

Consider two dimensions

Simple macros — complex ones

Application-specific macros — general purpose ones.

New languages

Macros can also be used to introduce a completely new notation...

Example: cl-yacc