Playing with with

What's "with"

"With" is a construct that can automatically clean up "garbage" after finishing a task. It comes from Lisp. In Tcl it is used in one of two ways: with-new-foo name script or with-existing-foo foo script. In the former case the command with-new-foo creates a foo, sets the caller's variable name to the new foo through upvar, runs the script, and then destroys the foo. In the latter case the command only runs the script and destroys the foo afterwards. It is up to the user to acquire a foo first.

For example, open is usually combined with a chan close. A "with" construct will automatically close the channel (file descriptor). See withOpenFile and Category With Pattern.

Reasonable use of the "with" construct can make your code visibly clearer.

An implementation

proc getFinalByOp {op opret} {
    switch -exact -- $op {
        open {
            return [list chan close $opret]
        }
        default {
            return [list]
        }
    }
}

#
# op: a command
# opargs: @op's arguments
# body: code that will be executed
# ?_finally?: provide a 'finally' yourself
# ?varname?: result of @op
#
proc with {op opargs body {_finally {}} {varname handle} } {
    set finally $_finally
    try {
        set [set varname] [$op {*}$opargs]
        if {$finally eq {}} {
            set finally [getFinalByOp $op [set [set varname]]]
        }
        eval $body
    } finally {
        eval $finally
    }
}

This implementation only supports open, it's not extensive, and it's buggy (see Discussions below).

How to use it

with open {a.txt w} {chan puts $handle "hello world"}
with open {a.txt r} {puts [read $fd]} {} fd
with puts {"a test"} {set a {hello}} {puts $a}               ;# a meaningless example

A more complex example

Read in a file (employees.txt) of the following format,

name1,salary1
name2,salary2
name3,salary3

The real file is,

Mike Foo,10000
Jack Bar,2000
John Doe,3000

If salary < 3000, add 200, and in the end print the result, write it back.

A possible implementation (can be easier?),

# ... "with" here ...

with open {employees.txt r+} {
    set contents [chan read -nonewline $handle]
    foreach line [split $contents \n] {
        set list [split $line ,]
        if {[lindex $list 1] < 3000} {
            lset list 1 [expr {200+[lindex $list 1]}]
        }
        lappend result "[lindex $list 0],[lindex $list 1]"
    }
    chan seek $handle 0
    chan truncate $handle 0
    puts [join $result \n]
    chan puts -nonewline $handle [join $result \n]
}

Discussions

From the IRC channel,

<miguel> I am afraid that 'general with' will not work - not with bodies that try to use variables from the calling env
<miguel> ... nor with commands that resolv differently in the calling env and in the proc's namespace