Version 4 of withOpenFile

Updated 2014-01-11 18:47:45 by PeterLewerin

Peter Lewerin 2014-01-11: with open file is an old programming pattern or idiom that abstracts the following:

  1. open a channel to a file
  2. do something with the channel
  3. close the channel

In Tcl code, e.g. on this wiki, this procedure is usually written step-by-step, usually without error handling (and sometimes without actually closing the file):

set f [open $myfile]
set txt [read $f]
close $f

This is tedious, verbose, over-specific, and error-prone. I can shave off a couple of characters while allowing the operation on channel to be specified at invocation, adding basic error handling, ensuring that the file will be closed, and making it a single-invocation one-liner. In fact, I will show two ways of doing that.

First variant

The idiom has been turned into a language element by Common Lisp, specifically as a Lisp macro, with-open-file (see CLHS ). A Tcl command along the lines of (NB not quite the functional equivalent of) the CL macro might look like this:

proc with-open-file {__varName __script args} {
    try {
        open {*}$args
    } on ok $__varName {
        try {
            eval $__script
        } on ok __scriptResult {
            # do nothing, just get scriptResult
        } on error {__result __options} {
            # do nothing, just collect result and options
        }
    } on error {__result __options} {
        # do nothing, just collect result and options
    } finally {
        # before we leave, close the file (if it was ever opened)
        if {[info exists $__varName]} { close [set $__varName] }
    }

    if {[info exists __options]} {
        # we have an exception, rethrow it
        return -options $__options $__result
    } else {
        return $__scriptResult
    }
}

Usage (the file foobar.txt does not exist):

% with-open-file f { read $f } foobar.txt
##! raises exception 'couldn't open "foobar.txt": no such file or directory'
% with-open-file f { puts $f foo } foobar.txt w
% with-open-file f { read $f } foobar.txt
foo

This command takes as arguments a ''variable name'', a ''script'' (the variable name can be dereferenced to a channel identifier when evaluating the script), and a ''list of arguments'' to be passed to the `[open]` command.  The `with-open-file` guards against exceptions both when opening the file and when evaluating the ''script'', and makes sure that the channel is closed even if an exception is raised.  On return, the command either rethrows any exceptions raised or returns the result of evaluating the ''script''.

I've used the non-standard `__xxx` form for the local variables to, if possible, avoid collisions with whatever the caller decides to call the variable that will hold the channel id.  To be really sure there are no collisions, consider implementing `getGloballyUniqueName`.

The example at the top of the page can now be written as:

set txt [with-open-file f { read $f } $myfile]

Second variant

I'm not quite satisfied with the above implementation. There's the name collision issue, and there's also the fact that the caller can't tell the command what to do in case of an exception. Hence this slightly different variant:

proc withOpenFile {func errfunc args} {
    try {
        open {*}$args
    } on ok f {
        try {
            apply $func $f
        } on ok scriptResult {
            # do nothing, just get scriptResult
        } on error {result options} {
            # do nothing, just collect result and options
        }
    } on error {result options} {
        # do nothing, just collect result and options
    } finally {
        # before we leave, close the file (if it was ever opened)
        if {[info exists f]} { close $f }
    }

    if {[info exists options]} {
        # we have an exception, try calling errfunc
        if {$errfunc ne {}} {
            apply $errfunc $result $options
        } else {
            # no errfunc lambda to call, rethrow the exception
            return -options $options $result
        }
    } else {
        return $scriptResult
    }
}

Usage (the file foobar.txt does not exist, no error function):

% withOpenFile {f {read $f}} {} foobar.txt
##! raises error 'couldn't open "foobar.txt": no such file or directory'
% withOpenFile {f {puts $f foo}} {} foobar.txt w
% withOpenFile {f {read $f}} {} foobar.txt
foo

With error handling (consider the file foobar.txt to be non-existing):

% withOpenFile {f {read $f}} {{result -} { puts "Oh dear, $result" }} foobar.txt
##- prints the text 'Oh dear, couldn't open "foobar.txt": no such file or directory'

This variant takes two lambda arguments (anonymous functions), func and errfunc to be called using the apply command (the second argument is allowed to be a (usually invalid) "null lambda" with the value {}) and a list of arguments to be passed to open as in the first variant. If the command manages to open the file, it calls func and passes the channel id to it as an argument. If anything goes wrong when opening the file or while evaluating func, the command checks to see if errfunc is a lambda (or at least not an empty string), and either calls errfunc with the exception data as arguments or just rethrows the exception. In either case, the file is closed before exiting the command.

The errfunc in this example doesn't use its second parameter (the return options dictionary), so I use the variable name - for it to make it more anonymous; it's just a personal convention.

The example at the top of the page can now be written as:

set txt [withOpenFile {f {read $f}} {} $myfile]

or like this:

set func {f {
    read $f
}}
set errfunc {{r o} {
    puts "The message is '$r' and the error code is {[dict get $o -errorcode]}."
}}
set txt [withOpenFile $func $errfunc $myfile]