Contents
Next

Maxima Programming


Programming in Maxima has two very different aspcets:

The use of the Maxima programming language is highly recommended for serious work. Programming with Lisp is work for advanced users. Occassionally, it may be necessary to use both programming languages to solve a problem. The use of files is a candidate for bilingual programming.


A simple Maxima program is nothing but a sequence of Maxima commands. To be able to reproduce a lengthy computation at a later time, it is often prudent to record its commands in a batch file. Such a protocol file is in fact a Maxima program.

In the chapter about Tubes around Spacial Curves we used this definition to compute the tangent of a spacial curve:

tangent(fn, x) :=
       diff(fn(x), x) / ((diff(fn(x), x) . diff(fn (x), x))^(1/2))$

This is a straightforward definition, but it has a serious flaw: It computes the derivative of f three times. It would be much better to compute the derivative only once and to store it for later use.

tangent(fn, x) :=
  block ( [df: diff(fn(x), x)],
           df / (df . df)^(1/2)
        )$

Explanations:


The Structure of Expressions

An expression is either an atom or a structure that is built with an operator and a list of arguments. The function op is used to access the operator of an expression, the function args is used to obtain a list of all arguments. Some examples explain this:

 op (a + b + c);
    +
 args(a + b + c);
    [c, b, a]
 op (sin(3*x));
    sin
 args (sin(3*x));
    [3 x]
 op (f(a, b, g(c), d));
    f
 args (f(a, b, g(c), d));
    [a, b, g(c), d]

Note that exp(x) is translated into a power to the constant %e:

 op(exp(2*x));
  ^
 args(exp(2*x));
  [%e, 2 x]

A Function that collects the Operators of an Expression

A non-atomar expression contains always exactly one operator and a collection of arguments that are in turn expressions. An expression is thus a tree. It is sometimes helpful to traverse that tree and to collect all operators. The knowledge of all used operators can be used to select promising simplifications and to skip simplifications that are obviously not needed in the absence of some operators.

This section explains how such a function is programmed.

Outline of the desired Solution

We want a function that takes an expression and answers a list with all operators of the expression:

allOps(a);
[]
allOps(sin(x) + cos(x));
[cos, sin, +]
allOps(sin(x) + 2*sin(2*x));
[sin, *, +]

The answered list should contain every found operator once. Repetions are not desired. The last expression exemplifies this requirement.

The function allOps is very simple: It delegates the traversal of the expression to a function that takes a subexpression and a list of operators that were found so far:

allOps(expression):=
 block( [ ],
        allOpsPriv (expression, [])
       );

The function allOpsPriv requires more work. A first attempt handles only the simple cases:

  /* incomplete preliminary version  */
allOpsPriv(expression, opList) :=
 block ( [x],
        if atom(expression)
           then opList
           else
             (x: op(expression),
              cons(x, opList)
             ) 
        );

This definition handles two simple cases:

The definition uses an if-statement to identify and to handle both cases. An if-statement always answers the value of its evaluated alternative as its result. For an atomar expression, the above if-statement answers the value of opList. The else-branch of the if-statement contains two statements that are written in parentheses. The result of cons(x, opList) is the result of the entire if-statement when the else-branch is selected for evaluation.

Note that the definition of allOpsPriv uses a temporary variable to store the operator of a nonatomar expression. This is not really necessary, it is also possible to write:

cons(op(expression), opList)

When we try the definition that we have now, we obtain:

allOps(a);
[]
allOps(sin(x) + cos(x));
[+]

The inspection of the subexpressions is obviously missing. For a complete solution we have to:

A refinement of this definition requires additional actions in the else-branch of the if-statement.

Added code is displayed in bold:

allOpsPriv(expression, opList) :=
 block ( [x, args, newList],
        if atom(expression)
           then opList
           else
             (x:    op(expression),
              args: args(expression),
              newList: cons(x, opList),
              for arg in args do
                newList: allOpsPriv(arg, newList),
              newList
             ) 
        );

Here we use the language element

for arg in args do
   <statement>

which iterates over all elements arg in the list args. Note that the variable arg is not declared as a block-local variable: The declaration of that variable is implicit and its scope is the statement that is evaluated under by the iterator.

A quick test reveals both remarkable improvements and an imperfection of the algorithm:

allOps(a);
[]
allOps(sin(x) + cos(x));
[cos, sin, +]
allOps(sin(x) + 2*sin(2*x));
[sin, *, sin, *, +]

The operators sin and * are mentioned twice. This is not really a surprise, since we do not check whether an operator is already in the list before we add it. It is however easy to fix that:

allOpsPriv(expression, opList) :=
 block ( [x, args, newList],
        if atom(expression)
           then opList
           else
             (x:    op(expression),
              args: args(expression),
              newList: if member (x, opList)
                          then opList
                          else cons(x, opList),
              for arg in args do
                newList: allOpsPriv(arg, newList),
              newList
             ) 
        );

You find this code in file allOps.mc. When you copy that file into the subdirectory user of your Maxima installation, you can load it with

batch("allOps");

The function allOps collects all operators of an expression. To do that, it has to completely traverse an expression. For a similar problem complete traversal is not allways needed:

When we are looking for the presence of a given operator in an expression, we can stop traversal as soon as we have found the operator we are looking for.

containsOp(expression, opList) :=
 block ( [x, args, found],
        if atom(expression)
           then false
           else
             (x:    op(expression),
              args: args(expression),
              if member (x, opList)
                 then true
                 else 
                  (found : false,
                   for arg in args while not found do
                        found: containsOp(arg, opList),
                   found)
             ) 
        );

Here we use a variant of the earlier used language element for arg in args do:

for arg in args while not found do
   <statement>

which iterates over the elements arg in the list args for as long as the condition holds. Iteration terminates when the condition becomes nil or when all list elements are processed.

Contents
Next