Controlling Evaluation

One of the characteristics of Mathematica and other computer algebra systems that educators sometimes find frustrating is the fact that these systems generate results without showing intermediate steps. Leibniz addresses this problem by giving the user very fine control over just what part of an expression gets modified and how drastically the expression gets modified in a single step.

By selecting only a portion of an expression to work on, or by dragging and dropping within a larger expression, the user gets to focus the calculation. Leibniz supports this focusing behavior by sending just the relevant part of an expression to the kernel. When the result comes back, its gets swapped into place in the larger expression it came from, and that expression is then formatted and displayed.

Even with this focusing behavior, special care is needed to avoid unwanted evaluation. Consider our seemingly innocent example of a rule for moving a term from one side of an equation to the other.

This seems straightforward enough, but what happens when, say, `b` is an integral? The user's intention will be to subtract that integral from both sides without evaluating it, but Mathematica's default evaluation mechanisms will cause `b` to be evaluated, thus making the integral disappear after subtracting it from both sides. One obvious solution would be to give `LZShuffle` the attribute `HoldAll` and replace the result above as follows.

This solves the problem of the unwanted evaluation, but then causes the expression to fail to evaluate correctly when the user wants it to. For example, if `c` were 3 and `b` were 2, the user would expect to be able to subtract 2 from both sides of the original equation giving a new right-hand side of 1. However, with both `c` and `b` held as shown here, the user would see a new right-hand side of the unevaluated expression .

The lack of control offered by the standard evaluation mechanisms and the `Hold` mechanism make it necessary to find a more refined strategy for controlling evaluation.

The first element of this strategy is to force every expression that the system works with to be a purely symbolic expression. This is done by replacing all conventional mathematical functions such as `Plus`, `Integrate`, `D`, and so forth with purely symbolic analogs called `LZPlus`, `LZIntegrate`, `LZD`, and so on. Thus, rather than rendering the expression `a+b` as `Plus[a,b]`, the Leibniz parser passes the expression to the kernel as `LZPlus[a,b]`.

With a little bit of care, we can do pattern matching with these new expressions just as easily as we could with more conventional expressions. For example, by giving the function `LZPlus` the attribute `OrderLess`, we can write pattern matching rules for `LZPlus` without regard to the ordering of terms involved.

Because none of these new functions come with evaluation rules, everything gets held by default. To release the implicit hold, `LZCompute` uses a couple of mechanisms. The first mechanism releases the hold partially so that the usual evaluation rules can perform obvious algebraic simplifications.

Thus, the `LZShuffle` rule shown above becomes the following.

This solves both of the problems described earlier. If `c` is 3 and `b` is 2, the right-hand side will correctly evaluate to 1. If `b` is an integral, `LZPEval` will leave it unevaluated in the `LZIntegrate` form.

The second evaluation mechanism used is a complete evaluation mechanism, which converts every function in an expression from its LZ form to its conventional form. This mechanism is implemented via a function called `LZEval`.

The `LZRelease` function maps each individual LZ function to its conventional equivalent. For example, here is part of the definition of `LZRelease`.

This hold and release mechanism provides other benefits, as well. Because `LZRelease` has to map the held forms to their conventional equivalents, we can take advantage of the need to do that mapping to provide more sophisticated handling for certain special cases. The best example of this is matrix multiplication. Experienced Mathematica programmers know that if we try to compute the product of a two by two matrix and a column vector by naively writing

we get the incorrect result

If we remember to use `Dot` instead of `Times`, everything works out just fine. Leibniz solves this problem for the user by using `LZTimes` for everything that looks like a product and then relying on the release mechanism to sort out the details. `LZRelease` uses some simple and straightforward pattern matching to ensure that `LZTimes` maps to the appropriate equivalent.