Version 26 of List Comprehension

Updated 2009-11-14 03:13:08 by Duoas

Python can now do it, oh why oh why can't tcl?

List Comprehensions is a short hand way to construct lists from other lists.

From Haskell: The Craft of Functional Programming by Simon Thompson:

In a list comprehension we write down a description of a list in terms of the elements of another list. From the first list we generate elements, which we test and transform to form elements of the result.

Playing Haskell has an interesting Tcl take on list comprehensions. Here is another:

 # Synopsis:
 # listc expression vars1 <- list1 [.. varsN <- listN] [condition].
 #
 proc listc {expression var1 <- list1 args} {
    set res {}

    # Conditional expression (if not supplied) is always 'true'.
    #
    set condition {expr 1}

    # We should at least have one var/list pair.
    #
    lappend var_list_pairs  $var1 $list1

    # Collect any additional var/list pairs.
    #
    while {[llength $args] >= 3} {
      lappend var_list_pairs [lindex $args 0]
      # skip "<-"
      lappend var_list_pairs [lindex $args 2]
      set args [lrange $args 3 end]
    }

    # If there are no more triplets and at least 1 arg left...
    #
    if {[llength $args] > 0} {
      # Grab condition
      #
      set condition [lindex $args 0]
    }


    # Build the foreach commands (for each var/list pair).
    #
    foreach {var list} $var_list_pairs {
      append foreachs "foreach \{$var\} \{$list\} \{"
    }
    # Insert the conditional expression.
    #
    append foreachs {if {[eval $condition]} {lappend res [eval $expression]}}


    # For each foreach, make sure we terminate it with a closing brace.
    #
    foreach {var list} $var_list_pairs {
      append foreachs \}
    }

    # Evaluate the foreachs...
    #
    eval $foreachs

    return $res
 } 

Here are some examples:

 set i [list 1 2 3 4 5 6]

 set l2 [listc {set i} i <- $i]
 puts "\tA copy of the list: $l2"

A copy of the list: 1 2 3 4 5 6

 set dbl [listc {expr $n*2} n <- $i]
 puts "\tDouble values from list: $dbl"

Double values from list: 2 4 6 8 10 12

 set evn [listc {set i}  i <- $i {expr {$i%2 == 0}}]
 puts "\tOnly even numbers: $evn"

Only even numbers: 2 4 6

 proc digits {str} {
     set lstr [split $str ""]
     return [listc {set d} d <- $lstr {string is digit $d}]
 }

 puts "Just digits from (703)-999-0012= [digits (703)-999-0012]"

Just digits from (703)-999-0012= 7 0 3 9 9 9 0 0 1 2

 set names1 [list Todd Coram Bob Jones Tim Druid]
 set lf [listc {list "$l,$f"} {f l} <- $names1]
 puts "From ($names1)\n\tLast,first = $lf"

From (Todd Coram Bob Jones Tim Druid) Last,first = Coram,Todd Jones,Bob Druid,Tim

 set l3 [listc {set f} {f l} <- $names1 {string match "T*" $f}]
 puts "From ($names1)\n\tOnly names starting with 't': $l3"

From (Todd Coram Bob Jones Tim Druid) Only names starting with 't': Todd Tim

Now, let's get fancy. Given a proc like lisp's setp and a simple lambda:

 proc lset {vars {values {}}} {
    if {$values == {}} {
      foreach var $vars {
        lappend values [uplevel set $var]
      }
      return $values
    }
    uplevel [list foreach $vars $values break]
    return $values
 }

 proc lambda {arglst body} {
    set name [string map {" " _ $ _} [info level 0]]
    proc $name $arglst $body
    set name
 }

You can get even more functional:

 set names2 [list {Todd Coram} {Bob Jones} {Tim Druid}]
 set lf [listc {[lambda nm {lset {f l} $nm; return "$l,$f"}] $n} n <- $names2]
 puts "From ($names2)\n\tAnother last,first = $lf"

From ({Todd Coram} {Bob Jones} {Tim Druid}) Another last,first = Coram,Todd Jones,Bob Druid,Tim

This listc proc also supports multiple (nesting) list variables:

 set l4 [listc {list $n1 $n2} n1 <- [list a b c] n2 <- [list 1 2 3]]
 puts "Create a matrix pairing {a b c} and {1 2 3}: \n\t$l4"

Create a matrix pairing {a b c} and {1 2 3}: {a 1} {a 2} {a 3} {b 1} {b 2} {b 3} {c 1} {c 2} {c 3}

-- Todd Coram


RS has this sugary list comprehension:

 proc all {varName "in" list "where" condition} {
    upvar 1 $varName var ;# import into local scope
    set res {}
    foreach var $list {
        if {[uplevel 1 expr $condition]} {lappend res $var}
    }
    set res
 }
 % all  i in {1 0 3 -2 4 -4} where {$i>1}
 3 4
 % all  k in {1 0 3 -2 4 -4} where {$k<1}
 0 -2 -4

This allows calls somehow resembling SQL:

 % array set income {Jim 1234 Jack 2345 Bill 3456789}
 % all i in [array names income] where {$income($i) > 2000}
 Jack Bill

See also Nested-loop join


DKF: Ah, so it is a striding cross between the map and filter functional operations?


AMG I made an alternative, lcomp, which constructs its command line using a different method.


See also another list comprehension


Duoas I very much like RS's all there. Here is another version which, while a little less pretty, gives you the ability to bind on multiple variables (much like a single-list foreach):

 proc all {varNames "in" list "where" condition} {
    foreach varName $varNames {
        upvar 1 $varName $varName
    }
    set result {}
    foreach $varNames $list {
        if {[uplevel 1 expr $condition]} {
            set ls [list]
            foreach varName $varNames {
                lappend ls [set $varName]
            }
            lappend result $ls
        }
    }
    return $result
 }

And a simple example:

 % array set income {Jim 1234 Jack 3456 Bill 3456789}
 % all {name salary} in [array get income] where {$salary > 2000}
 {Jack 3456} {Bill 3456789}

Notice how there is nothing wrong with using the same name for the local variable as the external frame's variable when using upvar. Also notice that the original example works the same:

 % all i in [array names income] where {$income($i) > 2000}
 Jack Bill

(I have specifically avoided constructs available only in modern versions of Tcl.)


[ Category Concept | Arts and crafts of Tcl-Tk programming ]