Version 13 of More compact "for" loop

Updated 2017-05-02 21:02:56 by RKzn

Typing out a "for" loop is just getting to be a drudge. Eventually, I decided to just tighten it up a bit.

The syntax is "do var range script" where "range" is specified like 1..10 - and you can add an 'x' and a step if needed: 1..10x2. It will also count backwards if the starting value is higher than the ending one, changing the sign of the step value for you.

 do i 0..10 {puts "value of i is $i"}

 do j 10..0x2 {puts "value of j is $j}

"break" and "continue" work as expected, since it's just translated to the equivalent "for" loop

I use split to handle the parsing and ran into a nasty limitation I didn't find at all obvious. Split splits on any of the split characters, it has no concept of using some character combination to split. So I fixed that, too, adding a means of translating multiple character strings in a list as a way to split. I called it "chop". It has a limit of no more than 9 different strings to use for the operation since I could only figure out 9 regular characters to map to that I thought wouldn't interfere.

Ideally, there should be a "parse" function to handle string->list conversions. In the case of ranges, something like "parse list {.. x} from to by" would handle parsing the range easily.

  proc parse {str markers args} {
    set l [chop $str $markers]
    set l# [llength $l]
    each item $l {> &var; set var $item; incr l# -1}
    while {${l#}>=0} {> &arg; set arg ""; incr l# -1} 
  }

Which uses:

 proc > {args} {
    upvar args argl
    while {[llength $args]>0} {
      set arg [lindex $args 0]; set args [lrange $args 1 end]
      set val [lindex $argl 0]; set argl [lrange $argl 1 end]
      if {[str1st $arg] eq "&"} {
        set arg [string range $arg 1 end]
        uplevel upvar $val $arg
      } else {
        upvar  $arg local
        set local $val
      }
    }
  }

and "do" itself:

  proc do {var range loop} {
    lassign [chop $range ".. x"] from to by
    set cond <
    if {$by eq ""} {set by 1}
    if {$to<$from} {set by [= {-$by}]; set cond >=}
    set script "for \{set $var $from\} \{\$$var$cond$to\} \{incr $var $by\} \{"
    set script "${script}$loop\}"
    uplevel $script
  }


  # fixing split - won't split using string of chars as markers
  set splitchars {! @ # % ^ * | ~ `}

  proc chop{list marks} {
    set sp $::splitchars; set map {}
    foreach mark $marks {set map "$map $mark [hd sp]"}
    set list [string map $map $list]
    return [split $list $::splitchars]
  }

EF: Where can we find map, ^^, each and ^?

Actually, they're built-ins. Forgot to remove my "smithisms" when I posted. Fixed above. I hope.

The command can also be written without special parsing commands:

proc do {varName range loop} {
    upvar 1 $varName var
    if {[scan $range {%d..%dx%d} from to by] < 3} {
        set by 1
    }
    if {$to < $from} {
        if {$by > 0} {
            set by -$by
        }
        set cond >=
    } else {
        set cond <
    }
    for {set var $from} [concat \$var $cond $to] {incr var $by} {uplevel 1 $loop}
}

ak - 2017-05-02 19:41:52

Tcllib provides a splitx command which is similar to chop. See textutil::split


ak - 2017-05-02 19:47:21

Question about the range syntax, i.e. 0..10. What where the reasons it was chosen ? I can currently see two disadvantages over specifying the range in two arguments, i.e. do 0 10 ..., which are related:

  1. The range is stored as string and has to be parsed every time into the two ints. Using two arguments the conversion to int happens once and is stored in the Tcl_Obj.
  2. When iterating over a dynamic range the it has to be constructed as string ("${from}..${to}"), only to be deconstructed immediately again in the procedure.