Using expr on lists

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 utilizes [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.

Update: I made a small, subtle and somewhat silly mistake - the original catch statement read:

   if { [catch $expr result] } { ...

instead of

   if { [catch {$expr} result] } { ...

The implicit eval done by catch caused the command to fail every single time (it tried to fill in the variables u and v and therefore failed). Now the note about [catch] is no longer valid: it was I who failed, not [catch].

The moral: use [catch] with care - you may easily end up catching too many errors.


 # 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 (revised) script:

 u > v? 1
 u > v? 0
 Do some timing ...
 Length = 3: 5 microseconds per iteration - 3 microseconds per iteration
 Length = 10: 8 microseconds per iteration - 5 microseconds per iteration
 Length = 30: 14 microseconds per iteration - 12 microseconds per iteration
 Length = 100: 39 microseconds per iteration - 36 microseconds per iteration
 Length = 300: 104 microseconds per iteration - 105 microseconds per iteration
 Length = 1000: 332 microseconds per iteration - 353 microseconds per iteration
 Length = 3000: 1037 microseconds per iteration - 1034 microseconds per iteration
 Length = 10000: 3418 microseconds per iteration - 3458 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

AM Hm, the message I failed to convey here is that my [all] procedure works on all kinds of expressions as long as the variables referred to are either one-level lists of two or more elements or scalars. The construction with [catch] makes sure that [makeListProc] is called to (re)construct the underlying procedure, if this is the first time this particular expression is used or if the "type" of the variables has changed (causing [expr] to fail).

So, [all {$u+$v>$w}] will work as well.

But I have noted there are quite a few Wiki pages concerned with the subject. Is it time to consolidate this inside some Tcllib module?


AM Yet another page on lists and operations on their elements: Looking at LISP's SERIES extension


schlenk There are some functions in tcllib for things like this, see the tcllib struct::list module.


  '''one''' and '''all''' boolean operators

HaO 2011-05-02 An implementation (using the double-evaluation of expr of its argument) of one (true if one true) and all (true if all true) list operators.

Given a list l with values interpreted as booleans:

set l {1 8 true no}

a check for at least one true may be done by:

expr [join $l ||]

If the empty list should not cause an error:

expr [join [linsert $l end 0] ||]

The same for any:

expr [join $l &&]

and

expr [join [linsert $l end 1] &&]