On mathematical notation

Arjen Markus (7 september 2005) The other day Lars Hellström sent an email to a discussion on the Tcllib developers' list, regarding the extensibility of the [expr] command.

I found this "essay" rather illuminating, so I took the liberty of putting it in its entirety on this page. For an implementation of these ideas, see Vector spaces.

My apologies if the following is a bit long, but it concerns a matter I have often thought about, and since a neighbouring matter came up in the discussion last week I might just as well put forth my findings on the matter.

At 21.39 +0200 2005-08-30, Techentin, Robert W. wrote:
>Andreas Kupries
>> However the only extensibility hooks which are exposed are
>> math functions. Operators are whole different kettle of fish.
>> I haven't looked at the parser code, but nevertheless believe
>> that the operator priorities are hardwired.
>
>I wasn't thinking about adding new operators or priorities. Just redefining
>existing operators so that they know how to handle a new "data type." The
>BLT vector expression, for example, does different things for "double +
>double" and "double + vector." Tcl_CreateMathFunc() lets me replace the
>atan() function with one of my own cooking, and it would be nice if I could
>replace the operators as well.
>
>Of course, more complexity is always more fun. :-) If I wanted to teach
>expr to handle complex numbers, then I'd have to be able to specify
>operands and parameters that went beyond int/wide/double. Same for vectors
>and other stuff.

I'd advice *against* aiming to extend expr to handle new types (as sketched above); in a way it already tries to handle too many. A better approach would be to have a cexpr for calculating with complex numbers, a vexpr for vectors, etc.

The reason I say this is that in my opinion as a mathematician, the usual mathematical notations for operations are grossly unsuitable for the task of instructing a computer. The basic problem is that in mathematical writing the actual meaning of pretty much everything depends on the context (is e.g. * ordinary multiplication, inner product of vectors, multiplication of matrices, or what?) and the TRANslation of a FORmula to computer text generally fails to capture anything but minute traces of that context. As a consequence, there are a number of standard tricks around that are routinely employed for rediscovering that context.

The most common trick is to have a fixed standard context which everything is relative to, and this is pretty much where expr is today. In the many languages where it is a task in its own right to even store any piece of data more complicated than a double, this is of course a very natural approach. It is however fundamentally impossible to rely upon in a system that can be extended.

The most celebrated trick is polymorphism/overloading: the combination of argument types decide which of the identically named operations should be applied. Here one can furthermore distinguish between static ("compile-time") and dynamic ("run-time") variants, since the role of the programmer in specifying the context are somewhat different.

Static polymorphism is actually something one sees quite a lot in mathematical formulae; it is for example traditional to denote vectors by bold letters, matrices by upper case letters, and scalars by lower case letters; one knows Au denotes matrix-vector product because A is a "matrix letter" and u is a "vector letter". In most computer languages this takes the form of explicit type declarations for variables, so that the choice of operation implementation can be guided by the types of the operand variables. Since Tcl doesn't type-tag variables however, static polymorphism isn't much of an option for us.

What one could possibly do (and I tend to think that this is not a bad idea, but not something I particularly wish to engage in) is to introduce a little "math" language that was more powerful than that of expr, and seek to make that extendable. If a little language has variables of its own, then these can be declared with types, and static polymorphism becomes possible. If one allows several statements per "math" Tcl command, then one may find that very mathematical Tcl procedures can be written with bodies entirely in the little "math" language, so there is no need to make tight coupling between general Tcl variables and the little language variable. If the little math language is implemented by a compiled extension (it probably should be, for speed reasons), then it could have its own bytecode engine so that we wouldn't need to petition the TCT for hooks into the core engine, etc. But that isn't the kind of suggestion that was discussed above.

What remains for us is rather dynamic polymorphism, where the values of the operand determine the operation applied. This too exists in the present expr, and has its most striking appearance for "/", where for example expr 3/2 is quite different from expr 3.0/2.

Even dynamic polymorphism faces some rather obvious obstacles in a typeless language like Tcl, since there is no existing type system and no visible tagging of the values to fall back on; it is in principle necessary that the values are repeatedly examined to see if they can be seen as belonging to a particular type. Efficiency-wise this can be a great burden, but there are probably ways to minimise the effects in practice.

More problematic is that the type of a value will probably not be unique. A reasonable encoding of a complex number is as a two element list of doubles, but that is also the natural encoding of a vector in R^2, so how is {1.0 0.0}+{0.0 1.0 0.0} to be interpreted? Scalar (although complex) plus vector (automagically interpreted as "add this scalar to each vector element"), or an error because the vectors are of different lengths, or what? The ambiguities will get pretty severe once one starts combining code by different authors.

My main critique of such dynamic polymorphism is however not the above, but that it is in almost all cases stupid. I, as a programmer, typically _know_ when I write a command _exactly_ which operation I want performed (e.g., is this going to be a vector-vector or matrix-vector product?), so a programming language that doesn't allow me (or even merely discourages me) to tell the computer this certainly has a built-in flaw. That the computer guesses my intentions when I write commands interactively is nice, but in a programmatic situation it is of little use and very likely to lead to unexpected run-time errors further on. It should be avoided whenever possible.

If one has a cexpr command that is like expr but computes with complex numbers (upgrading doubles and integers whenever necessary, and preferably doesn't has a special case for integer division), then the ambiguities can be kept somewhat under control, since the author of this cexpr has taken on the task to sort the edge cases out. The same goes for a vexpr for vectors. Edge cases still exist, but they don't arise accidentally when different extensions are combined in the same interpreter, only when someone actually extends someone else's extensions.

The downside of this is that if someone has done a cexpr for complex numbers and a vexpr for real vectors then there's no automatic way of combining these features to make a cvexpr for complex vectors---instead it will be necessary to duplicate the code. Sensible resolution of ambiguous expression interpretations probably requires doing it that way, but since vectorification and complexification are mostly orthogonal one might still wish for some kind of modularisation that would make it easy to combine the two. Ideally one should be able to pick a "complex" module and a "vector" module off the shelf that would trivially combine into a "complex vector" module!

Actually, this modularity is pretty straightforward to accomplish in Tcl, if only one is prepared to give up on the traditional underspecified notation. A notation that will work well is instead a prefix notation, where one first specifies the "number system" (more properly: algebraic structure) in force, then the operation within that system, and finally the operands. Supposing that Z, Q, and C are the integers, rationals, and complex numbers respectively, one might imagine calculations such as the following (with % for prompt):

  % Z * 12 5
  60
  % Q + 2/3 3/5
  19/15
  % C / 1+2i 3+4i
  0.44+0.08i
  % Q * [Q - 2/3 3/5] 5/4
  1/12

(Realistic pure Tcl implementations would probably use {2 3} and {1 2} as representations rather than 2/3 and 1+2i, but the more traditional forms of these representations here make the examples easier to understand.)

The point of having such "number system" commands is that they make it very easy to generically implement e.g. vectors. Instead of having one codebase for real vectors, another for complex vectors, etc., one can have just a single vectorspace command that constructs a vector space on top of an arbitrary base field1. To make C^2 a two-dimensional complex vector space one might say

   vectorspace C^2 -scalars C -dim 2

after which the following would probably work:

   % C^2 + {{1.0 0.0} {0.0 0.0}} {{0.0 0.1} {0.0 2.0}}
   {1.0 0.1} {0.0 2.0}

(this result being (1.0+0.1i,2.0i)); whereas to make R^3 a three-dimensional real vector space one would say

   vectorspace R^3 -scalars R -dim 3

and similarly to make W an eight-dimensional rational vector space

   vectorspace W -scalars Q -dim 8

etc. (Personally I more often need to define polynomials over some particular ring than vectors, but the approach is the same in both cases.) What the commands created by this vectorspace command do is simply that they pick apart the vectors they are handed as arguments into components, but rely on the specified -scalars command to implement the necessary arithmetic with these components. That way, it is straightforward to build complex vectors as vectors on top of complex numbers.

All it takes, really, is good specifications of the interfaces between the various modules. Primarily people must agree on what to call things ("+" or "plus" or "add"?). Secondarily there is the need for introspection. Sometimes a higher level operation must be implemented in different ways depending on what is available on the lower level, and then this "what is available" query must be possible to perform programmatically. But here I'm getting waaaaay ahead of things.

My basic point is that one shouldn't be too concerned about getting expr to do fancy things, since it's not a very good way to do things anyway, it just happens to be one we're very used to.

Lars Hellström

PS:
1 An algebraic structure which supports addition, subtraction, multiplication, and division of arbitrary elements (except division by zero) while satisfying the usual laws of arithmetic is known as a /field/. This is not in any way related to "vector fields", which are just a traditional name for "vector-valued function".