A Use For Macrolet

I recently had a good use for Common Lisp’s macrolet special operator. Just as let establishes a new variable bindings and flet establishes new function bindings, macrolet establishes a new macro definitions.

For example, here’s a locally-defined anaphoric lambda macro called fn.

(macrolet ((fn (&body body) `(lambda (_) ,@body)))
  (map 'string (fn (if (standard-char-p _) _ #\*)) "naïve"))
;; => "na*ve"

My particular use case was about making my code cleaner for a brainfuck interpreter. The state of the machine was being tracked by this struct. (Interesting side note: SBCL warns about using p as a slot name because the accessor function will look like a predicate.)

(defstruct bf
  (p 0)
  (mem (make-array 30000 :initial-element 0)))

The BF instructions + and - increment the byte at the data pointer. The Common Lisp incf and decf macros can be used to do this. Similarly, the , instruction sets the byte at the data pointer, which can be done with setf. All three of these macros are place-modifying.

(defun interp (program state)
  ;; ...
  (incf (aref (bf-mem state) (bf-p state)))
  ;; ...
  (decf (aref (bf-mem state) (bf-p state)))
  ;; ...
  (setf (aref (bf-mem state) (bf-p state)) (char-code (read-char))))

That’s a whole lot of redundancy for a Lisp program. Under similar circumstances elsewhere I might use flet to reduce it.

;; This won't work.
(defun interp (program state)
  (flet ((ref () (aref (bf-mem state) (bf-p state))))
    ;; ...
    (incf (ref))
    ;; ...
    (decf (ref))))

The problem is that ref isn’t a generalized reference, which incf, decf, and setf all require. Common Lisp’s place-modifying utilities are implemented as macros. It’s known at compile-time what kind of place they are modifying: a variable, array index, object/struct slot, car, cdr, or many other things (Emacs cl package allows all sorts of things to be setfed, like (point)). The macro expands into the proper form for setting that kind of place.

The specific expansion is implementation-dependent, but, for example, setf could expand into a setq when the first argument is a symbol. New generalized references can be defined with defsetf.

In my case, a simple macro expansion can fill the role. Below, the place-modifying macro will expand ref (after looking elsewhere) to decide what to do, and ref will expand to an aref form.

(defun interp (program state)
  (macrolet ((ref () '(aref (bf-mem state) (bf-p state))))
    ;; ...
    (incf (ref))
    ;; ...
    (decf (ref))
    ;; ...
    (setf (ref) (char-code (read-char)))))

Because the macro has no parameters I could have even more easily used symbol-macrolet. I just didn’t think of it at the time.

Load Comments

null program

Chris Wellons