Tcl: A Custom For Loop

The Problem

I often need to loop through a list of items and need both the index position, and the item itself. Typically:

set names {John Paul George Ringo}
set i 0
foreach name $names {
    puts "$i - $name"
    incr i
} 

Output:

0 - John
1 - Paul
2 - George
3 - Ringo

Initial Solution

Since I frequently do this, I decided to implement my own loop and call it for_item_index for lack of creativity. Here is my loop and a short code segment to test it:

proc for_index_item {index item iterable body } {
    uplevel 1 set $index 0
    foreach x $iterable {
        uplevel 1 set $item $x
        uplevel 1 $body
        uplevel 1 incr $index
    }
}

# Test it
set names {John Paul George Ringo}
for_index_item i name $names {
    puts "$i - $name"
}

I have tested it with break, and continue and found my new loop performs as expected. My concern is the excessive use of uplevel command in the code.

Here are my own review of my code:

  • Excessive use of uplevel – I don’t know what to do about it. If you have an idea, please send it my way.
  • The index always starts at zero. There are times when I want it to start at 1 or some other values. To add that feature, I will probably introduce another parameter, startValue
  • Likewise, the index always get incremented by 1. The user might want to increment it by a different values such as 2, or –1 to count backward. Again, introducing another parameter, step might help, but at this point, the loop is getting complicated.

Revised Solution

Shortly after the initial solution, I posted it on StackExchange’s CodeReview site. While waiting for comments, I started to revise the initial solution to include the start- and increment values. Here is the revised solution and code snippets to demonstrate:

proc for_index_item {indexexpr itemvar iterable body} {
    # Break down indexexpr
    set step 1
    set start 0
    if {[llength $indexexpr] == 3} {
        set step [lindex $indexexpr 2]
    }
    if {[llength $indexexpr] >= 2} {
        set start [lindex $indexexpr 1]
    }
    upvar 1 [lindex $indexexpr 0] index
    set index $start

    upvar 1 $itemvar item

    # Actual loop
    foreach item $iterable {
        uplevel 1 $body
        incr index $step
    }
}

#
# Test
#

puts "\nDefault, start with 0"
set names {John Paul George Ringo}
for_index_item i name $names {
    puts "  $i - $name"
}

puts "\nStarts from 1"
set names {John Paul George Ringo}
for_index_item {i 1} name $names {
    puts "  $i - $name"
}

puts "\nWith start value and increment"
set names {John Paul George Ringo}
for_index_item {i 110 10} name $names {
    puts "  $i - $name"
}

Output:

Default, start with 0
  0 - John
  1 - Paul
  2 - George
  3 - Ringo

Starts from 1
  1 - John
  2 - Paul
  3 - George
  4 - Ringo

With start value and increment
  110 - John
  120 - Paul
  130 - George
  140 - Ringo

A few comments:

  • I took in a suggestion and used upvar instead of uplevel, which makes the logic a little cleanner.
  • Half of my code is now devoted to parsing
  • The word index is miss-leading: it has nothing to do with the index as in a list index. It is just part of a sequence of numbers. In the future, I might rename it to remove confusion.

2 thoughts on “Tcl: A Custom For Loop

  1. Nagarajan Chinnasamy

    My be you can use upvar, to reduce the number of uplevel:

    proc for_index_item {indexvar itemvar iterable body } {
    upvar $indexvar index
    upvar $itemvar item

    set index 0
    foreach x $iterable {
    set item $x
    uplevel 1 $body
    incr index
    }
    }

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s