Version 13 of Iterator using Closures

Updated 2002-08-31 04:53:24

An example of implementing iterators using Closures. This is just an experiment!!! -- Todd Coram

The code below (using code from Closures) allows us to create a forward iterator command in pure Tcl that works with strings, lists, channels and ranges of numbers. This way we can write truly "generic" functions in Tcl that operate on the above without knowledge of type.

Scroll to the bottom for examples.

 make-closure-proc input_iterator {_type _target} {
    variable type $_type
    variable target {$_target}
    variable pos -1
    variable end -1
 } {
    variable type;
    switch -glob -- $type {
        -str* {
            set end [string length $target]
            set nextbody {
                incr pos
                if {$pos >= $end} {
                    return {}
                }
                return [string index $target $pos]
            }
        }
        -li* {
            set end [llength $target]
            set nextbody {
                incr pos
                if {$pos >= $end} {
                    return {}
                }
                return [lindex $target $pos]
            }
        }
        -chan* {
            set end {}
            set nextbody {
                if {[eof $target]} {
                    return {}
                }
                return [gets $target]
            }
        }
        -rang* {
            set end [lindex $target 1]
            set pos [expr {[lindex $target 0]  - 1}]
            set nextbody {
                if {$pos >= $end} {
                    return {}
                }
                return [incr pos]
            }
        }
        default {
            error "iterator: bad option \"$type\": must be -string, -list, -channel or  -range"
        }
    }
    # if you use the lambda hack (from [Closures]):
    #   lambda {} "variable target; variable pos; variable end; $nextbody"
    # otherwise the below 2 lines will work too..
    proc next {} "variable target; variable pos; variable end; $nextbody"
    return [namespace current]::next
 }

Here are a few examples:

 set i1 [input_iterator -string "hello world"]
 set i2 [input_iterator -list {hello world}]
 set i3 [input_iterator -channel [open somefile.txt r]]
 set i4 [input_iterator -range {1 100}]

 proc count {iter} {
   set count 0
   while {[$iter] != {}} {
     incr count
   }
   return $count
 }

 count $i1 ;# returns 11
 count $i2 ;# returns 2
 count $i3 ;# returns the number of characters in the file.
 count $i4 ;# returns 100

We can now define a useful generic function:

 proc map {iter cmd} {
    set res {}
    while {[set v [$iter]] != {}} {
        lappend res [eval $cmd $v]
    }
    return $res
 }

Which can be used as follows:

  proc double {x} {expr {$x * 2}}
  map [input_iterator -range {0 10}] double
  map [input_iterator -list {1 2 3 4 5}] double
  map [input_iterator -chan stdin] double ;# double every number entered on the console (1 per line)
  map [input_iterator -string "12345"] double ;# this doesn't do what you may expect ;-)

The map proc looks better using lambda:

  map [input_iterator -range {0 10}] [lambda x {expr {$x * 2}}]

Here is a rewrite of map that works with iterators or lists:

 proc map {iter_or_list cmd} {
    set res {}
    if {[info command $iter_or_list] == {}} {
        foreach v $iter_or_list {
            lappend res [eval $cmd $v]
        }
    } else {
        while {[set v [$iter_or_list]] != {}} {
            lappend res [eval $cmd $v]
        }
    }
    return $res
 }

 map {1 2 3 4} double
 map [input_iterator -range {1 4}] double
 map [input_iterator -chan stdin] double

Todd Coram-- Things get more interesting when you define output iterators. Its starts to look a bit more like STL. Given:

 make-closure-proc output_iterator {_type _target} {
    variable type $_type
    variable target $_target
 } {
    variable type;
    switch -glob -- $type {
        -str* {
            set nextbody {
                append target_ref $val
            }
        }
        -li* {
            set nextbody {
                lappend target_ref $val
            }
        }
        -chan* {
            set nextbody {
                puts $target $val
            }
        }
        default {
            error "output_iterator: bad option \"$type\": must be -string, -list, or -channel"
        }
    }
    proc out {val} \
        "variable target; upvar \$target target_ref; $nextbody"
    return [namespace current]::out
 }

The generic functions get more interesting:

 proc copy {in out} {
    while {[set v [$in]] != {}} {
        uplevel $out $v
    }
 }

 proc iter_map {in out cmd} {
    while {[set v [$in]] != {}} {
        uplevel $out [eval $cmd $v]
    }
 }

Here are some examples:

 copy [input_iterator -chan $file1] [output_iterator -chan $file2]
 copy [input_iterator -chan $file1] [output_iterator -list l]
 copy [input_iterator -range {0 10000}] [output_iterator -chan stdout]

 iter_map [input_iterator -string "1234"] [output_iterator -string dbl_s] double
 iter_map [input_iterator -chan stdin] [output_iterator -list dbl_l] double