back




February 2016

What may be the most powerful programming abstraction is also one that's used the least. The macro: a long instruction that generates a pattern of code. If you don't use macros you don't see macros adding much compared to using functions. And yet programmers who write macros go as far as refuse to program without them.

What do macro programmers know that function programmers don't?

What they know is their programs are shorter. With macros you write programs that use the fewest number of elements. If we entertain the hypothesis that shorter programs are more powerful, macros are more powerful than functions. But macros are powerful in an additional sense. Macros can do things functions simply can't.

Now we have three questions. How do macros make programs shorter, how much shorter is that, and what can macros do that functions can't?

I'm going to explain how macros make programs shorter with examples and give numbers to support the code savings. As a basis for discussion, please treat these as base cases that show how code size explodes. If you have a juicy example I missed feel free to send it over. If the version with macros can be shortened with functions the burden is now on you to give a counterexample.

The examples are written in C. That's a good language to write the examples in because of how low level the language is. Any problems that arise with C will have to be solved by higher level languages too.

I'll measure code size in number of lines. That's not as accurate as number of elements. I can't calculate elements in C as easily as lines of code but this metric is good enough to drive the point across.



Example 1: Functions can't take operators as arguments

Let's start with an example program that does only arithmetic: addition, multiplication, subtraction, and division. Here is the program in C with functions and here it is with macros.

Measured in number of lines, the program with macros is 1 one line shorter. It uses 7 lines instead of 8. The only way to write this program with functions is to write four functions. Whereas with macros you can write the pattern of the body only once and call it four times.

The root of the problem is you can't pass an operator as a function parameter. Here we couldn't pass an arithmetic operator but the same problem applies to relational, logical, and other operators.

There is a threshold past which code savings from macros start to pay off. The example wouldn't prove its point with only three functions, because then the code size for either the function or the macro version would be equal: 6 lines of code each.

All this work to save 1 line of code? How could that ever amount to anything? Well, the point in this example isn't the absolute code savings, but that it's possible for a macro to save code. As we'll see later a compounding factor is the nature of the problem solved, not operators as arguments specifically.



Example 2: Functions can't take data types as arguments

A fairly general problem with functions is you can't easily generate multiple versions of them with different signatures for each version. One instance of this problem is generating functions with multiple data types. You can't pass a data type as an argument. [1]

Coming back to example 1, you can't make its functions also work with floats instead of only integers. You have to write new functions by hand. Here is how that looks with functions and here it is with macros.

Measured in number of lines, the program with macros is 5 lines shorter. It uses 11 lines instead of 16. It's impossible to write a shorter version of this program in C with functions. You can't generate these eight functions automatically using the programming language alone.

Some programming languages like Java allow us to define opaque data types in the source code and generate multiple versions of the functions at compile time depending on the caller. But this code generation is limited to data typing. It doesn't offer full blown macros.



Example 3: Macro code savings depend on the problem

Examples 1 and 2 were contrived to prove a point. They aren't very useful programs and it's rare to need to write them. Now let's upgrade both to code taken from a real program: the arithmetic calculator of a programming language. A programming language may be the most powerful program you could write. So code savings writing the language are a powerful example themselves.

This example does everything examples 1 and 2 did. It adds, multiplies, subtracts, and divides numbers, but it does so by interpreting the numbers and storing the results in lists. To do all this, the code in the body of the macro increases from only 2 lines in the examples to 19 lines. Add to that the four invocations of the macro and the code uses 23 lines of code with macros, but would have taken 19x4=76 lines of code to write with functions. That's 3.3x savings in code size.

And that's the lower bound of code savings because the code calls four other macros (list_new, store_error, number_set, apply_number) that add their own code savings. If we add these four macros too and any macros these macros call (memory_check, number_init), and expand them to see the version of the code with functions, macros use 101 lines compared to 544 for functions. Who would have thought that measly 1 line of code saved in example 1 could blow up to 5.3x code savings. [2]

See now how the code savings compound? They are driven by the nature of the solution inside the macro. A solution may be written to be as powerful as possible, which means as short as possible, and yet produce many lines of code when the code is macro expanded.

Powerful solutions (in the sense that they are as short as possible) that also happen to be big solutions when macro expanded are a reflection of the bigness of the problem. It's hard to estimate an upper bound of the code savings because the bigger the problem the bigger the savings. The upper bound depends on the problem. [3]



Example 4: Functions can't generate patterns

The advantages of macros we've seen so far were both rooted in two limitations of functions in present day programming languages: not passing arithmetic operators and datatypes as arguments. If future programming languages fix these limitations, will macros still have any advantages?

Yes, they still will. Function code often contains patterns that programmers write by hand.

Here is an example of a 4-line if statement written as a macro used three times in this function that uses 43 lines of code. This is a pattern based on control flow, not arithmetic operators or datatypes. When this macro is expanded in the function the final code uses 53 lines of code. Compared to 43+4=47 lines of code for the macro version, the macro saves 6 lines of code.

Once again 6 lines of code savings don't look like much for a macro called only three times. But as we learned in example 3, macro code savings depend on the problem. The savings compound when long macros are called often.



Example 5: Macros save typing more characters

Some of the macros in example 3 could have been written as functions except for two inefficiencies. They would need an extra line of code for a return statement, and they would need typing in more characters to call them.

Like the macro for list_new. It takes 14 characters to type in and pressing the Shift key three times; once for the underscore and once for each parenthesis:

list_new(new);

If list_new is written as a function, it takes 25 characters to type in and pressing the Shift key five times:

list_t *new = list_new();

The difference is only 11 characters and two Shift presses in this case. But the amount of typing now also depends on the length of the name of the data type returned. In this example list_t is only 6 characters but there is no limit to the number of characters in a C data type, and as a consequence there is no upper bound to the number of extra characters you would have to type in. Even if you used a dynamic programming language, where you don't have to declare the data type of a variable, you'd still have to type 1 to 3 extra characters for the assignment multiplied by the number of times the function is called.

Is saving as little as 1-3 characters worth all this fuss? Yes, says example 3. The mindset that makes you save 1 character is the same mindset that compounds code savings up to 5.3x. [4]



Example 6: Functions can't modify functions

This is the biggest difference by far between a function and a macro. A function can't modify a function. A macro can.

I said something slightly inaccurate in example 1. Although we can't pass an operator as a parameter to a function, we can get pretty close by simulating it. We can pass a function as a parameter which contains code that executes the operator.

Here's how it can be done. We write our function once, write four separate operator functions, and pass an operator function as a parameter to our function when we call it. [5] This may seem counterproductive, because it causes a net code increase of 2 more lines of code, but it shows an important feature of functions: a function can execute code passed as a parameter. It's the closest a function can get to generating code.

Sadly it's not close enough. What a function still can't do is the opposite of generating code: removing code. A function can't accept code as a parameter and remove parts of the code before executing it. There is again some wiggle room to simulate removing code by executing only parts of the code depending on the code's parameters. But only if the code was written to support this, and even then changing the body of the code isn't possible. If a function will execute code, it will be the exact code it was given to execute.

Macros on the other hand are flexible: macros can change code. A macro can accept code as input and rearrange the code in any way it likes. It can change parts of the code to do something different, add to the code to do more, delete parts of the code to do less, or ignore the code altogether and not generate it. Functions simply can't do this. [6]



Impact

If you're writing a lot of macros already, how do you know if you are writing enough? We may have an empirical answer to this question. A preliminary version of the language from example 3, which supports only arithmetic but no function calls, consists of 476 lines of code, 131 of which are macros. That's 27% percent macros. [7] Viaweb's editor was as dense, roughly 20-25% macros. An inherent though not formally proven property of powerful programs may be that they must consist of at least 20% macros.

If you were suspicious of the mythical powers of macros and never saw benefits in them, now you have data on the benefits. Using macros saves code: 2.7x-5.3x for sure and probably a higher factor.

But what may be the biggest thing macros save is the hope of writing impossible programs. Software withers when it grows in size. Macros make software smaller. Smaller software has a better chance of getting somewhere no one else did than bigger software.

Keep a program small with macros and you're saving a program that ordinarily wouldn't be written at all.







Notes:

[1]  In some static languages you also have a reverse though slightly different problem. You can't omit the data type from a function signature. C has this problem for example; you must always declare a datatype for parameters. While Java doesn't because it has a way to make templates called generics.

Data typing can also cause problems in dynamic languages, because a division operator may lead to integer instead of decimal results unless you write more code in the function. For example, dividing in Python 2 the number 5 by 4 returns 1 instead of 1.25.

[2]  I give only a macro expanded version of this code instead of one written with functions. I saw the code would be shorter to write with macros when I was writing it, so that was the only way I wrote it.

The expanded version was prepared by running the C preprocessor `gcc -E`, running grep to remove preprocessor lines starting with #, and running indent for formatting. I used `wc -l` to count the lines of code.

[3]  Macros don't just help write programs that write themselves. In principle functions can write programs that write themselves too, but functions need more code to do that because they can't generate code using only the syntax exposed by the language.

A stronger claim is that the shortest and therefore most powerful version of a program past some size threshold can only be written with macros.

[4]  If trying to save characters seems extreme, imagine how bad it'd get if we let the number of characters grow. In a programming language without macros I once saw a variable instantiation that was 250 characters long.

[5]  In fairness, passing an operator function as a parameter here would have reduced the 3.3x-5.3x code savings presented in example 3. The savings from macros would probably be only a few lines of code. This is the key counterargument that opponents of macros present.

Why bother writing macros then if the difference is so small? Because functions still can't do examples 4, 5, and 6.

[6]  We know a function can't change the control flow of the body of a function. But it also can't change control flow to return from the function's caller: it can't jump across the stack. The code to do that can only be written in the caller, which means with functions a caller needs additional code to interpret return values accordingly. That's another source of a code size increase. Whereas a macro expands right in the caller and generates neither a call nor a return.

Using setjmp/longjmp has a similar effect: it needs more code to be written in both caller and callee. It also isn't a general solution to changing control flow. You can return to the point where setjmp was executed, but not to any other point in between the setjmp and the longjmp. Like return to a stack frame other than the one that executed the setjmp.

[7]  That it's also 2.7x code savings is a coincidence.

To be conservative I counted only code macros, not #define statements that set a global symbol to a value.

To calculate the code savings comparing the source code size to its expanded size, I prepared a version of the expanded source by running the C preprocessor `gcc -E`, manually cutting from line 1 to the first line of source code (the line that defines list_s) to remove C library definitions, ran indent for formatting, and ran `wc -l` to count the lines of code.

The expanded code used 1321 lines compared to 476 lines used as input. That's 2.7x code savings.