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:
- "pause" the generation of the code until run time
- 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!