More compact "for" loop

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. This version now runs entirely in the enclosing scope, so you can access the counter without difficulty.

  proc do {var range loop} {   # syntax x..y<,x1..y1><xStep>
    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 "<"}
    set script "for \{! $var $from\} \{\$$var $cond $to\} \{incr $var $by\} \{$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.

LarrySmith: True. I am probably channeling my childhood experience as a little baby programmer starting with Pascal... =) Although Pascal actually used "to" or "downto" keywords for the step, but they were optional. This way is more compact (but a little slower as noted) with the x$by parameter being parsed out - the step is often left off, whereas with parameters the step cannot be omitted since the last parameter must be the script. I supppose it might be put at the end, after the script, but for a large loop, putting the little afterthought of the step maybe on the next page just seems...odd. I was less concerned about speed and more about shrinking more utilities down to one-liners. I also alias set to "!" (which led to the idea of using the pencil in Unicode Commands. Hey, I never denied I was eccentric. ;)


ak - 2017-05-03 17:20:55

Fair enough.

I would most likely do(sic!) this via:

  1. Use args after the fixed part (var from to).
  2. Check the number of arguments (1 or 2, i.e. with and without step)
  3. Take the last argument as the script
  4. Take the possible left-over as the optional step, or compute a default step.

See example below, which integrates the last 3 items of the list into one switch command.

proc do {var from to args} {
    switch -exact -- [llength $args] {
        1 {
            # ... compute default step ...
        }
        2 {
            set step [lindex $args 0]
        }
        default {
            error ...
        }
    }
    set script [lindex $args end]
    ...
}

Could be done with keywords as well, similar to processing of leading options, just without dashes.