Class function revisited: IMHO we are badly confused about two concepts here
Richard B. Kreckel
kreckel at thep.physik.uni-mainz.de
Sun Jun 24 15:48:46 CEST 2001
Dear all,
In the meeting last friday I was announcing that we could easily have
functions generated on the fly (as demanded by some people for doing
clever tricks with manual cancellations of divergenct subterms). The
issues involved were partly those raised by Roland Richter in
<http://www.ginac.de/lists/ginac-list/msg00073.html>. For those that
were at the meeting: scratch my claim that it could easily be done.
The idea was this: Have a functor class with an overloaded operator()
to return the corresponding object of class GiNaC::function. There
would have to be a number of such functors, all derived from a base
class, say `fncall', with the proper argument number, basically:
class fncall {};
class fncall_1 : public fncall {
public:
const function operator()(const ex & x1) const { return function(index, x1); }
private:
unsigned index;
};
Remember, that objects of class function (we could call them
pseudofunctions) are distinguised by the sequence of arguments they
hold and their serial number. All we *want* is a clean way of writing
`sin(x)', `x' being an arbitrary `ex'. Right now, this is being done
by declaring a global function with the right function name:
const function sin(const ex & x) { return function(index, x); }
This is just a sort of entrance into the GiNaC pseudofunctions. The
functor per se ideally suited to be another such entrance.
Pseudofunctions are registered at program startup time. This is when
their name, eval function, diff function etc. are set up in a static
std::vector<GiNaC::function_options>, subscripted by a plain unsigned
which happens to correspond exactly to the index. In order to do this
dynamically in a clean way, we would really have to abandon this
static vector and instead set the data up in a std::map<unsigned,
GiNaC::function_options>. Note that lookup-time in such a structure
is not constant but rather logarithmic, however with a rather high
offset penalty (since internally a map is a RB-tree which has left and
right node pointer and an additional color state). It can still be
done without any visible impact on efficiency.
So, the new gateway into the pseudofunctions would defined as such:
const fncall_1 sin = fncall_1(function::register_new(...... ));
which still does not look very much different from the setup macro we
use now. We can then still write sin(x), but now
fncall_1::operator()(const ex &) is being called. It worked okay
until I tried to roll this code in, when an old friend problem
reappeared: The conflict of above object with this function:
const numeric sin(const numeric & x);
The most obvious solution out of this would be to remove these and
instead add an
const numeric operator()(const numeric &);
to our fncall_1 class. However, the class cannot know which
arithmetic function the object `sin' is supposed to call! We would
have to set up the arithmetic function at construction of the `fncall'
object. It could be done with passing a pointer to such a function.
That pointer would then have to be stored twice: Once in the
pseudofunction object corresponding to `sin' as its `evalf_funcp'
(because class function can never call the object's `operator()(const
numeric &)' -- the class function cannot know about it's programmatric
entrance `sin'. And once again in the functor object `sin', so that
the operator looks something like this:
const numeric operatir()(const numeric &x) {
if (arithmetic_funcp!=0)
return arithmetic_funcp(x);
throw (std::runtime_error("buaaaaaaahhhh"));
}
I hope everyone agrees that this is just crap. The problem is that
the class doesn't know exactly what the object is supposed to do. So,
alternatively, we could move that information directly inside the
class by declaring one entrance class per entrance object, as such:
class fncall_sin : public fncall {
public:
const function operator()(const ex & x) const {
return function(index, x);
}
const function operator()(const numeric & x) const {
return _numeric_sin(x);
}
private:
unsigned index;
};
In order to make this really sematically reasonable, however,
fncall_sin would have to be a singleton class. Ugh! The way people
define their own classes also becomes quite messy. So this isn't good
either. The basic problem is always that we are mixing functions and
pseudofunctions (objects of class function) in a non-orthogonal way.
We could choose to disentangle the pseudofunctions from the arithmetic
functions. There are really two different concepts here: Once, the
symbolic pseudofunctions that may eventually `.hold()' and second the
ones that are supposed to be doing something straightforwad, like
mapping from numeric to numeric. We could wrap the latter ones into a
namespace `arithmetic' sitting inside the overall namespace `GiNaC'
and disambiguate the calls by using declarations. All functions which
could ever make sense to be `.hold()' should be handled like this.
This may include real(numeric) and imag(numeric) though they do not
(yet?) have a corresponding real(ex) and imag(ex) but it would not
include gcd(numeric, numeric) since that will never by treated as a
symbolic function. I strongly invite you to look at the declarations
in <ginac/numeric.h>: It seems to me like there is a very clear cut
between normal functions and such functions that may eventually lead
to collisions with pseudofunctions!
This seems to work so far and I really start to like the idea to some
extent. However, there this is still not entirely satisfactory.
There are two problems here, one is a permanent one, another one is a
temporary one. First the temporary one:
#include <ginac/ginac.h>
using namespace std;
using namespace GiNaC;
int main(void) {
symbol x("x");
cout << sin(x) << endl;
return 0;
}
Alas, we cannot do this yet. It breaks GCC-2.95.2 which treats `sin',
`cos', etc. as mysterious internal entities:
<internal>:6: first declared as `double sin (double)' here
GiNaC/ginac/inifcns.h:41: also declared as `const GiNaC::fncall_1 GiNaC::sin' here
The three-times-cursed GCC-2.96 is affected as well, GCC-3.0 and SGI's
compiler on IRIX (which I love!) handle this one correct.
Now the permanent problem:
#include <cmath>
using namespace std;
#include <ginac/ginac.h>
using namespace GiNaC;
int main(void) {
symbol x("x");
cout << sin(x) << endl;
return 0;
}
This will always leave us with ambiguous usages of `sin' because the
object clashes with the function definition in <cmath>. It will, of
course, also also clash if a using directive for GiNaC::arithmetic is
specified. This can never work well with using directives, only with
using declarations like using GiNaC::sin; by the language rules.
To summarize: In my opinion, the whole confusion is caused by the fact
that we hazardously mix functions with our concept of pseudofunctions.
As long as we do not steer clear of the potential clashes triggered by
this, we are going to have trouble. We should really start thinking
about these two as totally different entities. Functions are well
suited for arithmetic mappings, functors are excellently suited as
entrances to our pseudofunctions, making them more flexible. But we
must not mix these concepts and the language forces us to not mix
their names. In the long run, we could consider making all entrances
to pseudofunctions uppercase, thus writing Sin(x) to distinguish
between these two concepts, but that will require some discussion.
[Remember that GiNaC is Not a CAS and the wish to write sin(1.2) to
return 0.932 and sin(x) to return sin(x) is conventional in CAS but we
should be allowed to raise the question if such a wish can really be
justified as orthogonal to system design.]
Okay, and if anybody has another suggestion or a comment or a wish,
I'ld very much like to hear about it.
Cheers
-richy.
--
Richard Kreckel
<Richard.Kreckel at Uni-Mainz.DE>
<http://wwwthep.physik.uni-mainz.de/~kreckel/>
More information about the GiNaC-devel
mailing list