Menu

Macros

Alfred Steffens Jr

Macros

A macro is an alternative to defining a function. It's purpose is to automate certain actions that occur with recursive processing. For example, the following macro does nothing but count from 1 to some number N,

macro M1
    first : 1
    += 1
end_macro

The macro is executed with the mexec function call which takes the macro name and the number of iterations as arguments,

n1 = mexec(M1, 3);

In the above macro the first statement initializes the macro state to 1. The second statement increments the macro state by 1. The return value from the macro is the value of the macro state after the last statement of the macro block, after its last iteration. So the above mexec call should return 3.

The macro is a kind of state machine. Each statement, or "clause", has a state value which is the calculation result of that clause. In addition, the macro itself has an ongoing state value that is updated after each clause is executed. The macro state value will simply be overwritten by each clause unless a special operator is used to accumulate the on going calculations. In the example, there is a += operator (plus equals) which means 'add the result of this clause to the last value of the macro state.

One iteration is counted after every clause in the macro block has been executed. The exeception would be a first, second, third or nth clause. The FIRST clause in the above example is executed only once, and the FIRST clause is itself counted as one whole iteration. During subsequent iterations the FIRST clause is skipped.

In the following example the macro adds the first N natural numbers, that is, 0 + 1 + 2 + 3 + ... + N-1.

macro M2
    += @0
end_macro

Notice the @0 symbol in the macro's only clause. The @0 symbol is the "up counter" that counts the number of iterations the macro has executed. The up counter is equivalent to a loop index in a for loop or while loop. It is automatically initialized to 0, and after each iteration it is automatically incremented by one. Think of @0 as k = 0, 1, 2, ..., N.

Another way to count the first N numbers is with the down counter. The following example counts 4 + 3 + 2 + 1.

macro M3
    += %0
end_macro

In this example, the %0 symbol is the down counter. Think of %0 as j = N, N-1, N-2, ..., 1.

In the first iteration the value of the upcounter, @0, will be 0, and the value of the down counter, %0, will be N (N given in the mexec call). During the last iteration the value of the up counter will be N-1, and the value of the down counter will be 1.

The result of each clause calculation becomes the state value of the clause until the next iteration. The macro itself has an "ongoing" state value that is updated after each clause is executed. The macro's state value will become the value of the clause just executed unless an accumulation operator is used. But the macro also has a state value that is updated after each complete iteration. In the following example the end-of-iteration state value is summed. In other words, it adds to itself its last value.

macro M6
    first : 1
    += &0
end_macro

The symbol &0 is the state value of the macro saved at the end of its last iteration. The following table shows the end-of-iteration state value of the macro at the end of each iteration.

iteration result
1 1
2 1 + 1 = 2
3 2 + 2 = 4
4 4 + 4 = 8
5 8 + 8 = 16

So, calling mexec with N = 5 as the number of iterations would return 16.

To obtain the saved values of the macro before the last iteration, the &0 symbol is used but with a number appended. So, &01 is the value of the macro before the last iteration, and &02 was the value of the macro before that, and so on.

The &1 symbol is used to save the difference between the last iteration and the iteration before that. In other words, &1 = &0 - &01. This last difference symbol is used internally by the macro to monitor convergence. To make use of the convergence feature the mexec function must be called with a precision parameter as the termination condition in place of the number of iterations. The mexec call will interpret the termination parameter as a convergence limit if the value is in the range 0 to 1. Otherwise, it will be truncated to an integer and interpreted as the number of iterations.

At any line (clause) in the macro the value from previous lines can be accessed with the previous line symbol, which is an underscore followed by an integer. The _1 symbol gets the value calculated on the previous line, _2 gets the value from two lines above, and so on. The number is relative to the current line. Note that the previous line symbols literally get the value from that line regardless of the sequence of execution. This is sometimes called "lexical," meaning that it points to a lexical position in the definition, not the previous clause executed. Consider the following macro.

macro M9
    first : 1
    += _1
    second : 2
end_macro

The first iteration of the macro executes the FIRST clause only, which will have a value of 1. On the next iteration the SECOND clause is executed only, which will have a value of 2. Begining on the third iteration the line with += _1 will be executed. Since the FIRST and SECOND clause are only executed once each, the remaining iterations of the macro will execute only the line with += _1. The _1 symbol gets the value of the clause one line above, which is the FIRST clause. But the value in the FIRST clause is always 1. The following table shows the state values of the macro for three iterations.

iteration last state clause state per clause macro state
1 0 1 1
2 1 2 2
3 2 2 + 1 3

Observe that on the third iteration the calculation takes the last state (the result of the SECOND clause) and adds to it the value from the above line (the result of the FIRST clause) with a per clause macro state of 3, which is also the per iteration macro state. If the third iteration is the last one, the return value will be 3.

Each clause can execute a few primitive arithmetic operations. The rule is that there can be one additive-type operator (+, -) and one multiplicative-type operator (*, /, ^) in an expression. The limited expression resembles a linear equation such as a*x + b, whereas the multiplication might be replaced with a division or an exponentiation.

The following example calculates the square root of 2 using the iterative Newton method, x = x/2 + 1/x,

macro M10
    first : 1       #  trial value of 1
    &0 / 2          #  x/2
    += 1 / &0       #  add 1/x
end_macro

n10 = mexec(M10, 3);

Although the macro in this example was executed for a fixed number of iterations, it could have be run with a precision parameter, say 0.0001.

n10 = mexec(M10, 0.0001);

Since the last clause returns the new trial value, which is also the end of iteration state value, the macro will compare the last clause value with the previous end of iteration value and stop if the absolute value of the difference is less than the precision parameter.

The macro has an ongoing state value that is recalculated after each clause is executed, as well as the end-of-iteration state value. The per clause
macro state is accessed with the $0 symbol. The following example calculates the square root of 2 using the per clause state value.

macro M11
    first : 1
    inv : &0        #   1/x
    &0 / 2  + $0
end_macro

In this example the $0 symbol accesses the per clause macro state of the preceding line, which is 1 / x. In this instance the macro value, $0, is equal to the clause value, _1, of the preceding line. But that is not always true. If the clause contains an accumulation operator, like +=, then the $0 symbol gets the accumulation of the previous two lines whereas the _1 gets the clause result only.

Iteration comes in two types: looping and recursive function calls. In its basic form the macro does looping by default. One might observe that the macro is just a special form of do loop, except that for the added benefit of the automatic counters. The following table shows how to access its variations.

counter count offset
@0 k = 0, 1, 2, ..., N-1
@1 k + 1
@2 k + 2
@3 k + 3
. .
@01 k - 1
@02 k - 2
@03 k - 3
. .
%0 j = N, N-1, N-2, ..., 1
%1 j + 1
%2 j + 2
%3 j + 3
. .
%01 j - 1
%02 j - 2
%03 j - 3

The macro can either do looping or recursively call itself, but not both. The macro will execute in recursive mode if there exists an REXEC statement in the macro, otherwise it will run as a loop. There can only be one rexec statement in the macro.

The most common example used to illustrate recursive function calls is the factorial of a whole number, N: F(N) = N * N-1 * N-2 * ... * 1. The following example is a macro that calculates the factorial of N.

macro M14
    last : 1
    rexec
    *= %0
end_macro

n14 = mexec(M14, 4);

The number of iterations parameter to mexec specifies the whole number N. So the macro uses the down counter, %0, to access N, N-1, etc. A recursive macro must have a LAST clause, which is the value the macro returns when it reaches its inner-most recursive call. Each time the macro calls itself with the rexec clause the iteration counters advance: the up counter increments, and the down counter decrements.

For recursive iteration the counters work differently than when iterating a loop. When REXEC is executed the counters advance, but when REXEC returns the counters are restored to what they were before the REXEC call. The counters represent how deep into the nested recursion the macro is now running. The counters advance immediately prior to the REXEC call. The first thing that happens in the nested call is that the termination condition is checked. Either the down counter has reached zero, or the precision condition, specified by a TERM clause, is met. If the termination condition is true the macro will execute the LAST clause only and return. Otherwise, the any clause that precede the REXEC clause will be executed until the rexec clause causes the next nested call.

The state values of the clause and the macro are not updated until the REXEC call returns. So the precision termination method cannot be used, because the state value will have never been updated. This is a general feature of nested recursion algorithms. The inner-most level of the recursion is executed first, and the outer-most expression is last.

In the factorial macro example above the the first clause to be executed is REXEC. The LAST clause is only executed when the macro reaches the inner-most level. The counters are advanced, and macro calls itself. Again, it finds the first clause is REXEC. Nothing happens except for the counters advancing until REXEC finds that the down counter is zero, at which point it executes the LAST clause which evaluates to 1. REXEC returns 1 and then the next clause is evaluated, which takes the current macro state, 1, and multiplies it by the current down counter, which is 1. REXEC returns again, and the next clause is executed, which multiplies the current macro state, 1, by the current down counter, which is 2. After each REXEC return the down counter moves up a step. After the outer-most REXEC call the down counter is N, and the last clause in the block completes the factorial calculation.

Another example of using REXEC in a macro is an elementary Taylor series calculation of the geometric series, represented as a nested recursion:

1 + x + x^2 + x^3 + ...  =   1*(1 + x*(1 + x*(...)

macro M16
    last : 1 + $1
    rexec : $1
    1 + $1 * &0
end_macro

n16 = mexec(M16, 3, 0.5);

Notice that we can specify the value of x in f(x) by providing a parameter to the mexec function call. The $1 symbol represents the first parameter that comes after the two required arguments.

A macro may call a limited number of math functions, such as SIN, COS, TAN, ASIN, ACOS, ATAN, LOG, LOG10, EXP, EXP10. A function call is written with the name of the function followed by a colon followed by one or more arguments. For a standard math function such as SIN there is only one argument, and the clause appears as, for example,

sin : 1.31

where the argument can be any valid expression. If the function requires more than one argument, like stirling1 and stirling2, the arguments are separated by commas.

stirling1: @0, @01

Conditional functions, the equivalents of IF and ELSE, are included as BELOW, ABOVE, INSIDE, OUTSIDE, and TRUE. The functions BELOW, ABOVE, INSIDE and OUTSIDE evaluate to 1 if the result is true, otherwise 0. The following table illustrates the use of these functions.

Function call Result Condition
below: -1, 0 1 -1 < 0
below: 1, 0 0 1 < 0
above: 3, 2 1 3 > 2
above: 0, 2 0 0 > 2
inside: 0, -1, 1 1 -1 <= 0 <= 1
inside: 3, -1, 1 0 -1 <= 3 <= 1
outside: 3, 1, 2 1 3 < 1 or 3 > 2
outside: 1.5, 1, 2 0 1.5 < 1 or 1.5 > 2

The function TRUE evaluates the first argument and returns the second argument if true else the third argument. This is analogous to the C language expression

x > y ? 1 : 0

which means if x is greater than y then the result is 1, else 0. To see the TRUE statement in its simplest form we could write

true : 1, 7, 5

which would return 7. That is, if the first argument evaluates to nonzero return the second argument, else the third argument. The TRUE function will usually be used in conjunction with one of the conditional clauses. Its first argument will usually be an input argument line $1, a counter, or a state reference.

When the macro arguments, counters, or state references are not enough for a computation there is also a "stack" buffer than can be used to store numbers. To save a number on the stack you would use the PUSH function. For example,

push:  &0
push : _1

To retreive numbers from the stack you reference the _A, _B, _C, ... symbols. The _A reference gets the last value that was pushed. The _B reference gets the next-to-last number pushed, and so on. There is no associated POP function that you would find for a true stack in a processor. Numbers are continually "pushed" onto a circular buffer which, when it finds the end starts overwriting at the beginning. The clause value resulting from a PUSH function is just the number that was pushed.


MongoDB Logo MongoDB