Version 8 of Arity corner cases in math commands

Updated 2004-03-18 17:33:25

SS 18Mar2004

Background

TIP 174 proposed to add Tcl commands as an alternative to expr to do math with Tcl. Ideally, all the operators and mathematical functions in expr should be provided as commands, but in this document I want to focus in the arity corner cases problems, so I'll take as examples mainly +, -, *, and /.

In fact while it's trivial to guess what the command [+ 2 3] should do (just add the two numbers), it isn't for [- 4], [/ 40 10 2] and so on. This short memo will try to summarize the ideas discussed in Tcler's chat, and may espress in some case my particular vision on what is the best design for TIP 174.

Locking for already done design

We are not the first to face this problems. Lisp dialects already had to find solutions for our problems. We will study the solution adopted by the Scheme language that's one of the formally cleaner dialects of Lisp, and try to improve in respect to Tcl fit.

Binary arity case

The simpler case, is of course the binary one. being +, -, * and / all binary operators the meaing is just:

  [+ a b] => a + b
  [- a b] => a - b
  [* a b] => a * b
  [/ a b] => a / b

That's of course the Scheme's behaviour.

Three or more arguments

The next step is to decide what to do with more than three or more arguments.

For + and *, the commands just do the sum or product of all the arguments passed. For - and /, left association is used. So:

  [+ a b c ... z] => a + b + c + ... + z
  [- a b c ... z] => ((((a - b) - c) - ... ) - z)
  [* a b c ... z] => a * b * c * ... * z
  [/ a b c ... z] => ((((a / b) / c) - ... ) / z)

This seems what the user expects, following the least surprise principle. To avoid such an extension is just to lose an opportunity to type less.

One or zero arguments

To handle the one or zero argument cases is a bit more complex. That's what Scheme does: if we pass a single argument, it uses it as second argument, and use for the first the neutral for this operator (0 for + and -, 1 for * and / of course).

So

  (+ x) is equivalent to (+ 0 a), that's x
  (- x) is equivalent to (- 0 a), that's -x
  (* x) is equivalent to (* 1 a), that's x
  (/ x) is equivalent to (/ 1 a), that's 1/x

With zero arguments, + and * just return the neutral:

  (+) returns 0
  (*) returns 1

- and / are invalid with zero arguments.

What are the advantages of this for Tcl?

  • Save you some (minimal) keystroke when you need reciprocal.
  • Commands that works with any arity in a meaningful way (except for - and / with zero arguments).
  • [- x] returning -x is somewhat natural visually.

What about the problems of this solution?

  • Least surprise rule violated, it's non obvious.
  • Does not play well with {expand} sometimes.
  • Other operators may have a less sounding extension with 0 or 1 arg.

An example of bad interaction with {expand} is the following:

  - $n {expand}$list

The intuitive meaning of the expression is "to subtract all the elements of $list in turn, from $n". You may not expect it to return the reciprocal of $n if $list happens to be empty. Sometimes you may also want to raise an error if there are at least two arguments, in order to avoid hard to trace bugs. Of course you can always write:

  - $n 0 {expand}$list

to avoid the reciprocal problem, but the default behaviour is still not good, and there is no way to raise an error for default on a suspicious number of arguments.

An alternative solution that may work better, is to use the neutral argument as second argument if the user provided just one. So:

  [+ $x] will be equivalent to [+ $x 0]
  [- $x] will be equivalent to [- $x 0]
  [* $x] will be equivalent to [* $x 1]
  [/ $x] will be equivalent to [/ $x 1]

This saves us from the reciprocal problem with {expand}. Still the least surprise principle is violated for quite little advantages. On the other side this two solutions are sounding in the ortogonality and coherence side. What's a good alternative?

The proposed solution

A collaborative design effort in the Tcler's chat, in form of a discussion that started with very different ideas and slowly converged, reached the following solution: To raise an error for *every* binary operator called with less than two arguments. This is of course very simple to explain, and still, to type:

  [- 0 $x] instead of [- $x]

is not this great problem ;). On the other side this wins the least surprise battle, and interact quite well with {expand}.

  [+ {expand}$l] ; # Will raise an error if the list has less than two elements
  [+ 0 {expand}$l] ; # Will raise an error if the list has less than one arg.
  [+ 0 0 {expand}$l]; # Returns zero with empty list. Otherwise the sum.

What's important here is that the user can select from different behaviours, with the default being the safest.

It works well even with - and /.

  [- $n {expand}$l]; # Error on empty list.
  [- $n 0 {expand}$l]; # Returns $n on empty list, what most expect otherwise.

It's simple, and seems the better for Tcl. That's why some Tclers are now convinced that this can be a good solution (and I hope all the guys that agree with this to add their names in this page). What is important, I think, is to not add exceptions to this rule, because exceptions are not a good idea in design, and they will create different usage patterns with {expand}.


NEM Just want to clarify some stuff here. Firstly, I have a scheme interpreter sitting here (actually, it's LispMe [L1 ] on my palm pilot. It's apparently a mostly conforming scheme interpreter). The results I get are:

 (+) -> 0
 (-) -> error (wrong # args)
 (*) -> 1
 (/) -> error (wrong # args)

+, and * both operate on lists, and are defined (it seems) equivalently to:

 proc + {args} { expr [join [concat $args 0] +] }
 proc * {args} { expr [join [concat $args 1] *] }

However, - and / work on either 1 or 2 arguments, and no more. Anything else gives an error. In the one argument case, - acts as negation, and / acts as reciprocal (if they're the right terms):

 (- 1) -> -1
 (/ 2) -> 0.5

In the two argument case, they work as expected. The more I think about it, the more this behaviour makes sense (with the possible exception of (/ x) being equivalent to (1/x)). If you try and make - work on a list, then you need to specify a different operator for negation, IMHO.


SS You are right, (-) and (/) does not return 0 and 1 but just an error. About three or more arguments for - and /, that's what R5RS states:

 procedure:  (+ z1 ...)
 procedure:  (* z1 ...)

 These procedures return the sum or product of their arguments.

 (+ 3 4)                         ===>  7
 (+ 3)                           ===>  3
 (+)                             ===>  0
 (* 4)                           ===>  4
 (*)                             ===>  1

 procedure:  (- z1 z2)
 procedure:  (- z)
 optional procedure:  (- z1 z2 ...)
 procedure:  (/ z1 z2)
 procedure:  (/ z)
 optional procedure:  (/ z1 z2 ...)

Actually they are optional, just my interpreter support this (mzscheme), and your implementation does not. I corrected the above document about - and / without arguments.


male - 18.03.2004:

Sorry - in my opinion, we have already non-intuitive commands in tcl and/or tk, so ... if it is clearly described in the man pages, that ...

 [- x {expand}list]

... is equal to ...

 [- x listElem1 listElem2 ... listElemN]

... than everything is ok!

But to disallow such things like ...

 [- 1]

... and to force to use ...

 [- 0 1]

... is most counter intuitive and to be prevented!