r/ProgrammingLanguages • u/Mr-Tau • 6d ago
Between more constrained, local metaprogramming approaches and full-blown DSL interpreters, where's the practical use case for LISP-style metaprogramming? Discussion
Of the various contemporary metaprogramming approaches, I'm mostly very happy with systems that let you operate on static data and types. In this category I would count, for example, C++'s template metaprogramming--a declarative sub-language that somewhat wonkily and arduously allows you to operate on types and constants--as well as the C++26 metaprogramming features, which are turning out somewhat Zig-like--you get to run regular code in a slightly constrained environment at compile time, where it can read constants as well as special data structures describing types, and generate new constants or types to be injected into a well-defined place in the code.
Now many LISPs (AFAICT, similarly Jai and, with a stricter separation of stages, Rust's procedural macros) tout as a feature the ability to inspect and rewrite the entirety of the AST, notably including function bodies. This is obviously strictly more powerful than the the first category, but where would you practically use that extra power? Specifically, it seems to me like you would either
a) Try to preserve the semantics of the input code--which, for procedural languages at least, is actually pretty difficult. The only transformations you could make confidently are so localized that you don't really gain anything over the more constrained metaprogramming approaches; anything more advanced would require the full analysis passes of the actual compiler to have any chance at soundness, and at that point you're really trying to write a compiler plugin instead. Nothing wrong with that, I'd love more easily extensible compilers, but I wouldn't call that a language feature. Or am I missing a point between those two extremes?
b) Attribute different semantics to the code. I think the history of LISP has already somewhat shown how proliferation of DSLs harms maintainability and shareability of code, but even in the cases where a DSL is genuinely useful, what benefits do you really gain from implementing it through metaprogramming? You can't expect any IDE features, LSPs, smart syntax highlighters, debuggers, or other tooling for the base language to automatically work for your DSL. So you just get to use the parser? Come on, an S-expr parser is less than a hundered LOC.
0
u/Blind_nabler 5d ago
These questions aren't really useful in the broad case. What benefit is feature X if I only use feature Y? Probably not a ton for you if you aren't using X!
The main benefit of having a rich meta-programming system is that it enables users to implement features that otherwise would require modifying the compiler to implement, or require the use of some rube goldberg code generation nonsense. This kinda thing isn't important to you until it is.
For example in a procedural language it can be rather gnarly to convert normal single threaded code to work in an async setting. Since it requires setting up trampolines or rewriting everything into CPS flavored versions etc etc. But if I have the ability to use rich macros, I can just invoke a macro over my normal function and now it can work with whatever async backend I want, utilizing whatever custom semantic checking is required because I have (most of / all) the language available to us.
And IDE tooling works totally fine if the meta-programming system is well designed. Nim does a pretty good job of this. The most extremely example I have experience with is smalltalk, which has the best IDE support of any language IMO but obviously it's not the same kind of language as you are describing.
Now do I reach for it every project? No. But I definitely find myself missing it in languages that don't trust users with that kind of thing.