Where parallels cross

Interesting bits of life

On the power of macros: a dynamic lazy let

Recently I have been working on making moldable-emacs easier to extend. One of the challenges was to define common variables in a single place. For example, I wanted to make this code

(:given (:fn (let ((a (run-something))
                   (b (run-something-else)))
               ...))
 :then (:fn (let ((a (run-something))
                  (b (run-something-else)))
              ...)))

into this one

(:let ((a (run-something))
       (b (run-something-else)))
 :given (:fn ...)
 :then (:fn ...))

The second piece of code can save a bit of copy paste for when I write molds. I also need that a and b get calculated lazily for the :given clause: if there is something like (and nil a b), I want to skip to calculate the bindings (because it may be useless).

Since I am creating a little Domain Specific Language for defining molds, your Lisp-senses should scream: macros!

Let's start easy.

If you want to write a macro that wraps thing is a let, it is simple.

(defmacro with-my-let (&rest body)
  `(let ((a (+ 1 2)))
     ,@body))

(with-my-let (+ a 1))
4

This is good and easy because we know a in advance. In my case I realize the bindings of the let only at run-time. We then need as input the list of bindings.

(defmacro with-my-let (let-bindings &rest body)
  `(let (,@let-bindings)
     ,@body))

(with-my-let ((a (+ 1 2))) (+ a 1))
4

So far so good! And what if I get let-bindings as a variable?

(defmacro with-my-let (let-bindings &rest body)
  `(let (,@let-bindings)
     ,@body))

(let ((let-bindings '((a (+ 1 2)))))
  (with-my-let let-bindings (+ a 1)))

This breaks because the with-my-let macro expands to the following.

(let let-bindings
  (+ a 1))

This happens because macros don't evaluate their arguments. But even if they did, let-binding obtains a value only at run time! This is the first challenge: get the bindings at run time.

We need to do this in two steps:

  1. "pause" the generation of the code until run time
  2. at run time inject the value in the code

Here is how it looks.

(defmacro with-my-let (let-bindings &rest body)
  `(funcall
   (lambda (bindings body)
     (eval `(let* ,bindings ;; here ,bindings = ((a (+ 1 2)))
        ,@body)))
   ,let-bindings ;; here ,let-bindings = let-bindings-var
   ',body))

(let ((let-bindings-var '((a (+ 1 2)))))
  (with-my-let let-bindings-var (+ a 1)))
4

The trick is a function that takes the let-bindings variable we pass. This function is somewhat like a macro: it produces a sexp itself (the bit `(let)! But, it evaluates it as well (the eval bit).

Well, let me show you how it expands:

(let ((let-bindings-var '((a (+ 1 2)))))
  (funcall
   (lambda
     (bindings body)
     (eval
      `(let ,bindings ,@body)))
   let-bindings-var
   '((+ a 1))))

I must confess: it took me some time to fully understand what I did when I wrote it!

Now lets get in the funny bit: what if we want to have lazy bindings? By lazy I mean bindings getting a value only at the latest possible moment. This means that if we don't use a value, we don't invest any time in producing it!

Emacs is so cool that it already has a way to do that: thunk.el. This library provides thunk-let*, which does exactly what we need: it makes all bindings lazy!

So the macro will change only slightly to become amazing!

(defmacro with-my-let (let-bindings &rest body)
  `(funcall
    (lambda (bindings body)
      (eval `(thunk-let* ,bindings ;; here ,bindings = ((a (+ 1 2)))
               ,@body)
            t))
    ,let-bindings ;; here ,let-bindings = let-bindings-var
    ',body))

(let ((let-bindings-var '((a (+ 1 2))
                          (slow-poke (sleep-for 20)))))
  (with-my-let let-bindings-var (+ a 1)))
4

If you try this code, you will see that you will skip slow-poke's long sleep time! All we needed to do was to substitute our let* with thunk-let* AND make sure that sexp is evaluated in a lexical context. You can do that by giving eval an extra argument.

How amazing is this macro?! Well if it is not, let me know because I would still like to improve it, if possible.

And keep in mind that your body must be inline! For example, this cannot work:

(defun f (x)
  (+ a x))

(let ((let-bindings-var '((a (+ 1 2))
                          (slow-poke (sleep-for 20)))))
  (with-my-let let-bindings-var (f 1)))

So if you have something like that, you have to pass the binding or inject the function code in the body.

Now that I gave you that caveat.. we are done!

Thanks to stick around so far and hopefully you will find inspiration to write your own useful (lazy?!) macros!

Happy macro-ing!

Comments