scope exit

D programming language has an interesting construct, scope(exit), that arranges something to be done when leaving the enclosing block.

{
  auto fd = File("/dev/null");
  scope(exit) { close(fd); }
  ...
}

The same thing is added easily to TCL:

proc scope(exit) {args} {
    upvar {___ scope guard} guard
    set guard ""
    trace add variable guard unset \
        [list %scope(exit)onTrace [uplevel 1 {namespace current}] $args]
}
proc %scope(exit)onTrace {nativeNamespace command name1 name2 op} {
    namespace eval $nativeNamespace $command
}
proc test {} {
    set channel [open [info nameofexecutable] r]
    scope(exit) close $channel
    scope(exit) puts "Bye-bye. Channel closed: $channel"
}

It can be argued that try ... finally or catch ... return may do the same, but the advantage of scope(exit) is that a clean-up command for each resource may be declared near the place where the resource is allocated. Thus using scope(exit) may improve code readability.

Two or three bad things about this hack:

  • When the cleanup command is called, the stack frame of the enclosing procedure is already deleted. Things as scope(exit) { close $fd } are impossible: $fd substitution can not happen in scope exit handler
  • Errors from the scope exit handlers are silently ignored.
  • In TCL, a loop/if/eval/catch/etc. body does not have its own scope. Because of this, scope(exit) semantics may be unobvious here:
while {$something} {
   open something...
   scope(exit) cleanup something ;# doesn't work as expected
}

NEM A similar approach is taken in the generator package I wrote recently [L1 ]. In that package, there is a generator finally command that can be used to register cleanup to be performed when the generator is destroyed, which is a form of scope-based cleanup. This simplified the cleanup code, as then you don't need any complex signalling or exception handling mechanism to ensure that resources are cleaned up even in the case of early termination of a generator. For example:

generator define lines file {
    set in [open $file]
    generator finally close $in ;# ensure cleanup on exit
    while {[gets $in line]} { generator yield $line }
}
generator foreach line [lines /etc/passwd] {
    puts [format "%4d| %s" [incr lineNum] $line]
}

A/AK I also began to value this hack with the appearing of coroutines. It's interesting (and sad) that it's now the only way to clean up something when coroutine is deleted. try { yield } finally { cleanup } doesn't provide it. And it's not something that can be fixed easily. If there would be some catchable unwinding when coroutine is deleted, it would lead to as many potential bugs as it would fix. Making try .. finally do something different than catch could be a better way, but it would break current semantics of catch as an universal exception-handling primitive.

NEM That's just the semantics of coroutines: they are non-local jumps. The same issue occurs in use of e.g., vwait: try { vwait forever } finally { cleanup } . Tracing the destruction of the coroutine is the best way to handle this.

A/AK The issue with vwait is not strictly the same. Each vwait either runs or unwinds easily; though it may run forever, it's no more a problem than an infinite loop running in the same try.

BTW, I have a proof-of-concept implementation of try ... finally that does cleanup on coroutine destruction:

proc try-sooo-hard {script "finally" something } {
    set guard ""
    trace add variable guard unset "[list uplevel \#[expr {[info level] - 1}] $something];#"
    catch {uplevel 1 $script} rv ro
    return -options $ro $rv
}
proc test-hard-try {{yieldfromtheinside 0}} {
    try-sooo-hard {
        puts "Setting a variable"
        set fd 1
        try-sooo-hard {
            puts "Nested hard try"
            set v 123
            if {$yieldfromtheinside} {yield}
        } finally {
            puts "Finally for the nested try"
            puts "with v = $v"
        }
    } finally {
        puts "Entered finally handler"
        puts "Seeing fd = $fd in finally handler"
    }
}
proc test-co-hard-try {} {
    coroutine ::running hard-try 1
    rename ::running ""
}

NEM 2009-10-01: OK, I see clearer the problem you are trying to solve. It's not that the yield may never return (which is the same for any command), but rather that the coroutine might be destroyed, removing some of the stack without properly unwinding. As you point out, variable traces are one way to cope with this. The other approach (used in the generator package) is to trace the destruction of the coroutine itself, via a command trace. It might be a good idea to change the implementation of try/finally in the core to ensure that this case is covered.