Call-by-macro vs. call-by-name


Call-by-macro is a simple and dumb function evaluation strategy: it directly replaces occurrences of the arguments of a function in its body with the corresponding raw expression passed as input.

This can lead to issues, as showcased by the following function:

void swap(macro int a, macro int b) { int temp = a; a = b; b = temp; } int v1 = 8; int v2 = 3; swap(v1, v2); // v1 = 3 and v2 = 8, OK

At first glance, everything looks fine. However, for some arguments, things go wrong:

int v1 = 8; int temp = 7; swap(v1, temp); // v1 = 8 and temp = 7, KO

The problem is that each occurrence of b is directly replaced with temp. As a consequence, the global variable temp gets shadowed by the local one. See for yourself:

void swap(macro int a = v1, macro int b = temp) { int temp = a v1; a v1 = b temp; // v1 is assigned its own value b temp = temp; // This assignment targets the local temp }


Call-by-name can refer to two different weird things. Here, we consider the ALGOL 60 style function evaluation strategy (the alternative is another evaluation strategy which is lazy: expressions passed as arguments are evaluated each time they are used; if they is unused, they are not evaluated at all; if they are used ten times, then they are evaluated ten times).

Call-by-name is call-by-macro's cousin who's heard of hygiene, albeit just in passing: it knows that a function's arguments and local variables shouldn't mix. Functionally, it does something equivalent to the following:

void swap(macro int a = v1, macro int b = temp) { int temp1 = temp; // Automatically generated int temp = a v1; a v1 = b temp1; b temp1 = temp; }

Of course, such a textual substitution would be inefficient in practice, but methods resulting in the same functional behavior are used. Also, an important property of call-by-name is that the value of an argument is computed only when it is required (this property also holds for the other meaning of call-by-name and for call-by-macro) — the above example respects this property as the value of argument b is always computed and also always required, but keep it in mind.

So everything is good then? No:

int id = 1; int arr[3] = {5, 2, 3}; swap(id, arr[id]); // id = 2 and arr = {5, 2, 1}, KO

In slow-mo:

void swap(name int a = arr[id], name int b = id) { int temp = a id (= 1); a id = b arr[id] (= arr[1] = 2); b arr[id] (= arr[2]) = temp (= 1); }

Some conflicts still exist when using related inputs. In fact, with call-by-name, it is not possible at all to write a swap procedure which is correct for all inputs.

However, call-by-name's strange behavior makes some neat interesting tricks possible, such as Jensen's device.

Parting remarks

Note that most of our remarks concern impure contexts. These strategies felt a bit dirty, didn't they? Well, in pure enough contexts, such non-strict evaluation strategies are perfectly fine ("strict" means "such that each expression passed as argument is always evaluated once"). See Haskell style call-by-need for instance!

Both of these strategies share similarities with that-other-form-of-call-by-name in pure contexts. The thing is, pure functional languages evaluate the expressions at the scope where the arguments are defined and not at that where they are used. Does something block pure functional languages from doing things ALGOL 60 style (besides common sense, that is)?

The bottom line is, you really need to get rid of impurity to get non-strict evaluation strategies to behave decently.