Wednesday, May 15, 2013

RIFL Examples

In the last post, I introduced a pattern I call RIFL, or "Resource Is Function-Local".  The goal is to control access to a resource in such a way that there is a guarantee that it is released exactly once.  The idea is that the resource never exists in the wild.  It is created within a function, and can only be used when passed into another function which is a parameter of the first.  That's a little confusing, but let me jump into some examples which should make it clearer.

First, here is a RIFL wrapper around writing files in Go.  Note that a function declaration in Go is something like "funcName(argName argType) returnType".

package rifl
import "os"

type File struct{ file *os.File }

func WithFile(fileName string, useFile func(file File) error) error {
    file, err := os.Create(fileName) // create or truncate, open for writing
    if err != nil {
        return err
    }
    defer file.Close() // see note below
    return useFile(File{file})
}

func (file File) WriteString(s string) (int, error) {
    return file.file.WriteString(s)
}

I think this example is fairly readable for those who don't know Go.  The only thing that really needs explaining is the "defer" statement.  "defer" basically means: put the rest of the function in a "try" block, and put this in the "finally" clause.  That is, it contains a function call which is not executed until the rest of the function is completed; but is guaranteed to execute, even if there is exception (which Go calls a "panic").

And this is how the rifl package might be used, with poor error handling:

package main
import "rifl"

func main() {
    rifl.WithFile("out.txt", func(file rifl.File) error {
        file.WriteString("Hello\n")
        file.WriteString("Goodbye\n")
        return nil
    })
}

Go is a really good language in many ways, but if you're used to C++'s destructors, you'll miss them mightily.  RIFL can help you feel a bit better.

Note that in the main package, there are no references to os.File, only to rifl.File; we're not using the raw resource, just the RIFL wrapper.  I only implemented WriteString, but I could have implemented the entire interface, or (in principle) improved it.  Or made it worse.  Also, unlike my abstract example in the previous post, the Obtain function (os.Create) and the Use function (WriteString) both take arguments and return multiple values.  So it's a bit noiser.

I deliberately made the useFile argument the last argument to WithFile, because it tends to run on over many lines.

The most important point, of course, is that, in the WithFile function, we guarantee that the file will be closed, by using "defer".  It doesn't matter how many goats are sacrificed while running "useFile"; the file will get closed.

Also, the os.File object "file" is hidden from users.  In Go, identifiers with an initial uppercase letter (File, WithFile, WriteString) are exported; others (file) are not.  This means the caller has no way to call Close().  But that's fine, since WithFile handles that.

Now for something completely different.  Here's a RIFL wrapper for a database transaction in Perl, annotated (comments after # signs) so that those who don't know Perl can (I hope) follow it:

package XAct; # class
use Moose;

has '_dbh' => (is => 'ro'); # private data

sub Atomically { # RIFL function
    my ($dbh, $func) = @_;
    my @results;
    $dbh->begin_work; # start transaction
    eval { # try
    my $wrap = XAct->new(_dbh => $dbh);
    @results = $func->($wrap); # in transaction
    $dbh->commit;
    };
    if ($@) { # catch
    $dbh->rollback;
    die $@; # rethrow
    }
    return @results;
}

sub prepare { # pass-through Use
    my ($self, @args) = @_;
    return $self->_dbh->prepare(@args);
}

1; # end package

The RIFL function Atomically guarantees that the transaction is in effect when the passed-in function, $func is called; and that it is cleaned up afterwards.  A transaction, unlike a typical resource, can be Released either by a Commit or by a Rollback.  Atomically guarantees that if $func does not throw, the transaction is committed; and if it does throw, the transaction is rolled back.  So, this is an additional complexity easily handled by RIFL.

As before, the transaction object (which is really the the database handle) is wrapped to prevent direct access to the commit or rollback.

Note that the database handle itself is a resource, which could also be managed by a RIFL function, but that is not included in this example.

Here is an example function using Atomically:

sub InsertSomeNames {
    my ($dbh, %map) = @_;
    my $sql = 'INSERT INTO SomeNames(Id, Name) VALUES(?,?)';
    XAct::Atomically($dbh, sub {
    my ($xact)=@_; # get resource
    my $sth = $xact->prepare($sql); # use directly
    while (my ($id, $name) = each(%map)) {
        $sth->bind_param(1, $id);
        $sth->bind_param(2, $name);
        $sth->execute; # use indirectly
        }
    });
};

Here the transaction is being passed to the inner function as $xact, which is being used to insert many values into the database.  If any of those inserts fail, all of them will be rolled back; they will only be committed if they all succeed.

The point of this post is that the RIFL pattern is pretty easy to implement and pretty easy to use in many languages.  It relies only on good anonymous functions (lambda functions), used when calling the RIFL function.  Of course, you also have to be able to reasonably manage the resource so that it is guaranteed to be freed at the end of the RIFL function; but it seems like that's a fundamental prerequisite to being able to manage resources in any way in the language.

In the next post, I'll compare RIFL with RAII and show some C++ examples.

1 comment:

  1. Pieter Droogendijk points out that Go does have finalizers, although they only run when the object is garbage collected (or rather, when it would be collected, as the finalizers keeps it from being immediately collected).

    ReplyDelete