TIP 629: Add a range command to the core of list commands

Login
Bounty program for improvements to Tcl and certain Tcl packages.
    Author:        Eric Taylor <[email protected]>
    State:         Draft
    Type:          Project
    Vote:          Pending
    Created:       28-06-2022
    Tcl-Version:   8.7

Abstract

This TIP proposes that a new list command, range, be added to the core. The command would be similar to the lrepeat command, in that it would conveniently produce a list, given some arguments. The command would take a range of numbers (integer or float) and produce a list. It would be most useful in a foreach loop, but could have other uses as well, such as when using the "in" or "ni" operators in an expression or if command.

Rationale and Discussion

Often one wants to iterate on a list of numbers, and the current most popular choices are using a foreach or for command. The for command is a throwback to the C for statement, and is somewhat ugly and can be difficult to read. It is also prone to off-by-one errors.

For ease of programming, especially when performance is not that important, I propose to add to the core a command, range, which would provide a utility that is often found in other languages as a keyword or built-in function. For example, Python permits a range of numbers to be entered easily and has syntax to iterate over all the values in a range.

Proposal

The range command would take the following forms:

range start .. end ?by? ?step?
range start to end ?by? ?step?
range start # count ?by? ?step?

The ".." and the words "to" and "by" would be filler words to make the command more readable. These could be optional, similar to how the word else is optional, but I have not given that much thought.

If operated on integers, the results should be exact. If using floating values, there could be some allowable round-offs that may or may not be undesirable to the programmer, who would be cautioned in the manual entry.

The most obvious use might be in a foreach loop. Instead of this to write the numbers 1 through 10,

   for {set i 1} {$i <= 10} {incr i} {
      puts $i
   }

one could instead write:

   foreach i [range 1 .. 10] {
      puts $i
   }

This would likely reduce the possibility of a programming error, where the programmer used < instead of <=.

It could also be used in an if statement, like so:

 if {$i in [range 2 .. 10]} {puts inside} else {puts outside}

The command would understand when to create a list of numbers that are decreasing, by the "start" and "end" values or by using the optional by and step arguments.

The # operator would indicate the desire to create N elements, starting at some value, with an optional increment (either positive or negative, with a default of 1).

Examples

% range 10 .. 1
10 9 8 7 6 5 4 3 2 1

% range 1 .. 10
1 2 3 4 5 6 7 8 9 10

% range 10 .. 1 by 2 ;# or this could be an error - or not
10 8 6 4 2

% range 10 .. 1 by -2
10 8 6 4 2

% range 5.0 to 15.
5.0 6.0 7.0 8.0 9.0 10.0 11.0 12.0 13.0 14.0 15.0 

% range 5.0 to 25. by 5
5.0 10.0 15.0 20.0 25.0

% range 25. to 5. by 5 ;# ditto - maybe this is an error
25.0 20.0 15.0 10.0 5.0

% range 25. to 5. by -5
25.0 20.0 15.0 10.0 5.0

% range 1 to 10 by 2 ;# note, 10 cannot be in such a list, but allowed
1 3 5 7 9

% range 1 to 10 by -2
range: invalid step = -2 with a = 1 and b = 10

% range 25. to -25. by -5
25.0 20.0 15.0 10.0 5.0 0.0 -5.0 -10.0 -15.0 -20.0 -25.0

% range 5 # 5
5 6 7 8 9

% range 5 # 5 by 2
5 7 9 11 13

% range 5 # 5 by -2
5 3 1 -1 -3

Implementation

The following is a pure Tcl prototype of the command. It also uses a command incrx which is an incr that can take a floating value.

Ultimately, it would be preferable if this were implemented in C code, for the best possible performance.

proc incrx {arg {arg2 1}} {
    upvar $arg n
    set n [expr {   $n + $arg2   }]
}

proc range {a op b {by by} {step 1}} {
    if       { $op eq
".." || $op eq "to"} {
        if { $a > $b &&
$step > 0 } {
            set
step [expr {   0 - $step   }]
        }
        if { $step == 0 || ($step <
0 && $a <= $b) || ($step > 0 && $b < $a)} {
            error
"range: invalid step = $step with a = $a and b = $b"
        }
        if { $by ne "by" }
{
            error
"range: unknown term for by : $by"
        }
        set step [expr {  
abs($step)   }]
        if { $a <=  $b } {
            incrx
a [expr {   0-$step   }]
            lmap b
[lrepeat [expr {int(   ($b-$a) / $step   )}] 0]
{incrx a $step}
        } else {
            incrx
a $step
            lmap b
[lrepeat [expr {int(   ($a-$b) / $step   )}] 0]
{incrx a -$step} 
        }
    } elseif { $op eq "#" } {
        incrx a [expr {  
0-$step   }]
        lmap b [lrepeat [expr
{   int($b)   }] 0] {incrx a $step}
    } else {
        error "unknown range op
$op"
    }
    
}

Compatibility

Adding any command to the core would risk the possibility that some program might have chosen to write a proc using the new name. However, for new programs, the programmer would likely accept this limitation.

Copyright

This document has been placed in the public domain.