by ianthehenry on 4/18/23, 1:51 PM with 74 comments
by nickdrozd on 4/19/23, 3:46 PM
> So people have spent a lot of time thinking about ways to write macros more safely – sometimes at the cost of expressiveness or simplicity – and almost all recent languages use some sort of hygienic macro system that defaults to doing the right thing.
> But as far as I know, no one has approached macro systems from the other direction. No one looked at Common Lisp’s macros and said “What if these macros aren’t dangerous enough? What if we could make them even harder to write correctly, in order to marginally increase their power and expressiveness?”
The first example discussed is a defer macro that can be invoked with "no indentation increase and no extra nested parentheses".
As a macro-lover and an indentation-hater, I think this is a brilliant and hilarious idea.
by munificent on 4/19/23, 5:02 PM
This is my new favorite typo:
(defmacaron . lefts [key & rights]
~(,;(drop-last lefts)
(get ,(last lefts) ,(keyword key))
,;rights))
We're working on a macro proposal for Dart and I wonder if users would like them more if we called them "macarons".by abecedarius on 4/19/23, 7:17 PM
(Just from a quick skim of this long post.)
by hzhou321 on 4/19/23, 7:22 PM
LISP has a full-featured macro system, thus hands down beats many languages that only possess handicapped macro system or no macro system at all. It uses the same/similar language to achieve it is mere accidental. In fact, I think LISP is an under-powered programming language due to its crudeness. But it's unconstrained macro system allows it compensate the programming part to certain degree. As a result, it is not a popular language and it will never be, but it is sufficiently unique and also extremely simple that it will never die.
What if, we have a standalone general-purpose macro system that can be used with any programming languages, with two syntax layer that programmers can put on different hat to work on either? That's essentially how I designed MyDef. MyDef supports two forms of macros. Inline macros are using `$(name:param)` syntax. Block macros are supported using `$call blockmacroname, params`. Both are syntactically simple to grasp and distinct from hosting languages that programmers can put on different hats to comprehend. The basic macros are just text substitution, but both inline macros and block macros can be extended with (currently my choice) Perl to achieve unconstrained goals. The extension layer can access the context before or after, can set up context for code within or outside, thus achieve what lisp can but using Perl. We can extend the macros using Python or any other language as well, but it is a matter of the extent to access the macro system internals.
Inline macros are scoped, and block macros can define context. These are the two features that I find missing in most macros systems that I can't live without today. Here is an example:
$(set:A=global scope)
&call open_context
print $(A)
print $(A)
subcode: open_context
set-up-context
$(set:A=inside context)
BLOCK # placeholder for user code
destroy-context
by sparkie on 4/19/23, 5:30 PM
I use Kernel a lot, which allows you to write first-class operatives which can influence the bindings of their caller, but they don't allow modifying the body of the calling function.
by remexre on 4/19/23, 3:45 PM
by openasocket on 4/19/23, 9:46 PM
by kazinator on 4/20/23, 1:30 AM
You make a macro called crazy-macrolet which is used like this:
(crazy-macrolet ((crazy-macro (left-forms right-forms arg ...)
....)
(other-crazy-macro (...)))
body)
Inside body, you use the local crazy macros. A little language made of these crazy macros can be wrapped up in a big macro. (defmacro bobs-crazy-dsl (&rest forms)
`(crazy-macrolet ((bobs-crazy-macro ...))
,@forms))
Then, you can only use bobs-crazy-macro in code wrapped in (bobs-crazy-dsl ...).crazy-macrolet needs to implement a code walker to in order to expand those macros.
Macros can be context-dependent without having access to the literal forms to the left or right (or elsewhere).
In TXR Lisp, I implemented tagbody as a macro, providing a measure of CL compatibility. The go operators are local macros. They do not expand in a context-free way; they communicate with the surrounding tagbody.
So for instance if go is asked to jump to a nonexistent label, it errors:
1> (expand '(tagbody (go a) b))
** go: no a label visible
1> (expand '(tagbody (go a) a))
(let ((#:tb-id-0019
(gensym "tb-dyn-id-"))
(#:next-0020
0))
(sys:for-op ()
(#:next-0020)
((sys:setq #:next-0020
(block* #:tb-id-0019
(sys:switch #:next-0020
#(((return* #:tb-id-0019
1))
()))
())))))
So obviously, (go a) is behaving differently based on whether it can "see" that there is an a in its context, on the left or right side.by kerkeslager on 4/19/23, 4:45 PM
Lisp enthusiasts like to point out the power of macros, and macros are the raison d'être for Lisp's homogeneous s-expression syntax. Most other features in Lisp (such as first-class closures and higher-order functions) can exist without s-expressions, but the powerful thing about s-expressions is that they enable Lisp macros.
But with great power comes great responsibility. When I'm writing a program I want as little responsibility as possible while still being able to solve the problem at hand. I don't want to be responsible for memory management and bounds checking, and I don't want to be responsible for the hygiene of my macros at both the definition and the call site.
With C, the responsibility of memory management and bounds checking comes with a power that people actually need to solve problems. For me these problems usually come up in the context of writing my own hobbyist interpreters/compilers, but there are a lot of real world cases where these come up. But often you don't need the capabilities of C, and I'd argue as a result that there are a lot of cases where using C is a bad idea because it's not the best way to solve your problem.
And here's the hot take: the power of Lisp macros isn't actually ever worth the responsibility in my experience. The problem Lisp macros solve is "this code is more verbose/ugly/boilerplate-y/etc. than I want it to be", which just isn't the problem you're writing the program to solve. Whenever you reach for a macro, there's another tool you could be reaching for to solve the actual problem at hand. At the very least, you can just write the code the macro would expand into. There's inherently never a case where the macro is the only way to solve the problem.
If you're good at writing macros, you won't always get burned by them, but nobody is ever perfect at writing macros, so everyone gets burned sometimes. If you're writing software that you actually need to work, the risk is rarely worth it.
When I have written production code in a Lisp (mostly Clojure) I've rarely reached for macros, and often bugfixes have been removing a macro that was of the "so preoccupied with whether or not they could, that they didn't stop to think if they should" variety. And if you spend enough time avoiding and removing macros, you start to wonder why you're destroying your eyesight trying to match parentheses, when the entire reason for the parentheses is to enable something you have to avoid and remove.
And don't get me wrong: macros are cool. "Because I like them" is a totally valid reason to write macros and Lisp.
by foodoos on 4/19/23, 5:02 PM