Math Operators as Commands

AMG: TIP 174 [L1 ], "Math Operators as Commands", adds commands named +, -, ==, etc., for the purpose of exposing math operations to the script level without needing to go through [expr]. These commands live in the ::tcl::mathop namespace. Functions (as opposed to operators) are in the ::tcl::mathfunc namespace.

For more information, including a list of math operators, see tcl::mathop.


AMG: When I first read the TIP, I wondered why the inequality operators were strictly binary. It seemed to me that they could be more general, that they could test to see if all elements of a list are distinct. In the zero- and one-argument cases, they would return true. In the two-argument case, they'd behave as expected. For three or more arguments, each possible argument pairing would be checked for inequality, e.g. (a!=b)&(a!=c)&(b!=c) for three arguments.

Today I asked the Tcl Chatroom, and the answer was that inequality operators are binary-only because the interpretation for other arities is not obvious; multiple reasonable interpretations exist.

  1. Pairwise inequality: (a!=b)&(b!=c)&(c!=d)
  2. Any inequality: (a!=b)|(a!=c)|(a!=d)|(b!=c)|(b!=d)|(c!=d)
  3. Total inequality: (a!=b)&(a!=c)&(a!=d)&(b!=c)&(b!=d)&(c!=d)

In the tcl::mathop table, every operator that does support three or more arguments (I'll call them ternary+ operators) has a commonly understood meaning when written a op b op c op d, for instance a == b == c == d. Never is a != b != c != d written because it would not be clear which of the above three interpretations is intended.

Moreover, for every ternary+ operator, interpretations (1) and (3) always yield the same result, assuming the list ordering isn't reversed when using asymmetric operators. This is because they all exhibit transitivity: (((a op b) and (b op c)) implies (a op c)). Try it and see. :^)

RS pointed out that (2) is simply [! [== a b c d]]]. He also suggested a nice, simple implementation of (3) using [lsort]:

 proc distinct {args} {== [llength $args] [llength [lsort -unique $args]]}

(Use the -real lsort option if you don't want "0", " 0", "0.0", "-0", etc. to be considered distinct.)

I can't imagine a single case where (1) is useful, but it wouldn't be difficult to implement.

wdb In my humble opinion, in opposite to the operator ==, the operator != cannot be transitive. Watch this infix notation:

 0 != 1 != 0

You can translate it to 0 != 1 && 1 != 0, but would you ever say because part 1 is true and part 2 is true, that the conclusion were 0 != 0?

AMG: Yes, the whole problem is that != isn't transitive, which is why the ambiguity exists. [!= 0 1 0] is true according to interpretations (1) and (2) but false according to interpretation (3). For any transitive operator, (1) and (3) always yield the same result.


wdb At moment, the lazy evaluating operator && cannot be implemented. Just an idea ... in the form above, it is not possible to realise && with lazy evaluation as every arg is evaluated when the function is invoked. But if we braced the list of args? Watch:

 && {expr1 expr2 ...}

results in the same bytecode as

 expr {expr1 && expr2 ...}

See also the ternary operator ? - RS ... which is of course if...else...

AMG: Would this behavior (automatic application of expr) be applied to all math operator commands or only to || and &&? The problem I see is that it would effectively make quoting required for all arguments, same as it is with [expr] itself. See brace your expr-essions for the reasons why.

wdb Both of you (RS and AMG) are right. One of my quick'n'dirty ideas. What if the operator (e.g.) + as well as && would concat and evaluate their arguments as well as nowadays expr? Example:

 && $expr1 $expr2

should be equivalent to

 && {$expr1 $expr2}

In the latter case of example above, $expr2 is not evaluated if $expr1 results in false; in the former case, both are evaluated. The result should be the same unless $expr1 is false and $expr2 produces some side effect such as an error (but not only). I am not sure if I have overseen some pitfalls. But is this idea good? Or bad?

AMG: Bad, I think, but I only say so because it gets in the way of the use I envision for operator commands. I find bracing my expressions to be a chore, and I look forward to not having to do this when using math operator commands. Combine this with the ::tcl::mathfunc namespace, and I won't need [expr] at all. I'd have to use [if] and friends to make up for the lack of conditionals and short-circuit evaluation, but this is fine because I rarely use these outside of the first argument to [if] and [while].

By the way, this change makes me much less interested in $[...] shorthand for [expr {...}]. I simply won't need it.

wdb But note that expr is part of the core as it is invoked implicitly as arg#1 for if, while, and arg#3 for for. -- Generally there comes some new dichotomy as expr {$a + $b} and + $a $b do the same job.

[name redacted]: the && and || commands can easily be faked using tcl::mathop::* and +: see Faking tcl::mathop::&& and ||.


LES I hope that TIP 174 never makes it to the core. That is all perfectly possible with sugar syntax. My position is: don't change the language except to implement something that is really not possible with the existing syntax and commands. If you want everything prêt-à-porter, make your own procs and classes and reuse them in every opportunity, or just code in PHP and its 1,000+ TV dinner functions.

AMG: Perhaps something similar to TIP 174 can be placed in tcllib to make it commonly available.

Lars H: You hope in vain, LES; TIP#174 is already (2007-01-13) part of the 8.5 core. An important reason for having it is that the commands already exist a bytecode operations, so its implementation was only about adding direct Tcl commands that compile to these things without taking the detour through the expr little language.


PWE Wouldn't it be nice if the commands were also able to handle lists, and process them as array's or vectors?

RS: See vector for links to a number of such extensions/code snippets. If they treat a scalar like the original operator, it might indeed be possible to overload math operators in that way...

Lars H: A good thing about having the math operations as commands in the tcl::mathop namespace is that you can easily have corresponding operations on vectors (defined the way you want them) in another namespace. In Tcl, one picks a command made for the type of data one wishes to operate on, rather than making commands that try to guess which operation should be applied based on the data they're fed.


AJD A couple of nice-to-have builtin operator commands would be: [<=>] (numeric comparison) and [cmp] (string comparison) as Perl has. These crop up when writing sort comparison commands. Of course you can write

 proc <=> {a b} { if {$a > $b} { return 1 } ; if {$b > $a} { return -1 } ; return 0 }

but that's no different to any of the other op's recently introduced.

RS: If I understand right, cmp is what Tcl calls string compare; a <=> could be done as sgn($a-$b), where the classic definition by rmax is

 proc sgn x {expr {($x>0)-($x<0)}}

DKF: Surely you mean:

 proc tcl::mathfunc::sgn x {tcl::mathop::- [tcl::mathop::> $x 0] [tcl::mathop::< $x 0]}

RS Do I? Maybe as a compromise, with namespace imported operators:

 proc tcl::mathfunc::sgn x {- [> $x 0] [< $x 0]}

But still I think, an expr with two or more different operators in play looks better (clearer) than a nest of prefix operators...

LV AJD, how do your <=> and cmp differ from Tcl expr's == and eq?

AJD ...see the above proc definition for [<=>]. While, as has already been noted, [cmp] is really just [string compare]. It should be obvious how they differ to == and eq: for example [== 0 1] is 0 while [<=> 0 1] is -1. The typical usage would be in a sort compasion routine:

  proc my_cmp {a b} {
     set int_a [do_something_that_returns_an_integer $a]
     set int_b [do_something_that_returns_an_integer $b]
     return [<=> $int_a $int_b] ;# or perhaps more readably as: [expr {$int_a <=> $int_b}]
  }
  set sorted [lsort -command my_cmp $some_list]

LV I see - you are right, those are useful functions. So, the next question is this - can one use the namespace ensemble functionality in Tcl 8.5 to write cmp and <=>, in a way that, as a result, expr would have them?

AMG: [cmp] and [<=>] can not be made into new operators, not without modifying the Tcl source code. TIP 174 change 3 says "the commands are not connected to their corresponding expr operator. Overloading or adding any command in ::tcl::mathop does not affect operators in expr or any other command that calls Tcl_ExprObj, and nor does overriding expr alter the behaviour of any command in :::tcl::mathop."

But they can be made into new functions. Put them in the ::tcl::mathfunc namespace and they'll be visible as cmp(a, b) and <=>(a, b). However, I don't think [expr] will like that last name, so call it sign or sgn.


KD: It is unfortunate that the ternary ? operator was not included in TIP 174. When I look at my programs, almost half of the cases where I could use "operators as commands" involve the ternary operator. Usually for cases like:

 $w configure -state [expr {$s ? "normal" : "disabled"}]

which becomes much cleaner when written as:

 $w configure -state [? $s normal disabled]

The TIP states that the ternary operator was not included because it has lazy evalution semantics in expr (when properly braced), but that is not a problem IMHO.

RS On the other hand, as if is a function that returns the result of the succeeding script, how about

 proc I x {set x} ;# identity
 $w configure -state [if $s {I normal} {I disabled}]

KD: Clever, didn't think about this. But I think it looks still better with the ? operator.

AMG: Going a step farther, you're free to make your own "ternary operator" command. Note that I brace the first argument to [if] because it is an expr.

 proc ? {test true false} {if {$test} {return $true} {return $false}}

I often find myself using ?: with more than just three arguments, for example (C):

 int i = input();
 int o = i == 0 ? 99 : i == 1 ? 98
       : i == 2 ? 97 : i == 3 ? 96
: i == 4 ? 95
94;

Writing this using [?] might yield the following (Tcl):

 set i [input]
 set o [? [== $i 0] 99 [? [== $i 1] 98\
       [? [== $i 2] 97 [? [== $i 3] 96\
       [? [== $i 4] 95              94]]]]]

To avoid excess brackets, a more general ? operator can be made that would work like [if ... elseif ... elseif ... else]:

 proc ? {args} {
    if {[llength $args] % 2 != 1} {
       error "wrong # args: should be \"? ?test1 out1 test2 out2 ...? default\""
    }
    while {[llength $args] > 2} {
       set args [lassign $args test true]
       if {$test} {return $true}
    }
    return [lindex $args end]
 }

I don't use the math operator commands in the above code because all math is done inside the first argument to [if] or [while] which already understands the expr language.

Rewritten example:

 set i [input]
 set o [? [== $i 0] 99 [== $i 1] 98\
          [== $i 2] 97 [== $i 3] 96\
          [== $i 4] 95           94]

Unlike with C, all the comparisons are performed before invoking the ? operator. If the tests have side effects, this is a problem. But I prefer to not have side effects in my tests, so it's okay with me, and the only remaining problem is the slight performance decrease.

It is possible to give [?] the power of short-circuit evaluation simply by removing the braces from its [if] test, and then all test arguments to [?] will need to be braced to avoid immediate evaluation. But for this facility to support variables and such, an [uplevel] is needed too.

To make the short-circuit evaluation behavior complete, do the same for the values returned by [?], and brace every argument to [?].

 proc ? {args} {
    if {[llength $args] % 2 != 1} {
       error "wrong # args: should be \"? ?test1 out1 test2 out2 ...? default\""
    }
    while {[llength $args] > 2} {
       set args [lassign $args test result]
       if [list [uplevel [list expr $test]]] "return \[uplevel [list expr $result]\]"
    }
    set result [lindex $args end]
    return [uplevel [list expr $result]]
 }

Safely constructing scripts to pass to [uplevel] can be a pain, but I think I successfully avoided quoting hell. (Tcl offers spiritual liberation! Put that in the man page!)

Usage example:

 % set x 5
 % ? {$x == 5} {$x * 2} {$x == 6} {$x * 3} {$x - 1}
 10
 % set x 6
 % ? {$x == 5} {$x * 2} {$x == 6} {$x * 3} {$x - 1}
 18
 % set x 7
 % ? {$x == 5} {$x * 2} {$x == 6} {$x * 3} {$x - 1}
 6

KD: When you take it this far, you cannot return strings anymore. So the example we started from ($w configure -state [? $ s normal disabled]) won't work anymore.

AMG interrupts: Actually you can return strings; you just have to use expr notation to do so.

 $w configure -stat [? {$s} {"normal"} {"disabled"}]

I don't advocate this usage; I am merely demonstrating a possible implementation in response to queries about [?] and lazy/short-circuit evaluation. But that's just me; someone may find it to be just the right thing.

KD continues: You can indeed implement operators like these yourself (see also Modeling COND with expr), but I'm a bit reluctant about defining single-symbol procs myself. Therefore it would have been a good idea to standardize the ? operator in TIP 174 with its usual (ternary) meaning.

Lars H: Yet another alternative to a ? command is lindex

 $w configure -state [lindex {normal disabled} $s]

(This assumes $s is 0 or 1, but one can coerce other booleans using ::tcl::mathfunc::bool.)


LV 2007 July 10 Maybe I don't understand the tcl::mathop table. My assumption, reading it, is that the columns are the name of the operation, the symbol used for the operation, what is returned if the symbol is used with 0 arguments, 1 argument, and 2 arguments, and finally an example of the operator in use.

However, at least two of the table entries confuse me:

 Product                   *        1      a      a*b             ((a*b)*c)*...
 Exponentiation            **       1      a     a**b          a**(b**(c**...))

What does it mean that a * or ** used with 0 arguments return a 1 and that used with one argument return the argument?

Why, at least in the 0 argument case, is the result not an error like nearly every other operator?

It's not a compatibility with expr thing:

 $ tclsh
 % set a [expr *]
 missing operand at _@_
 in expression "_@_*"
 % puts $a
 can't read "a": no such variable

Looking again over the table, I see quite a few cases where the operator is returning a value in the case of 0 or 1 argument where there are what seems to me to be arbitrary values being returned, rather than consistency. Again, if it were to be compatible with expr it would be understood. Otherwise, it seems like an explanation of why someone would do this would be helpful.

AMG: [+] evaluates to zero because the sum of zero numbers is defined to be zero, since zero is the additive identity. Extrapolating from that uncontroversial idea, [*] and [**] evaluate to one because the product of zero numbers is one, since one is the multiplicative identity. Viewed another way, these are the basis cases for the respective operations, and each input is (iteratively/recursively/whatever) applied to an accumulator which is initially set to the operator's respective identity.

 Sum           : ((((0)+a)+b)+c)+...
 Product       : ((((1)*a)*b)*c)*...
 Exponentiation: a**(b**(c**(...**(1))))

(The position of the (1) for exponentiation is due to right associativity.)

If that doesn't have you convinced, consider that the same concept applies to the bitwise operators:

Bitwise AND (&) clears the nth bit in its output iff at least one of its inputs has its nth bit cleared. Therefore the AND of zero inputs is all-bits-set (-1). None of the inputs have any bits cleared, so none of the output bits are cleared. [&] evaluates to -1.

Similarly, bitwise OR (|) sets the nth bit in its output iff at least one of its inputs has its nth bit set. Therefore the OR of zero inputs is all-bits-cleared (0); none of the inputs have any bits sets, so none of the output bits are set. [|] evaluates to 0.

Bitwise XOR (^) is also similar. The nth output bit is set iff an odd number of inputs have their nth bits set. If zero inputs have their nth bits set, the nth output bit is cleared because zero is not an odd number. If there aren't any inputs, by this definition all output bits must be cleared. [^] evaluates to 0.

(also AMG, added after escargo's comment but not in response to his comment) 100% [expr] compatibility is not the goal; since infix notation is not being used, operators are not limited to two arguments. To maximize utility, their definitions have been generalized to support as many arities as possible. When given two arguments (or one, in the case of the three negation operators), they behave the same as in [expr].

escargo - Perhaps some of these make sense for compatibility with other languages (J, APL, Scheme, Lisp). Or they are intended to be the appropriate identity element for the operation for vacuously true cases (like the empty product [L2 ]).