> Eventually, good Clojure programmers seem to stop using them because the trade-offs are worse error messages or uncomfortable edge-case behaviours.
I am not so familiar with Clojure, but I am familiar with Scheme. The thing is not, that a good programmer stops using macros completely, but that a good programmer knows, when they have to reach for a macro, in order to make something syntactically nicer. I've done a few examples:
pipeline/threading macro: It needs to be a macro in order to avoid having to write (lambda ...) all the time.
inventing new define forms: To define things on the module or top level without needed set! or similar. I used this to make a (define-route ...) for communicating with the docker engine. define-route would define a procedure whose name depends on for which route it is.
writing a timing macro: This makes for the cleanest syntax, that does not have anything but (time expr1 expr2 expr3 ...).
Perhaps the last example is the least necessary.
Many things can be solved by using higher-order functions instead.
My canonical use-case for macros in Scheme is writing unit tests. If you want to see the unevaluated expression that caused the test failure, you'll need a macro.
Python's pytest framework achieves this without macros. As I understand it, it disassembles the test function bytecode and inspects the AST nodes that have assertions in them.
Inspecting the AST ... That kind of sounds like what macros do. Just that pytest is probably forced to do it in way less elegant ways, due to not having a macro system. I mean, if it has to disassemble things, then it has already lost, basically, considering how much its introspection is lauded sometimes.
I am not so familiar with Clojure, but I am familiar with Scheme. The thing is not, that a good programmer stops using macros completely, but that a good programmer knows, when they have to reach for a macro, in order to make something syntactically nicer. I've done a few examples:
pipeline/threading macro: It needs to be a macro in order to avoid having to write (lambda ...) all the time.
inventing new define forms: To define things on the module or top level without needed set! or similar. I used this to make a (define-route ...) for communicating with the docker engine. define-route would define a procedure whose name depends on for which route it is.
writing a timing macro: This makes for the cleanest syntax, that does not have anything but (time expr1 expr2 expr3 ...).
Perhaps the last example is the least necessary.
Many things can be solved by using higher-order functions instead.