Why -x^(-1) and -1/x are not the same in Yacas

Wouldn't it be wonderful if we had a program that could do all the mathematical problems for us we could ever need? Need to solve a set of equations? Just call Solve with the appropriate arguments. Want to simplify an expression? Just call Simplify and you will always get the form you would like to see. A program, simply, that could replace any mathematician. An expert system, the domain of expertise being mathematics. Wouldn't that be great?

The answer to the above is, at least according to the author, a resounding no. It is doubtful such a program will ever exist, but it is not even sure that such a program would be desirable.

Humans have a long history of making tools to make their lives easier. One important property of a tool is that it is clear conceptually to the user of that tool what that tool does. A tool should not be clever. The user of the tool can be clever about using the tool, or combining it with other tools. A 'clever' tool often results in a tool that is not useful. It is hard to understand what a clever tool does, or why it does what it does. In short: its behavior will be unpredictable.

This is a passionate plea against generic commands like Simplify and Solve.

Consider this bit of interaction in Yacas:

In> a:= -x^(-1)
Out> -x^(-1);
In> b:= -1/x
Out> (-1)/x;
In> a = b
Out> False;

Now, that can not be right, can it? Clearly, these are the same? No, they are not. They have a slightly different form, and are thus represented differently internally. the = sign compares the internal representation of two expressions , so a = b returns False because the internal representations of the expressions a and b are bound to are different. Note that this is behaviour that is simple to explain. The = operator is a 'tool', it is simple, and does one thing but does it well. It is easy to use, an important property of a tool.

To drive home this point further, suppose we did modify the = operator to detect that a and b are indeed equal. Great! Wonderful! a=b now returns True. But consider

In> c:=1+r+r^2
Out> r+r^2+1;
In> d:=(1-r^3)/(1-r)
Out> (1-r^3)/(1-r);
In> c=d
Out> False;

c and d are equal for all values of r where r != 1, but there a limit can be taken:

In> Limit(r,1)d
Out> 3;
In> Limit(r,1)c
Out> 3;

Now, we have to modify the = tool to also detect that these are the same. Actually, this will have to be done for all known identities! Or we shall have to explain for which expressions it can determine equality. This will be a complex story, it will be hard to explain. It will be a complex tool to use. And, more practically, it will be a slow tool.

So, how do we go about verifying that a and b are the same? Or that c and d are the same?

The solution lies in devising new tools.


Canonical and normal representations

A canonical representation for a group of expressions is a representation for each object in that group such that if two elements of the group are the same, they also have the same (internal) representation. Thus, when expressions are brought to their canonical representations, the = tool can be used to verify that they are the same.

A representation is called a normal representation if zero only has one representation. Thus nf(a-b)=0 should be something that should return True if a and b are the same mathematically.

Consider a normal form defined on rational functions:

In> MM(a)
Out> MultiNomial({x},{{-1,-1}});
In> MM(b)
Out> MultiNomial({x},{{0,-1}})/
MultiNomial({x},{{1,1}});

However:

In> MM(a-b)
Out> MultiNomial({x},{{0,0}})/
MultiNomial({x},{{1,1}});
In> NormalForm(%)
Out> 0;

So here we have found a combination of tools that together allow us to decide that the a and b defined in the beginning of this section are the same: convert a-b to a normal form of a-b, and verify with the = tool that they are the same:

In> NormalForm(MM(a-b)) = 0
Out> True;

Now consider the c and d defined above. c and d are both functions of r only, c=c(r) and d=d(r). Now, let us define a function f(r)=c(r)-d(r):

In> f(r):=Eval(c-d)
Out> True;
In> f(r)
Out> r+r^2+1-(1-r^3)/(1-r);

It is not quite clear yet that this is zero. But we can decide that this is zero (and thus c(r)=d(r)) by first noting that f(r) is zero for some r, and then that the first derivative of f(r) with respect to r is zero, independent of r:

In> f(0)
Out> 0;
In> D(r)f(r)
Out> 2*r+1-(-((1-r)*3*r^2+r^3-1))/(1-r)^2;
In> NormalForm(MM(%))
Out> 0;

So here we have avoided bringing c and d to canonical forms, by for example first discovering that c is a geometric series, and gone straight to detecting that c-d is in fact zero, and thus c(r)=d(r).

Here again we have combined tools that are simple, do one thing but do it well, and for which it is easy to understand for human beings what they do.


But how can we then build a powerful CAS?

A new problem is introduced when algorithms are written down that require more powerful comparison tools, tools that are more sophisticated than the = tool for detecting that two expressions are indeed the same. The solution to this is to write the algorithm, but leave the actual comparison tool to be used by the algorithm configurable. This makes algorithms more flexible: the comparison operator can be passed in as an argument, or the algorithm can perhaps detect to which group its arguments belong, and use the appropriate tool to detect equality between two expressions.


Conclusion

A CAS (or any other system built to be used by humans for that matter) should be built up from small, well understood building blocks. Yacas contains hundreds of functions that can be combined into more powerful algorithms. These tools are documented in the documentation that comes with Yacas. Yacas solves the problem in that way. Let the user be smart, and choose the tools he needs based on understanding what the tools do. Large, complicated, cumbersome calculations can be done that way by just using well understood tools and combining them appropriately.