Version 2 of Using expr on lists

Updated 2006-04-19 13:26:35

Arjen Markus (19 april 2006) In languages like Fortran 90/95 and MATLAB, you can use expressions on arrays which are treated element-wise. For instance:

   real, dimension(10) :: a, b
   real                :: c   

   a = 2.0 * b + c

is evaluated in a way equivalent to:

   do i = 1,10
       a(i) = 2.0 * b(i) + c
   enddo

(to use a Fortran 90/95 example)

I thought it might be nice to have this ability in Tcl as well ...

For the sake of a simple example, I used the "all" function from Fortran:

  • It takes one argument, a logical expression using (one-dimensional) arrays,
     which can be viewed as an array itself.
  • It returns true if all elements of that array are true (or if there are none)

The script below turns the expression given as the argument to [all] into a (hidden) procedure that utilises [foreach] to evaluate the expression for each list element in turn.

Note:

I used the [catch] command to make memoisation trivial, but [catch] is fairly expensive.


 # listexpr.tcl --
 #     Using [expr] on lists:
 #     if u and v are lists, then [all {$u > $v}]
 #     returns 1 if all elements of u are larger than the corresponding
 #     elements of v and 0 if there is at least one element for which
 #     the condition does not hold.
 #

 # makeListProc --
 #     Create a procedure that handles the list expression
 # Arguments:
 #     expr      The expression to be examined element by element
 #     body      The body of the procedure (minus the preliminaries)
 # Result:
 #     Name of the generated procedure
 # Note:
 #     The body argument may contain the substrings EXPR and VARS
 #     - these are replaced by the expr argument and by the generated
 #     list of variables for use in the foreach construct.
 #
 # Limitations:
 #     The expression should not contain subcommands, that is:
 #     {$u > [splice $v 1]} would not be parsed properly.
 #
 proc makeListProc {expr body} {

     set vars [lsort -unique [regexp -all -inline {\$[a-zA-Z0-9_]+} $expr]]

     set eachlist ""
     set decls    ""

     foreach name $vars {
         set vname [string range $name 1 end]
         if { [uplevel 2 "llength $name"] > 1 } {
             append eachlist "$vname \$_$vname "
             append decls "upvar 2 $vname _$vname\n"
         } else {
             append decls "upvar 2 $vname $vname\n"
         }
     }

     set body [string map [list VARS $eachlist EXPR $expr] $body]

     proc $expr {} $decls$body

     return $expr
 }

 # all --
 #     Check if all elements in the lists referred to in an expression
 #     comply to that expression
 # Arguments:
 #     expr         Expression to be checked
 # Result:
 #     1 if all elements comply, 0 otherwise. If the lists are
 #     empty, return 1 too.
 #
 proc all {expr} {
     if { [catch $expr result] } {
         makeListProc $expr \
 {
     set result 1
     foreach VARS {
         if { !(EXPR) } {
             set result 0
             break
         }
     }
     return $result }

         set result [$expr]
     }
     return $result
 }
 proc all_simple {expr} {
     upvar 1 u u
     if { [llength $u] > 1 } {
         return [$expr]
     }
 }

 # main --
 #     Simple test case
 #
 set u {1 2 3 4}
 set v {0 1 2 3}

 puts "u > v? [all {$u>$v}]"
 set u {2 3 4 1}
 puts "u > v? [all {$u>$v}]"

 # Measure the time ...
 #
 proc check {u v} {
     set result 0
     foreach u1 $u v1 $v {
         if { ! ($u1 > $v1) } {
             set result 0
             break
         }
     }
     return $result
 }

 puts "Do some timing ..."
 set dummy [check $u $v]

 foreach len   {3     10   30   100 300 1000 3000 10000} \
         times {10000 3000 1000 300 100 30   10   3    } {
     set u {}
     set v {}
     for {set i 0} {$i < $len} {incr i} {
         lappend u [expr {2+rand()}]
         lappend v [expr {rand()}]
     }
     puts "Length = $len: [time {all {$u>$v}} $times] - [time {check $u $v} $times]"
 }

 # Note: all_simple breaks if we do this:
 #    set u 0
 #    all_simple {$u>$v}
 #

 puts "How about a combination of lists and scalar variables?"
 set u 0.1
 puts "v > 0.1? [all {$v>$u}]"
 set u -0.1
 puts "v > -0.1? [all {$v>$u}]"

The output from the script:

 u > v? 1
 u > v? 0
 Do some timing ...
 Length = 3: 86 microseconds per iteration - 3 microseconds per iteration
 Length = 10: 88 microseconds per iteration - 5 microseconds per iteration
 Length = 30: 95 microseconds per iteration - 12 microseconds per iteration
 Length = 100: 118 microseconds per iteration - 36 microseconds per iteration
 Length = 300: 182 microseconds per iteration - 103 microseconds per iteration
 Length = 1000: 408 microseconds per iteration - 337 microseconds per iteration
 Length = 3000: 1080 microseconds per iteration - 1065 microseconds per iteration
 Length = 10000: 3465 microseconds per iteration - 3538 microseconds per iteration
 How about a combination of lists and scalar variables?
 v > 0.1? 0
 v > -0.1? 1

See also Vector arithmetics

... and also fold, filter, map, and zip which are higher-order functions for invoking operations on every member of a list in this way. For instance, the "all" function above can be rewritten as:

 proc invoke {cmd args} { uplevel #0 $cmd $args }
 proc zipWith {op xs ys} {
     set ret [list]
     foreach x $xs y $ys { lappend ret [invoke $op $x $y] }
     return $ret
 }
 proc foldl {op id xs} {
     foreach x $xs { set id [invoke $op $id $x] }
     return $id
 }
 proc > {a b} { expr {$a > $b} }
 proc and {a b} { expr {$a && $b} }
 proc all {op xs ys} { foldl and 1 [zipWith $op $xs $ys] }
 # Then, e.g.:
 all > $u $v

[ Category Numerical Analysis | Category Mathematics ]