10.11.6 Composing Patterns in Rewrite Rules

There are three operators, ‘&&&’, ‘|||’, and ‘!!!’, that combine rewrite patterns to make larger patterns. The combinations are “and,” “or,” and “not,” respectively, and these operators are the pattern equivalents of ‘&&’, ‘||’ and ‘!’ (which operate on zero-or-nonzero logical values).

Note that ‘&&&’, ‘|||’, and ‘!!!’ are left in symbolic form by all regular Calc features; they have special meaning only in the context of rewrite rule patterns.

The pattern ‘p1 &&& p2’ matches anything that matches both p1 and p2. One especially useful case is when one of p1 or p2 is a meta-variable. For example, here is a rule that operates on error forms:

f(x &&& a +/- b, x)  :=  g(x)

This does the same thing, but is arguably simpler than, the rule

f(a +/- b, a +/- b)  :=  g(a +/- b)

Here’s another interesting example:

ends(cons(a, x) &&& rcons(y, b))  :=  [a, b]

which effectively clips out the middle of a vector leaving just the first and last elements. This rule will change a one-element vector ‘[a]’ to ‘[a, a]’. The similar rule

ends(cons(a, rcons(y, b)))  :=  [a, b]

would do the same thing except that it would fail to match a one-element vector.

The pattern ‘p1 ||| p2’ matches anything that matches either p1 or p2. Calc first tries matching against p1; if that fails, it goes on to try p2.

A simple example of ‘|||’ is

curve(inf ||| -inf)  :=  0

which converts both ‘curve(inf)’ and ‘curve(-inf)’ to zero.

Here is a larger example:

log(a, b) ||| (ln(a) :: let(b := e))  :=  mylog(a, b)

This matches both generalized and natural logarithms in a single rule. Note that the ‘::’ term must be enclosed in parentheses because that operator has lower precedence than ‘|||’ or ‘:=’.

(In practice this rule would probably include a third alternative, omitted here for brevity, to take care of log10.)

While Calc generally treats interior conditions exactly the same as conditions on the outside of a rule, it does guarantee that if all the variables in the condition are special names like e, or already bound in the pattern to which the condition is attached (say, if ‘a’ had appeared in this condition), then Calc will process this condition right after matching the pattern to the left of the ‘::’. Thus, we know that ‘b’ will be bound to ‘e’ only if the ln branch of the ‘|||’ was taken.

Note that this rule was careful to bind the same set of meta-variables on both sides of the ‘|||’. Calc does not check this, but if you bind a certain meta-variable only in one branch and then use that meta-variable elsewhere in the rule, results are unpredictable:

f(a,b) ||| g(b)  :=  h(a,b)

Here if the pattern matches ‘g(17)’, Calc makes no promises about the value that will be substituted for ‘a’ on the righthand side.

The pattern ‘!!! pat’ matches anything that does not match pat. Any meta-variables that are bound while matching pat remain unbound outside of pat.

For example,

f(x &&& !!! a +/- b, !!![])  :=  g(x)

converts f whose first argument is anything except an error form, and whose second argument is not the empty vector, into a similar call to g (but without the second argument).

If we know that the second argument will be a vector (empty or not), then an equivalent rule would be:

f(x, y)  :=  g(x)  :: typeof(x) != 7 :: vlen(y) > 0

where of course 7 is the typeof code for error forms. Another final condition, that works for any kind of ‘y’, would be ‘!istrue(y == [])’. (The istrue function returns an explicit 0 if its argument was left in symbolic form; plain ‘!(y == [])’ or ‘y != []’ would not work to replace ‘!!![]’ since these would be left unsimplified, and thus cause the rule to fail, if ‘y’ was something like a variable name.)

It is possible for a ‘!!!’ to refer to meta-variables bound elsewhere in the pattern. For example,

f(a, !!!a)  :=  g(a)

matches any call to f with different arguments, changing this to g with only the first argument.

If a function call is to be matched and one of the argument patterns contains a ‘!!!’ somewhere inside it, that argument will be matched last. Thus

f(!!!a, a)  :=  g(a)

will be careful to bind ‘a’ to the second argument of f before testing the first argument. If Calc had tried to match the first argument of f first, the results would have been disastrous: since a was unbound so far, the pattern ‘a’ would have matched anything at all, and the pattern ‘!!!a’ therefore would not have matched anything at all!