It makes it hard to know when things run. In Lisp you also have that problem everywhere, of course.
As the post shows this allows you to do stuff that looks like extending the syntax of the language.
I can’t decide if I love it or hate it!
carlosneves 13 hours ago [-]
Props to D for having such a simple implementation of fexprs[1].
The downside compared to macros I think is that, as argument expressions become lambdas, it becomes harder to manipulate them. Check the cond example which needs two fexprs, whereas one macro could do it.
Not sure about D, but if it were lisp, even lambdas could be manipulated as lists. Macro args not having the lambda wrapping just seems simpler.
Unless laziness is pervasive (I guess, no knowledge about GHC internals), you basically have an FEXPR (https://en.wikipedia.org/wiki/Fexpr) which was replaced by macros for good reasons.
floxy 15 hours ago [-]
Or you could wrap arguments in lambdas (anonymous functions) for eagerly-evaluted languages. Lisp unfortunately has a bulky syntax for lambdas, compared to something like Smalltalk. Of course, you can fix this with reader macros in Common Lisp (but no one does). Clojure has a shorter syntax I believe as well: #().
phyzix5761 15 hours ago [-]
You could do that as well with lisp by passing around lambdas to functions but that adds unnecessary syntax.
kimi 16 hours ago [-]
Remember the golden rule of Lisp macros: don't write a macro.
iLemming 13 hours ago [-]
That is badly generalized proverb. Clojure has unhygienic macros, no expander extension points, and no error-message layer, so "don't" is a rational default there. Yet, for example Racket writes macros that write macros as a matter of course. And that's because the language has the infrastructure: hygiene by default, phase separation, `syntax-parse` with grammar-quality error messages, a module system that actually knows about compile-time dependencies.
But even with some limitations, there are cases in Clojure where macros do achieve things otherwise unattainable. One practical example is multi-host code generation. You can have a Clojurescript macro that parses and analyzes some javascript lib, doing that on JVM side, while emmiting code generated to run in Javascript. Hyperfiddle/Electric is the best example of that kind of macro ingenuity.
Or take core.async's `go`. It takes an arbitrary body, analyzes it into an AST, and rewrites it into a state machine so that <! and >! can park and resume. On the JVM it lets you avoid blocking threads. In Clojurescript it is the only way to have the model at all, because JS has no threads to block.
Nothing about that is expressible as a function. The transformation needs the entire body as data. Same family: core.match compiles a pattern matrix into a decision tree. And there are many more example use cases for macros.
"Don't write macros" rule has the second part: "unless you truly have no choice."
drob518 11 hours ago [-]
My own personal rule is that every Lisp programmer should understand macros and very rarely reach for them. They are a super-power, but easy to misuse. I think Paul Graham is partly responsible for the “Lisp = macros” thinking and the general overuse of them. On Lisp was a great book but it does lots of things I wouldn’t consider good practice today. After Graham popularized Lisp and macros, there was a period when every Lisp newbie, myself included, was writing macros all the time. Now that I’m older, I know better. Clojure is my daily driver and I haven’t written a macro in years (though I’ve debugged ones other people have written). But I know defmacro is there and I can use it anytime I really need to.
iLemming 9 hours ago [-]
I don't think Paul ever envisioned Lisp evolving into something like Clojure, where macros indeed are a bit less than a first-class citizen. And I think the whole adage of "First rule of macro club - don't write macros. Second rule of macro club - don't dispute the first rule...", etc. became a thing only with Clojure. I don't disagree with you - genuine use cases for macros are uncommon. Yet it's an instrument - nothing's wrong with using it properly. Knowing when and how is a skill, experienced Lispers indeed - rarely reach for it.
chiply 14 hours ago [-]
Author here. I think in general this is sound advice, but in Emacs it is an incredibly convenient utility. I use them all the time. Mostly simple things like wrapping function definitions, or even other macros that wrap function definitions. Most of my lisp experience is in Emacs where the config layer is essentially exposed through an elisp API. In that context, I find myself using macros a lot. But if I was building some software from scratch, like say a data processing system, I can't immediately think of scenarios where I would define macros.
Worth mentioning in Elisp that even if you don't define your own macros, you use them more often than you would think. defun, unless, def-custom, etc (more examples in the blog post) are all macros. The fact they look and feel like non-macros like special forms and built in functions implemented in C is part of what makes elisp so cool to me. There are plenty of homoiconic languages, but you rarely feel the distinction between program and data in elisp, and macro-supporting lisps in general.
pkal 14 hours ago [-]
If you look at the definition of a macro like defcustom, you'll see that it just expands to a function call, where all the logic is implemented. This could also be implemented in the macro, but that is more complicated and brittle, due to the risk of double-evaluations and having to produce code that will (usually) later evaluate with the intended side effect.
Furthermore, macros and functions constitute a kind of function coloring. To use a macro in a function, like defcustom, you couldn't pass the name of the user option you are defining in as a symbol, as that is not evaluated. So instead you'd have to call eval on a runtime constructed expression, which is a cludge.
So I agree with the top comment, to avoid macros is a sign of Lisp maturity. It is easy and fun to admire them when you are coming from languages with arbitrary restrictions in their macro systems like C, but ones you get used to them you don't treat them with any more wonder than any other arbitrary restriction that a language may lack (bad ad hoc example: nobody praises C for the lack of a CALL keyword).
downut 5 hours ago [-]
Ok, common-lisp newbie here.
Could you explain why Paul Graham's book "On Lisp" is wrong? Or misguided? Or misunderstood by newbies like me? 30 years of c++ with a lot of dealing with templates and I'm not feeling the mandatory function purity here.
floxy 3 hours ago [-]
I don't think that is a reasonable request for a response to a comment post, but in my experience, many lisp macro advocates come from other procedural / OO languages and aren't well acquainted with other programming paradigms, so they don't generally know what the don't know about other styles, say functional programming, and how to solve problems using higher order functions, etc. instead of reaching for macros. Which is OK. Not everyone has to become a programming language dilettante.
hencq 15 hours ago [-]
Meh, that seems to be a bit of a clojure thing. In the Racket world, with hygienic macros and phase separation, they'll routinely write macros returning macros, etc.
drob518 11 hours ago [-]
Macros returning macros happens in every Lisp, including Clojure. It’s no biggie. Hygiene has nothing to do with it and there are ways of properly managing unique symbols in all Lisps, even if some dialects are more manual than in Scheme/Racket.
hencq 2 hours ago [-]
I know, but the point is that in Clojure there's a bit of a culture of not writing macros unless you have to, while in Racket there's no such thing. Agreed that you can write macros just fine in other lisps.
matheusmoreira 14 hours ago [-]
Why?
anthk 13 hours ago [-]
The Computational Beauty of Nature it's similar and it has the code of the book there:
Turnstile relies on the fact that in Racket, you can have identifier macros [1] which can be expanded in pretty much any location (except binding, naturally). Racket is unique in this regard. Clojure has a mechanism to get roughly the same thing, but it's complicated. [2]
Typed racket too, might I add. I just imagine static typing around stable core APIs would help users understand how eglot and vertico compare to prior art. The way I learned how the completion stack works was to ask claude to show me what the elisp entrypoints would look like if implemented in typescript. It's not because I'm unfamiliar with lisp, I'm not, it's just easier to reason with the world in terms of data structures and their contracts.
I always thought it odd that Penrose gets dropped from the Escher conversations.
mangodrunk 15 hours ago [-]
Can you elaborate? Roger Penrose and his father Lionel did independently discover and popularize the Penrose Stairs and Penrose Triangle but Oscar Reutersvärd had created both years earlier. What do you think should be said about Penrose?
chiply 14 hours ago [-]
Yeah, I think the story is that Penrose saw an illusion in an Escher print that encouraged him to create the Penrose Triangle, his dad Lionel created the Penrose staircase, and then sent a copy of these in the paper "Impossible objects..." to Escher, who then used the staircase in "Ascending and Descending" and others. It's kind of cool that they cite eachother in these works as inspiration here. I bet Escher could have made an interesting print of them pointing at one another in some kind of impossible object.
And apparently neither Penrose nor Escher knew about Oscar Reutersvärd's work! Oscar got robbed lol.
mangodrunk 7 hours ago [-]
Yeah, it’s odd that Escher didn’t respond to Reutersvärd’s letters. Also, Penrose found out about Reutersvärd’s work in 1984:
>It wasn’t until 1984 that Roger Penrose (whose father Lionel died in 1972) discovered that Oscar Reutersvärd had invented the triangle and the stairs much earlier.
In 1954, Escher inspired Roger Penrose to create the impossible triangle. Roger shared this with his father, Lionel, who then designed the impossible staircase. In 1958, they published a paper on these illusions and sent a copy to Escher. Escher used Lionel's staircase to create Ascending and Descending and Roger's triangle to create Waterfall. Both Penrose and Escher were completely unaware that Oscar Reutersvärd had independently invented these shapes earlier.
16 hours ago [-]
bossyTeacher 10 hours ago [-]
Why is most AI work done in Lisp? There are many reasons, most of which are somewhat technical, but one of the best is quite simple: Lisp is crisp. Or as Marilyn Monroe said in The Seven-Year Itch, "I think it's just elegant!"
This claim didn't age well.
Rendered at 08:18:10 GMT+0000 (Coordinated Universal Time) with Vercel.
Interesting, so if you’re using a lazy language then you don’t need a macro here and could write my-unless as a function.
https://dlang.org/articles/lazy-evaluation.html
It makes it hard to know when things run. In Lisp you also have that problem everywhere, of course.
As the post shows this allows you to do stuff that looks like extending the syntax of the language.
I can’t decide if I love it or hate it!
The downside compared to macros I think is that, as argument expressions become lambdas, it becomes harder to manipulate them. Check the cond example which needs two fexprs, whereas one macro could do it.
Not sure about D, but if it were lisp, even lambdas could be manipulated as lists. Macro args not having the lambda wrapping just seems simpler.
[1] https://en.wikipedia.org/wiki/Fexpr
https://dlang.org/spec/template-mixin.html
https://dlang.org/spec/traits.html
https://hackage-content.haskell.org/package/base-4.22.0.0/do...
But even with some limitations, there are cases in Clojure where macros do achieve things otherwise unattainable. One practical example is multi-host code generation. You can have a Clojurescript macro that parses and analyzes some javascript lib, doing that on JVM side, while emmiting code generated to run in Javascript. Hyperfiddle/Electric is the best example of that kind of macro ingenuity.
Or take core.async's `go`. It takes an arbitrary body, analyzes it into an AST, and rewrites it into a state machine so that <! and >! can park and resume. On the JVM it lets you avoid blocking threads. In Clojurescript it is the only way to have the model at all, because JS has no threads to block.
Nothing about that is expressible as a function. The transformation needs the entire body as data. Same family: core.match compiles a pattern matrix into a decision tree. And there are many more example use cases for macros.
"Don't write macros" rule has the second part: "unless you truly have no choice."
Worth mentioning in Elisp that even if you don't define your own macros, you use them more often than you would think. defun, unless, def-custom, etc (more examples in the blog post) are all macros. The fact they look and feel like non-macros like special forms and built in functions implemented in C is part of what makes elisp so cool to me. There are plenty of homoiconic languages, but you rarely feel the distinction between program and data in elisp, and macro-supporting lisps in general.
Furthermore, macros and functions constitute a kind of function coloring. To use a macro in a function, like defcustom, you couldn't pass the name of the user option you are defining in as a symbol, as that is not evaluated. So instead you'd have to call eval on a runtime constructed expression, which is a cludge.
So I agree with the top comment, to avoid macros is a sign of Lisp maturity. It is easy and fun to admire them when you are coming from languages with arbitrary restrictions in their macro systems like C, but ones you get used to them you don't treat them with any more wonder than any other arbitrary restriction that a language may lack (bad ad hoc example: nobody praises C for the lack of a CALL keyword).
Could you explain why Paul Graham's book "On Lisp" is wrong? Or misguided? Or misunderstood by newbies like me? 30 years of c++ with a lot of dealing with templates and I'm not feeling the mandatory function purity here.
https://github.com/gwf/CBofN
Some tools might crash X under CWM for OpenBSD, switch to FVWM or use some tool like mimalloc.
On Lisp, the book basically builds integers based on conses, kinda like Peano Axioms.
lexilambda created Typed Racket on top of Racket. https://github.com/racket/typed-racket
Turnstile relies on the fact that in Racket, you can have identifier macros [1] which can be expanded in pretty much any location (except binding, naturally). Racket is unique in this regard. Clojure has a mechanism to get roughly the same thing, but it's complicated. [2]
1: https://docs.racket-lang.org/guide/pattern-macros.html#%28pa...
2: https://lambdaland.org/files/2024_ecoop_type_tailoring.pdf
https://coalton-lang.github.io
or maybe
https://shen-language.github.io
And apparently neither Penrose nor Escher knew about Oscar Reutersvärd's work! Oscar got robbed lol.
>It wasn’t until 1984 that Roger Penrose (whose father Lionel died in 1972) discovered that Oscar Reutersvärd had invented the triangle and the stairs much earlier.
https://escherinhetpaleis.nl/en/about-escher/escher-today/os...
This claim didn't age well.