Macros are not necessary in programming languages.

Macros in Lisp are a powerful idea. They allow us to define compile-time functions that expand to more primitive Lisp code. With macros, big ideas can be encoded in a small footprint. This is great for the developer, but it has some downsides.

Chief among these downsides is comprehension. A macro can break the standard way we as human beings analyze code. Take for instance the following macro.

(for x in [1 2 3] (print x))

This macro is easy to read for most people familiar with programming. We iterate over the list [1 2 3] and bind each element, consecutively, to `x`. Then we print that value. Simple!

Now ascertain the meaning of the following macro.

(foo bar nax [1 2 3] (boo bar nax axxo))

This code is completely incomprehensible. We cannot distinguish variables from literals. What if `nax` is a literal in this context, and `bar` is a variable? Perhaps it's the other way around. You could say "surely it's the naming of variables that's unclear". You're right, but you'll stumble upon code where you are unfamiliar with the variables and literals. In such cases, a macro will simply give you no standard language keywords that you can anchor your mental model of computation to.

I present to you now the main use-cases for macros and why they're unnecessary if a language supports certain core features.

1. Bind lexical variables to a scope from within a macro

We've seen the canonical macroized for loop.

(for x in [1 2 3] (print x))

This macro can be transformed into a functional form.

(for [1 2 3] (fn(x) (print x)))

Here `fn` is the function declaration method of your language. Note how this immediately gives you an anchoring point for your mental model of the code. You instantly recognize `x` as the variable under consideration.

Let's consider the extreme example, now transformed to the functional form.

(foo [1 2 3] (fn(bar) (boo bar nax axxo)))

It is clear that all items (boo, bar, nax, and axxo) are simply variables because they're not literals defined by our given language. We also note that `bar` is contingent upon what `foo` provides to it. The other variables `boo, nax, and axxo` are clearly references to variables in the current lexical scope.

The meaning of this code is clear even though we do not know what `foo` does. With the small syntactic overhead of `fn` we retain clarity.

2. Generating a bunch of functions

Suppose we want to generate a bunch of logging functions. One way to do this is via a macro as such.

(generate-loggers trace debug info warn error)

Again, this is not very clear, are the arguments variables that are already defined? We could instead provide a quoted list.

(generate-loggers '(trace debug info warn error))

It is now clear that these are literals. Still, why bother with this when a function can do the same?

(def loggers (generate-loggers ["trace" "debug" "info" "warn" "error"]))

Instead of letting the macro bind to the lexical scope, we manually do so using `def`. We can access the loggers using the returned object via `(loggers.debug "hello world")`.

This is much clearer since it's difficult to know exactly what variables a macro will generate from its invocation alone. We'd have to go through the macro code. This can take considerable effort and time. Because `def` is a language construct, we can easily see that `generate-loggers` returns a dictionary/table which we can access.

If a language supports key-value tables and first-class functions, we do not need macros to manipulate variables in a scope. Without macros we lose the ability to declare variables from within the macro, but I believe this loss is justified as it makes code a lot clearer. If the only thing that can manipulate the scope is `def`, then that's a good thing simply because it's immediately apparent what variables are in the current scope.

If we really wish to bind the loggers to the scope it's better to be explicit.

    (def loggers (generate-loggers ...))

    (def trace loggers.trace)
    (def debug loggers.debug)
    (def info loggers.info)
    (def warn loggers.warn)
    (def error loggers.error)

3. Domain specific languages (DSL)

Macros shine when creating DSLs. These too are unnecessary when our core language has succinct ways of defining nested data structures. Consider the language Scribble used in Racket.

    @title{This is the title of my article}
    Here is some documentation text.
This language uses reader macros to exit standard Lisp list syntax. Scribble is easy to work with, but it's not strictly necessary when we can just use standard data structures.
    (doc [
        (title "This is the title of my article")
        "Here is some documentation text"
    ])

The above has considerable syntactic overhead, but it's not something I would worry about too much. It's still very legible and now even easier to work with. The code is much more easily testable. We can inspect the output from the function `title` and see what it does. By using the above we can make our documentation generator more modular. Features can be added in a seamless, functional way.

    (doc (fn (x) [
        (title "This is the title of my article, from file: " (x.filename))
        "Here is some documentation text"
    ]))

4. Reflection

In Rust there are procedural macros like `#[derive(Debug)]`. This macro generates a `Debug` printer for a struct or enum. The only reason for it being a macro is because we cannot iterate over the struct fields or enum variants. That's compile-time information that the program itself cannot access.

A language with reflection - the ability to inspect the code at run-time - does not need this kind of macro. Look at Nix (the language). This language has a `attribute sets`, which are just key-value stores coded as { x = 1; y = { x = 2; }; }. We can iterate through the keys and their values and generate another attribute set.

5. Type system generics

Any language with static typing will have its types declared at compile-time. Languages like Rust and C++ can abstract over types by using generics. In duck-typed languages this is unnecessary at the expense of the developer not knowing what a function requires of its input arguments without reading the code.

While statically typed language can support generics via macros, we can also just have the language be duck-typed with type annotations serving as assertions as well as developer documentation.

Speed considerations

Of course, all the language features that make macros unnecessary also elevate their solutions to run-time instead of compile time. This is a downside of not using macros. Rust does a good job of distinguishing macros from function calls; it designates a macro via the `!` suffix, and procedural macros are called using #[]. This at least makes it explicit at any invocation site that we are in fact dealing with a macro. Traditional lisps however do not distinguish between a macro and a function call at the call site.

Macros have a place in languages where speed is important. Even then, if we have language that does not support macros, it's possible to create processors.

Summary

There is great value in adhering to a common, known standard. It's easier for developers to have a common language to interact with. The value of macros/DSLs is not justified by the complexity they introduce; it is superceded by functional programming and simple, easy-to-specify data structures. In addition, we should allow reflection to prevent the need for certain classes of macros.