| I started using Clojure in 2009 and used it heavily for a long time. I even ran my local Clojure meetup for many years. Lately, I've moved away from Clojure however and today I mostly write Typescript, some hobby C++, some Rust. Gleam when I can (which is rare). Anyway, that's just context to say that I've written and enjoyed a lot of Clojure which made me think about macros a lot over the years. I've come to the conclusion that macros are NEVER the right choice for normal application developers and only RARELY the right choice for library authors. I would pick function over macros every single time. Even in libraries, I feel that most uses of macros are unjustified. Even in cases where macros enable something that wouldn't otherwise be possible, I question whether its really the best way. For example, its pretty cool that core.async could be implemented as macros, but I feel that core.async has rather poor developer ergonomics because of it and building it into the language itself would have lead to a much better system with a much better developer experience. I have a number of reasons, but I'll just mention the biggest here: macros cannot see into functions, so core.async requires its async functions to be called directly within a go block (eg you cannot wrap core.async/<! inside a helper function because the macro won't be able to find it to transform it). Sometimes using macros means that things can be optimised at compile time (eg expanding core.match or specter selectors), but I feel these cases are pretty rare. I do like many of the macros in clojure.core (threading macro etc) but I see these as a language implementation detail -- they could have been built into the language grammar or compiler and the end user experience would be the exact same. I do wish Gleam supported some limited form of macros to generate code based on annotating types (kind of like Rust's derive), but I very much agree with Gleam's logic for not including macros, and my experience with Clojure has basically solidified my feelings that macros are very rarely a good idea. |
I should have went into a little more detail, maybe.
The general advice has always been "prefer data over functions, prefer functions over macros", but I don't think "prefer" is strong enough. I would rephrase it as:
"Prefer data over functions. Only use macros if there's absolutely no other option."
That means that macros shouldn't be used to make code more terse, or more convenient, or more "pretty". They should be used when they make code possible that wouldn't otherwise be possible (at least without jumping through a lot of hoops). For all my complaints, core.async is actually a good example of a good use of macros, as far as a library goes. It adds functionality that would be quite difficult to do cleanly without macros. My complaint is just that a macro implementation of something so integral is very much inferior to an implementation that was part of the language itself. I don't think async should be something tacked on to a language as an afterthought.
An example of what I consider a bad use of macros would be something like this:
Imagine you have a system to register event handlers that can then be triggered by name:
Many clojure libraries exist with patterns like this, especially from earlier on before the community began to shift more closely to the `data > functions > macros` mentality.A macro-less version might look something like this:
Where the expression that the macro makes possible is wrapped in an anonymous function and the naming is explicit. Its not quite as convenient as the macro version, but it avoids magic and therefore surprises, and its more flexible because you can compose it or operate on it like any other function.