What is metaprogramming?

"Lisp isn't a language, it's a building material."

– Alan Kay

Any program, regardless of the language used, contains two entities: data and instructions that manipulate the data. The usual flow of a program is mostly concerned with manipulating data. The issue with instructions, though, is that once you write them, it's like they've been carved into stone, and so they are non-malleable. It would be more enabling if we could treat instructions as data and generate new instructions using code. Metaprogramming provides exactly that!

It's a programming technique where you can write code that has the ability to generate new code. Depending on the language, it can be approached in two ways: at runtime or at compile time. Runtime metaprogramming is available in dynamic languages such as Python, Javascript, and Lisp. For compiled languages, it's not possible to generate instructions at runtime because these languages perform the ahead of time compilation of programs. However, you have the option of generating code at compile time, which is what C macros provide. Rust also provides compile time code generation capabilities, and these are more capable and sound than C macros.

In many languages, metaprogramming constructs are often denoted by the umbrella term macros, which for some languages are a built-in feature. For others, they are provided as a separate compilation phase. In general, a macro takes an arbitrary sequence of code as input and outputs valid code that can be compiled or executed by the language, along with other code. The input to the macro doesn't need to be a valid syntax and you are free to define your own custom syntax for the macro input. Also, how you invoke a macro and the syntax for defining them is different across languages. For instance, C macros works at the preprocessor stage, which reads tags starting with #define and expands them before forwarding the source file to the compiler. Here, expanding means generating code by substituting inputs that are provided to the macro. Lisp, on the other hand, provides function-like macros that are defined with defmacro (a macro itself), which takes the name of the macro being created and one or more parameters, and returns new Lisp code. However, C and Lisp macros lack a property that's referred to as hygiene. They are non-hygienic in the sense that they can capture and interfere with code outside the macro upon expansion, which can lead to unexpected behavior and logical errors when the macro is invoked at certain places in the code.

To demonstrate the problem with a lack of hygiene, we'll take the example of a C macro. These macros simply copy/paste code with simple variable substitutions and are not context aware. Macros written in C are not hygienic in the sense that they can refer to variables defined anywhere, as long as those variables are in scope at the macro invocation site. For instance, the following is a macro SWITCH defined in C that can swap two values, but ignorantly modifies other values in doing so:

// c_macros.c

#include <stdio.h>

#define SWITCH(a, b) { temp = b; b = a; a = temp; }

int main() {
int x=1;
int y=2;
int temp = 3;

SWITCH(x, y);
printf("x is now %d. y is now %d. temp is now %d ", x, y, temp);
}

Compiling this with gcc c_macros.c -o macro && ./macro gives the following output:

x is now 2. y is now 1. temp is now 2

In the preceding code, unless we declare our own temp variable inside the SWITCH macro, the original temp variable in main is modified by the expansion of the SWITCH macro. This unhygienic nature makes C macros unsound and brittle, and can easily make a mess unless special precautions are taken, such as using a different name for the temp variable within the macro.

Rust macros on the other hand are hygienic and also more context aware than just performing simple string substitution and expansion. They are aware of the scope of the variables that have been referenced within the macro and do not shadow any identifiers that have already been declared outside. Consider the following Rust program, which tries to implement the macro we used previously:

// c_macros_rust.rs

macro_rules! switch {
($a:expr, $b:expr) => {
temp = $b; $b = $a; $a = temp;
};
}

fn main() {
let x = 1;
let y = 2;
let temp = 3;
switch!(x, y);
}

In the preceding code, we created a macro called switch! and later invoked that in main with two values, x and y. We'll skip explaining the details in the macro definition, as we will cover them in detail later in this chapter.

However, to our surprise, this doesn't compile and fails with the following error:

From the error message, our switch! macro doesn't know anything about the temp variable that's declared in main. As we can see, Rust macros don't capture variables from their environment as they work differently compared to C macros. Even if it would have, we will be saved from modification as temp is declared immutable in the preceding program. Neat!

Before we get into writing more macros like these in Rust, it's important to have an idea of when to use a macro-based solution for your problem and when not to!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset