easy-macros

2024-10-12

An easier way to write 90% of your macros

Upstream URL

github.com/tdrhq/easy-macros

Author

Arnold Noronha <arnold@tdrhq.com>

License

Apache License, Version 2.0
README

easy-macros: An easy way to write 90% of your macros

tdrhq

Easy-macros help you write macros of this form:

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

Under the hood, this automates the call-with pattern.

Examples

Let's rewrite some well known examples to show what we mean.

ignore-errors

First let's see how we might write ignore-errors the Old-Fashioned way:

(defmacro custom-ignore-errors (&body body)
  `(handler-case
     (progn ,@body)
    (error () nil)))

Not too bad, but it's error-prone. You might forget to use a ,, you might forget to wrap body in progn etc. But worst, if you change the definition of custom-ignore-errors, you will have to recompile all the functions that use it.

You can avoid some of these issues by using the CALL-WITH pattern:

(defmacro custom-ignore-errors (&body body)
  `(call-custom-ignore-errors (lambda () ,@body)))

(defun call-custom-ignore-errors (fn)
  (handler-case
    (funcall fn)
   (error () nil)))

Now most of the logic is inside a non-backticked function. But there's still some backquoting and macro expansion we need to do which is error-prone, and it's also very verbose for simple macros.

Use def-easy-macro to essentially automate this process:

(def-easy-macro custom-ignore-errors (&fn fn)
  (handler-case
     (funcall fn)
    (error () nil)))

This custom-ignore-errors has a slightly different API though:

(custom-ignore-errors ()
  ...body...)

All easy-macros takes a second list for arguments. This is true even if it takes no arguments and only a body.

Notice a few things:

  • We don't use backticks anywhere
  • Instead of a body, we get a lambda function. This function is provided by the &fn argument.
  • If you redefine custom-ignore-errors, all callers of the macro will point to the new code, unlike with regular macros. (With some caveats! See below.)

We don't need to use funcall by the way, the following is equivalent:

(def-easy-macro custom-ignore-errors (&fn fn)
  (handler-case
     (fn)
    (error () nil)))

We're still figuring out which one we like better. This version obviously is lesser code, but it also breaks the expectation that arguments in the lambda-list are variables. But anyway, moving on to next examples.

with-open-file

(def-easy-macro with-custom-open-file (&binding stream file &rest args &fn fn)
  (let ((stream (apply #'open file args)))
    (unwind-protect
       (funcall fn stream)
      (close stream))))

This can be used almost exactly like with-open-file.

Notice a few things:

  • We don't use backticks anywhere
  • This function takes one argument. easy-macro knows this based on the &binding argument, unlike the previous example.

uiop:with-temporary-file

(def-easy-macro my-with-custom-temporary-file (&key &binding stream &binding pathname prefix suffix &fn)
   ;; ... you get the idea
    (funcall fn my-stream my-pathname))

I didn't build out the example completely, but I wanted to show you how you could write more complex arguments in the macro.

All the arguments named with &binding are not part of argument-list, they will be sequentially bound to the &fn body function. The rest of expressions form the lambda-list for the argument-list.

maplist

Common Lisp comes with dolist, but not a maplist. Let's implement a quick maplist macro using loop:

(def-easy-macro maplist (&binding x list &fn fn)
  (loop for value in list collect (funcall fn value))

Before def-easy-macro this would've been too much work to define for something simple. With def-easy-macro it's just as easy to work with as any regular function, so you tend to macrofy even tiny abstractions like this.

Caveats with redefinitions

Most redefinitions will automatically be applied to all callers. If you change the lambda-list (either &binding or otherwise), the new definition may not be compatible.

Installation

We're waiting on this to be part of the next Quicklisp distribution, in the meantime you can use quick-patch to install:

(ql:quickload :quick-patch)
(quick-patch:register "https://github.com/tdrhq/easy-macros.git" "main")
(quick-patch:checkout-all ".quick-patch/")

TODO

This library is NOT very polished.

However, even with its limited polish it's been ridiculously useful in my work, so I thought I should put it out there and accept feedback and pull requests. There are few things that I'd personally like to see:

  • Less brittle lambda-list parsing: currently it's really hacky
  • A way to implement macros of the form:
(def-stuff my-stuff (...)
  ,@body)
  • In a similar vein as above: sometimes in macros you want to pass the quoted symbol name instead of the evaluated expression. In theory I can build that...
  • But I want to limit what this library does. I want to make it easy for somebody new to CL to write macros most of the time. Just because I can doesn't mean I should.

Author

Arnold Noronha arnold@screenshotbot.io

License

Apache License, Version 2.0

Dependencies (3)

  • alexandria
  • fiveam
  • fiveam-matchers

Dependents (1)

  • GitHub
  • Quicklisp