I did a quick scan of one of my side projects, and found a defun:defmacro ratio of 10:1, and (defun + defmethod):defmacro ratio of 16:1. In another codebase I worked on, the ratio is 7:1 and 9:1, respectively, but it gets tricky to count with grep[0]. Correcting for greppability, I'd estimate the ratio closer to 20:1.

That may seem like a lot of macros, but it's actually few functions. A big reason to use macros is to have them generate functions for you, or remove the need for having them in the first place[1].

The macros I write tend to cluster in two categories, that you could almost call "functional" and "imperative".

1. The "functional" macros are code generators that eliminate boilerplate. For example:

(switch-key key-event
("RET" (funcall on-activate widget)
t) ;; return T to mark event as consumed
("ESC" (... do some stuff ...)
t)
(t nil))

The line above looks like a simple switch-case, but it hides quite a bit of complexity. It ultimately expands to `cond` (Common Lisp's if/else/elseif), but:

- It injects appropriate code to compare against data hidden in key-event, which is a structure containing codes and modifiers.

- It injects code to transform string key descriptors like "RET" or "C-a" (CTRL+A) or "C-M-<F12>" (CTRL+ALT+F12) into something that can be compared against data in key-event.

- It detects when the switch keys are string literals (like in example above), and instead of injecting code to convert them, it executes it during compilation[2] time - this means string literals have zero runtime performance impact, they're compiled into the right data structures up front.

Conceptually, the macro is rather trivial - but this is already what Lisp people would call a DSL. This particular example is driven by the desire to have a switch-like API for key events over an Emacs-style keybinding notation, that's also zero-cost if keys are known at compile time. I have more macros in this style.

What makes these macros "functional" is that the code they generate doesn't have any compile-time or runtime side effects (other than those introduced by user code that's given as input to a macro invocation).

2. The "imperative" macros, or state-modifying macros, that modify compilation state. E.g.

(define-event foo (some-slot default-value) "Documentation")

This is a top-level macro that expands into:

- A global variable containing event definition - as metadata for debugging, reflection, compile-time correctness checking.

- A bunch of top-level functions like `make-foo` and `send-foo` (note the function name contains the name used in define-event).

Other macros like this generate classes, or family of classes, or fill in some global hash tables, etc. These macros create proper DSLs - they contain complexity that allows me to make concepts like "events" or "components" behave as if they were a part of the language already. This almost always forces them to be "imperative" - they alter the language runtime state[3], by defining new functions, methods, classes, types, creating or modifying global variables, etc. But the resulting functions, or instances of classes, can be then used in regular functional code.

--

On the "data over functions over macros", I'm not exactly sure what Clojurists mean by this, but I think I may have an idea. Many times, I've found myself writing an "imperative" macro that would define something as a global entity in the system, only to later wish I kept it as a data structure I can pass around.

My favorite example is defining REST API routing. In one project I worked on, we had a bunch of top-level macro invocations like:

(define-rest-function foobar-handler "/api/foo/bar" (some request params)
"Some docstring"
(actual handler body...))

The macro did the obvious thing - created a handler function, adorned with REST-related boilerplate, and registered it as the handler for the route "/api/foo/bar" in the global routing table. This pattern is something I see people commonly do, but it becomes a problem the moment you want to have more than one routing table - whether for testing or extending via composition.

So these days, I try to avoid macros like these - that define a global state for things that are only "global" in the sense that, in production, there should be only one of them. From one little job I did in it, I recall the Clojure style is to keep such definitions as data structures, and I think it's a good idea.

--

[0] - Macros are often used to generate functions, so in many places, code that's technically a function definition sits inside a macro invocation, making it hard to count with casual grepping like I'm now doing.

[1] - In my experience, two main roles of a functions is modularity (including testability) and readability. Macros eliminate some of the latter - they let you simplify code that would otherwise have you create functions just for the sake of keeping another function's body simple.

[2] - Actually, during "macro expansion time", which is separate from "compilation time" in Common Lisp, but it's usually the same thing.

[3] - The distinction between "compile time" / "load time" and "run time" in Lisp family of languages is mostly a matter of opinion. Defining a class isn't much different conceptually from modifying a global hash table, except it usually happens during what one would consider "compilation" instead of "execution".