Hacker News new | ask | show | jobs
by ux266478 19 days ago
In an awkward, error-prone, non-mathematically rigorous and easily subverted manner. That whole "make invalid states unrepresentable" thing relies upon the fact the semantic structures you're doing it with can't be shadowed silently by other parts of the code, and the safety constraint itself is guaranteed to be completely pure and deterministic. Lisp macros and reader macros fail at that criteria. They aren't any different from any other collection of procedures and their collective interface. Actually, they're a fair bit worse since the natural cognitive overhead from juggling multiple layers of evaluative indirection lends itself to blindspots of edge cases and unsoundness.

I think "macros are a safety mechanism" might qualify for the top 3 "new lisper mania" things I've ever read.

1 comments

It can be made as mathematically rigorous as anything computable, and as non-awkward as is possible to represent. You either reify the invariants and semantics as statically analysed syntax or they are implicit in your collections of procedures and objects which is far more cognitive load. If you need to judge multiple levels of abstraction while using the DSL then the DSL is the wrong fit for the problem. There is no 'evaluative indirection' because a macro is a _compiler_ and the underlying code can be extremely alien to the DSL semantics.

Any argument against macros must be levelled against compilers in general, as _they are the same thing_. Rust is compiler as a safety mechanism, not different from macros as a safety mechanism.

Again, fallacy by Turing completeness.

It's actually harder to make something not Turing complete - in and of itself this property is absolutely useless and tells you nothing about how practical something is.

yes it's desirable to use non turing complete languages as less shit can go wrong, which is why you need macros. You seem to over estimate the difficulty of writing compiler macros, remember you only need to go from the DSL -> Lisp, not lower it into machine code. The DSL's compose perfectly as well, lower one to another or combination of them + lisp.
I use plenty of DSL's in my daily work maintaining 1 million+ lines of C++ code used by very large companies around the world. No macros needed or wanted. More than 90% of the production code is generated from DSL's.

Outside of C++, Haskell has many great examples of powerful DSL's implemented without macros.

The problem with macros is the compile/run time cost. I could have implemented those DSL's using C++ templates (which are Turing complete by the way) but I prefer not to.

Well, compilers have layers - you ain't outputting assembly from the AST layer directly, usually there is some kind of IR. Like LLVM have plenty of layers.

And for a simple DSL you can usually just use a sufficiently expressive language, no need for macros. Like kotlin/Scala has html DSLs that look pretty decent.

For anything more complex though, you can't just "compose stuff", a type system is a global property so for that you literally have to write something way more complex than a local macro should contain.

>kotlin/Scala has html DSLs

these interpreted DSLs aren't at all what I'm talking about. The macro system should be able to compile arbitrary text into anything in the simplest possible way and have it seamless with the rest of the code + able to call into it and build off it.

A type system is paired with a language, not a global property. You can have language L1 with typesystem T1 compile into (L2,T2), etc. These typesystems can be domain specific and arbitrary as well.

After reading your posts I recommend you go ahead and learn common lisp macros, it will be more informative than more discussion.

> go ahead and learn common lisp macros

Lisp macros used to be the one advantage Lisp had over other programming languages. However nowadays macros are common. There are even languages that has more than one flavour of macro system (Haskell has both a typed and non-typed flavour of macros).

However I personally prefer custom code generators instead of macros. The problem with macros, for the kind of large scale systems I work on, is understanding macros with N layers of abstraction, and also the compile/run time cost of using them.

Also, you can write code generators in any language and generate code for any language. Which is a huge advantage. I (for example) use code generators that generate C++, Java, Typescript, SQL, PDF's, interface descriptions, protocol specs etc. Whatever is required by a customer or other members of the team I can generate without demanding that they use a specific programming language.

Don't get me wrong. I love playing around with macros. However I have decided not to use them for real work.

The inflection of the copula "can be" is exactly the problem, as is the 'anything computable'. There's a reason Isabelle is used as a proof assistant, and Prolog isn't. The former guarantees sound, mathematical rigor that can't be trivially violated. The latter doesn't. Prolog is much more powerful and flexible than Isabelle. Power and flexibility are disastrous for safety. As someone else said elsewhere in this thread, safety is about restrictions.

In this case, it actually runs deeper than this, there are structural issues which prevent them from being sound right from the beginning. It's the mere fact that the whole behavior of your DSL can be silently changed without touching the module it's defined in, or the file it's being imported into. A well-meaning junior, in a completely different department, can completely destroy your invariants in a completely unrelated part of the codebase. Whether by shadowing runtime functions, or a package-level collision that exists upstream. Even Scheme's hygienic macros don't actually solve this, they just make it less likely. At the point in which you have no behavioral guarantees of semantics, you do not have a safety mechanism at all.

> There is no 'evaluative indirection' because a macro is a _compiler_ and the underlying code can be extremely alien to the DSL semantics.

This is a trivial contradiction. That is precisely evaluative indirection. Yes, stacking multiple compilers on top of each other, especially when their semantics are non-isomorphic, is a massive burden of mental overhead. Given you already have this problem with the mapping between Lisp and binaries, adding even more complicated layers on top of it is a recipe for disaster. Particularly because: you are going to make mistakes in your logic. Macros provide no means to stop you from blowing your own leg off when writing them. That is not what they are designed for.

Finally, you cannot escape the awkwardness of the underlying metaprogramming system, you will always have to wrestle with quotation and quasiquotation. You have to define a DSL before you can use it.

Only at the point in which you treat and use Lisp like an unserious toy, rather than the powerful tool that it is, can the fantasy of "macros are a safety mechanism" begin to make sense.