TIP Expr Shorthands : an analysis

Introduction

FM Expr Shorthands is a proposal to improve the usability of mathematical calculations in Tcl. It consists in :

  • Three shorthands :
    • [( ... )] : an inline shorthand (to compute a command argument)
    • (( ... )) : an index shorthand (to compute an array index)
    • {( ... )} : a body shorthand (to compile a script as an expression)
  • Three expr improvement
    • an assignement operator, from TIP 282
    • a separator operator, from TIP 282
    • list capabilities

Example

I took a proc in tcllib, to compute the integral of a function.

proc integral { begin end nosteps func } {

   set delta    [expr {($end-$begin)/double($nosteps)}]
   set hdelta   [expr {$delta/2.0}]
   set result   0.0
   set xval     $begin
   set func_end [uplevel 1 [list $func $xval]]
   for { set i 1 } { $i <= $nosteps } { incr i } {
      set func_begin  $func_end
       set func_middle [uplevel 1 [list $func [expr {$xval+$hdelta}]]]
       set func_end    [uplevel 1 [list $func [expr {$xval+$delta}]]]
      set result      [expr  {$result+$func_begin+4.0*$func_middle+$func_end}]

      set xval        [expr {$begin+double($i)*$delta}]
   }

   return [expr {$result*$delta/6.0}]
}
proc f {x} {
    return $x
}

puts Orig:[timerate {integral -2 2 10 f }]; 
# 39.3619 µs/# 25405 # 25405.3 #/sec 999.988 net-ms
puts length:[(LO=[string length [info body integral]] )]=100%; 
# length:588=100%

I made 4 variants of this proc whith the proposed shorthand :

The first one is using only the inline shorthand:

proc integralSH1 { begin end nosteps func } {
   set delta    [( ($end-$begin)/double($nosteps) )]
   set hdelta   [( $delta/2.0 )]
   set result   0.0
   set xval     $begin
   set func_end [uplevel 1 [list $func $xval]]]
   for { set i 1 } { $i <= $nosteps } { incr i } {
       set func_begin  $func_end
       set func_middle [uplevel 1 [list $func $xval+$hdelta]]]
       set func_end    [uplevel 1 [list $func $xval+$delta]]]
       set result      [($result+$func_begin+4.0*$func_middle+$func_end)]
       set xval        [($begin+double($i)*$delta)]
   }
   return [($result*$delta/6.0)]
}
puts SH1:[timerate {integralSH1 -2 2 10 f }]; 
# 39.3878 µs/# 25388 # 25388.6 #/sec 999.978 net-ms
puts SH1.length=[(L = [string length [info body integralSH1]],  "=", 100-$L/$LO*100 )]; 
# length:558 = -5.102040816326522%

We can notice here that there is no practical difference. The inline shorthand [( ... )] is bytecompiled like expr. It is expr.

I create another little variation : as uplevel is waiting for a list, we can write [($func, $xval+$delta)] instead of [list $func $xval+$delta]]

proc integralSH2 { begin end nosteps func } {

   set delta    [( ($end-$begin)/double($nosteps) )]
   set hdelta   [( $delta/2.0 )]
   set result   0.0
   set xval     $begin
   set func_end [uplevel 1 [($func, $xval)]]
   for { set i 1 } { $i <= $nosteps } { incr i } {
       set func_begin  $func_end
       set func_middle [uplevel 1 [($func, $xval+$hdelta)]]
       set func_end    [uplevel 1 [($func, $xval+$delta)]]
       set result      [($result+$func_begin+4.0*$func_middle+$func_end)]
       set xval        [($begin+double($i)*$delta)]
   }

   return [($result*$delta/6.0)]
}
# SH2:43.7472 µs/# 22858 # 22858.6 #/sec 999.973 net-ms
# length:544 = -7.482993197278915%

Here, we can notice it is a little slower : the bytecode analysis shows that it has extra tryCVTtoNumeric. It's because there is a conversion of the list in compileExprTree.

Then I tried TIP282 changes :

proc integralSH6 { begin end nosteps func } {
    expr {
          delta = ($end-$begin)/double($nosteps);
          hdelta = $delta/2.0;
          result = 0.0;
          xval = $begin;
          func_end = [uplevel 1 [list $func $xval]]
      }
    for {set i 1} {$i <= $nosteps } {incr i} {
        expr {
              func_begin = $func_end;
              func_middle = [uplevel 1 [list $func [expr {$xval+$hdelta}]]];
              func_end = [uplevel 1 [list $func [expr {$xval+$delta}]]];
              result = $result+$func_begin+4.0*$func_middle+$func_end;
              xval = $begin+double($i)*$delta
          }
    }

    return [($result*$delta/6.0)]
}
# SH6:39.2464 µs/# 25480 # 25480.1 #/sec 999.997 net-ms
# length:539 = -8.333333333333343%

There is no practical difference also.

Another variant consist in testing body script shorthand {( ... )} capability. I used it on the for command

proc integralSH5 { begin end nosteps func } {
    expr {
          delta = ($end-$begin)/double($nosteps);
          hdelta = $delta/2.0;
          result = 0.0;
          xval = $begin;
          func_end = [uplevel 1 [list $func $xval]]
      }
    for {(i=1)} {$i <= $nosteps } {(i=$i+1)} {
        expr {
              func_begin = $func_end;
              func_middle = [uplevel 1 [list $func [expr {$xval+$hdelta}]]];
              func_end = [uplevel 1 [list $func [expr {$xval+$delta}]]];
              result = $result+$func_begin+4.0*$func_middle+$func_end;
              xval = $begin+double($i)*$delta
          }
    }

    return [($result*$delta/6.0)]
}

# SH5:40.5876 µs/# 24638 # 24638.0 #/sec 999.998 net-ms
# length:538 = -8.503401360544217%

The bytcode analysis show insertion of extra tryCVTtoNumeric. More, there is, more the speed reduce

proc integralSH3 { begin end nosteps func } {
    expr {
          hdelta = (delta = ($end-(xval = $begin))/double($nosteps))/2.0;
          func_end = [uplevel 1 [($func, $xval)]]
      }  
    
    for {( result = 0.0; i=1)} { $i <= $nosteps } {(i=$i+1)} {(
              func_begin  = $func_end;
        func_middle= [uplevel 1 [($func, $xval+$hdelta)]];
        func_end    = [uplevel 1 [($func, $xval+$delta)]];
        result =$result+$func_begin+4.0*$func_middle+$func_end;
        xval=$begin+double($i)*$delta
    )}
    return [($result*$delta/6.0)]
}
# SH3:49.3772 µs/# 20252 # 20252.3 #/sec 999.987 net-ms
# length:484 = -17.687074829931973%

... Same reason.

The last variant is to make the proc as an expression Script : Then it is a lot slower (allmost twice).

proc integralSH { begin end nosteps func } {(
    delta = ($end-$begin)/double($nosteps);
    hdelta = $delta/2.0;
    result = 0.0;
    xval = $begin;
    func_end = [uplevel 1 [($func, $xval)]];
    for({(i=1)},{$i <= $nosteps },{ incr i },{(
        func_begin = $func_end;
        func_middle = [uplevel 1 [($func, $xval+$hdelta)]];
        func_end = [uplevel 1 [($func, $xval+$delta)]];
        result = $result+$func_begin+4.0*$func_middle+$func_end;
        xval = $begin+double($i)*$delta
        )});

    $result*$delta/6.0
    )}
# SH:71.4310 µs/# 13999 # 13999.5 #/sec 999.963 net-ms
# length:454 = -22.78911564625851%

The bytecode analysis shows that the for loop is compiled separatly, in another step. That should be the reason why.


---


gold 3/24/2026. Added categories, so can find message in Wiki.