Importing expr functions

Richard Suchenwirth 1999-08-12 - Mathematics in Tcl is concentrated in the expr command, which has a syntax quite different from Tcl's: whitespace is irrelevant, function arguments are separated by comma and enclosed in parens. Compare the calling of a function/proc with two arguments:

   [f x y] ;#  Tcl style - similar to Logo, LISP, ...
   f(x,y)  ;# expr style - similar to C, FORTRAN, ...

Now, if we want to get the integer part of a number, we'd say

   set i [expr int($x)]

or even, for more efficient calculation,

   set i [expr {int($x)}]

where we have all brackets, braces, parens on parade, but it's slightly more convolved than

   i = int(x);

as you'd have it in C. Tcl sometimes has weird ways, but the weirdest is how you can change them. Imagine we'd want to "import" expr 's built-in functions, that is: make them available in the usual Tcl style. For the single int function, one might just say

   proc int {x} {expr int($x)}

and we're done. As usual, the argument can be considered just a string, and since it's evaluated in expr anyway, we can use all expr functionalities inside it, e.g.

   set i [int 2.5+1.3] ==> 3

To emulate the feature that whitespace is irrelevant, we can refine our proc to

   proc int {args} {eval expr int($args)}

I am confused about what aspect of whitespace you are removing by this? What you are creating is a buggy situation:

int 1.2 2.2 3.3333 results in the msg syntax error in expression "int(1.2 3.3 4.555)" while evaluating {int 1.2 3.3 4.555} instead of the more obvious proc int {x} {expr int($x)} int 1.2 1.3 called "int" with too many arguments while evaluating {int 1.2 1.3}

So what aspect of whitespace is the args trying to accomodate? RS: Between ""words": int $x+$y vs. int $x + $y

The variable with the special name args collects all remaining arguments into a list, which is spliced into the expr parens by passing it all through eval.

But expr has a lot more to offer than int. We may repeat the above one-liner for each function we want to "import", but if we really want to reuse that (simple) code, packaging it into a proc is usually better (and can be done fancier, as we shall presently see). Round one:

   proc expr:import {name} {
        proc $name args [list eval expr ${name}(\$args)]

Now this is getting philosophical. We define a proc that defines a proc, whose body is not constant (otherwise we could use the normal {} notation), but built up as a list into which the name variable is substituted. As this stands right before an opening paren, we have to prevent the parser from misunderstanding it as an array name, so we enclose the name "name" in curlies. On the other hand, "args" has no value yet (only when our generated proc is called), so we have to defer its evaluation until then with the backslash before the dollar sign.

Next we want to be able to call expr:import with more than one function name. Easily done:

   proc expr:import {args} {
        foreach name $args {
                proc $name args [list eval expr ${name}(\$args)]

More philosophy. The "outer" args is the list of function names we walk with foreach, but has nothing to do with the "inner args". This is not mysticism: we just have to use that special name for its collecting feature. -- Finally, we want to allow "*" or "all" as argument to imply that all available functions are exported, and, in the Tcl spirit of introspection, offer a "names" argument that just returns the list of allowed names, and check legality of names, so we make it:

   proc expr:import {args} {
        set names {
           abs acos asin atan atan2 ceil cos cosh double exp floor fmod 
           hypot int log log10 pow rand round sin sinh sqrt srand tan tanh
        switch -- $args {
        "*" - all {set args $names}
        names     {return [join $names]}
        foreach name $args {
                if {[lsearch $names $name]<0} {
                   error "$name is not one of $names"
                proc $name args [list eval expr ${name}(\$args)]

Notice another trick: The function names list is formatted with tabs and linebreaks to make this source look good. To make the result of "expr:import names" look good too, it is re-formatted to a blank-separated list with join.

Experimenting with this version, we find that for functions with more than one argument, the comma between is still required. This is not the Tcl style we want. So we trade the whitespace tolerance introduced above for joining the (inner) args with commas:

        proc $name args [list eval expr ${name}(\[join \$args ,\])]

Looks more and more magic, because we have to escape the brackets round the join command too. This way, we can have just whitespace between the arguments - and group with quotes or braces if we want to have embedded whitespace, too:

  hypot 3 {2 + 2}     ==> 5.0

One last thing to remember: our imported functions are applied only where Tcl expects a command. Inside expr expressions, the original is still taken, so both syntaxes may need to be mixed:

  hypot 3 hypot(4,5)  ==> 7.07106781187

but in such cases openly using expr may be the better choice ;-) And of course you can make the Tcl parser evaluate the second hypot call:

  hypot 3 [hypot 4 5] ==> 7.07106781187

expr's operators can also easily be exported to LISP-like procs that take a variable number of arguments:

 foreach op {+ - * / %} {
        regsub @op {expr [join $args @op]} $op body
        proc $op args $body
 set a 10
 set b [+ 100 [* 500 $a]]

Importing expr functions, part 2 takes this idea all the way, creating a package containing expr's functionality as procedures.

If you are interested in going the other way - writing functions for [expr] in Tcl without the use of square brackets (useful for when you want to expose expressions to users without having to tell them to learn all about Tcl, no matter how much that would benefit them in the long run. This sort of situation also requires the use of safe interpreters) - then you will probably find very interesting indeed.


RS 2006-03-27: Time has moved on, here's an update. Tcl by now has info functions which returns the functions known to expr. The following code tests for "arity" (number of arguments): rand is 0-ary, those that can be called with one argument will, while the rest lead to wrapper procs with two arguments:

 proc expr'functions {} {
   foreach f [info functions] {
      if {$f eq "rand"} {
         proc $f {} "expr ${f}()"
      } elseif [catch {expr ${f}(1)}] {
           proc $f {x y} "expr {${f}(\$x,\$y)}"
      } else {proc $f x "expr {${f}(\$x)}"}

See also: tcl::mathfunc