What is Metaprogramming?
Metaprogramming refers to the ability to write the source code of a program to generate the source code of another program, i.e. it's the programming of programming.
When you write a good program, it processes a lot of data using as little computer resources (CPU time, memory) as possible. However, this only matters at runtime, which is when the programming is running. Before this stage, the source code must be converted to something executable somehow. This is called build time (or compile time). This source code must be written by a human being, the developer, and it must be built by a program. This means you can have a source code that takes a lot of time to write, and a lot of time to build, that produces an executable that is well-performant.
With metaprogramming, we write programs that solve code-generation tasks before the source code is built or compiled. Metaprogramming follows the same principles of programming in general, such as reducing repetition and reusing code.
Example of Metaprogramming
The most typical example of metaprogramming are preprocessor directives used in C. When C code is compiled, first all the source code is parsed by a preprocessor that can conditionally remove source code from the next stage. For example:
#IFDEF __DEBUG__
do_some_expensive_debugging();
#ENDIF
In the code above, the line of code do_some_expensive_debugging();
won't be included at all in the next compilation stage.
C metaprogramming is very primitive and only deals with changing text tokens.
In C++, we have templates, that can generate structured code by changing the type of a data structure.
template <typename T>
T getBiggerNumber(T x, T y) {
if(x > y) {
return x;
} else {
return y;
}
}
If the template above is instantiated with getBiggerNumber<double>
, the C++ compiler will generate a function with the signature double getBiggerNumber(double x, double y)
. This means every time you use the function with a different type, the C++ compiler automatically generates the source code necessary to implement this function with the new type.
This is necessary because in C++ and low-level programming languages the compiler must know how many bytes of memory it needs to reserve before the function is executed. Different functions with different bytes would require different machine code to be compiled. Similarly, when a function is called in C and C++, the address of the function is written in the machine code output. As different types are handled by different functions, the address that has to be outputted needs to be different and must be known before the function is compiled. For example, x > y
will output different machine code depending on the types of x
and y
.
Higher level programming languages also support metaprogramming, and in much more simpler ways. A common metaprogramming technique is the use of closures for generating bound classes and functions. For example:
function makeCustomClass(uniqueValue) {
class Foo {
get() { return uniqueValue; }
}
return Foo;
}
In the Javascript code above, a new binding of Foo
will be instantiated every time makeCustomClass
is called with a different value, such that makeCustomClass(1) !== makeCustomClass(2)
. You could instantiate an object from the instantiation of this class calling new (makeCustomClass(1))()
;
Some languages, like CSS, don't support metaprogramming concepts natively, which has led people to create entire programming languages to make CSS metaprogrammable. Such as LESS, Sass (SCSS), and Stylus. These "CSS preprocessors" let you use mixins and perform all sorts of substitutions and transformations before outputting CSS code.
@mixin reset-list {
margin: 0;
padding: 0;
list-style: none;
}
@mixin horizontal-list {
@include reset-list;
li {
display: inline-block;
margin: {
left: -2px;
right: 2em;
}
}
}
nav ul {
@include horizontal-list;
}
The SCSS code above will generate the following CSS:
nav ul {
margin: 0;
padding: 0;
list-style: none;
}
nav ul li {
display: inline-block;
margin-left: -2px;
margin-right: 2em;
}
One of the languages with the best support for metaprogramming is Zig. In Zig, you can unroll a for
loop at compile time by simply declaring it as inline for
. This lets you use tuples containing different data types in the for
loop.
const values = .{
@as(u32, 1234),
@as(f64, 12.34),
true,
"hi",
};
inline for (values, 0..) |v, i| {
try expect(v);
}
The Zig code above will be unrolled into:
try expect(@as(u32, 1234));
try expect(@as(f64, 12.34));
try expect(true);
try expect("hi");
Leave a Reply