Emulating Dynamic Scope with Lexical Destruction

| 2 Comments

This past week I've been writing a lot of tests for a project that has little test coverage. Fortunately it's a young project and it's not too messy, but adding tests after the fact is always more difficult than designing with tests from the start.

One of the pieces of this system uses introspection of package global variables to define class options. (It doesn't use Moose, at least not yet.) In other words, you might see something like:

package MyProj::Item;

our $lifetime    = 'request';
our $persistence = 'nosql';

The parent class of all of these children inspects those attributes and constructs instances based on their values. In some of the tests I've decided to manipulate those values to get as much coverage as possible before we rewrite pieces of the code. That means manipulating those global variables.

The problem with global variables is, as always, action at a distance. If I write to one in one place, what effect do I have on code which reads from it in other places? What effect do I suffer from code which also modifies it in other places?

One of the useful uses of Perl 5's local is to allow you to change the value of a global variable within a delimited lexical scope. One of my favorite uses of this technique is when debugging an array of items, to change the list delimiter global variable:

    local $" = '] [';
    diag "[@_]";

I use that technique sparingly in tests, because it's slightly more magical than I want my tests to get. In this case, because there are so many of these global variables ("many" meaning "more than three") and because other people will work on this test suite too, I wanted to encapsulate changes to these globals behind a nice interface. I try never to make messes in my code, but I'm even more careful when I know other people will have to work with it.

This presented an interesting problem: how do you localize changes to global variables and hide that localization behind a nice interface?

I ended up with an interface that looks like this:

sub test_some_behavior :Tests
{
    my $self  = shift;
    my $guard = $self->localize_options(
        lifetime    => 'singleton',
        persistence => 'session_store',
    );

    ...
}

... and all of the magic is in localize_options(), which works something like:

sub localize_options
{
    my ($self, %args) = @_;

    my @restore;

    while (my ($var, $new_value) = each %args)
    {
        my $ref = do
        {
            no strict 'refs';
            \${ $package . '::' . $var };
        };

        push @restore, [ $ref, $$ref ];
        $$ref = $new_value;
    }

    bless \@restore, 'RestoreOnDestroy';
}

package RestoreOnDestroy;

sub DESTROY
{
    my $self = shift;

    for my $restore (@$self)
    {
        my ($ref, $old_value) = @$restore;
        $$ref = $old_value;
    }
}

This method loops through all of the variable names, grabs references to the global variables, and then saves the references and the original values. Then it overwrites those values with the new values. Finally it blesses the array of references and old values into a very specific class.

That class does only one thing: when it goes out of scope, its finalizer iterates through that array and restores all of the old values. (You can see a generalization of this pattern in Scope::Guard, for example.

The consumer of this interface only has to remember two things: receive the guard object returned from the method and don't let it escape the smallest scope desired. All of the knowledge of manipulating Perl 5's symbol tables is in a single place (and strict gets disabled for only a single line of code). Better yet, if the interface to declaring these attributes changes from package globals to something else, only this method has to change at all, and only in a limited way.

All of this comes from the discipline of treating code in test suites with the same care as other code. The desire to create a clean interface is just as important—and encapsulation often more so—when writing code to demonstrate that what you expect really does happen the way you expect.

Also it's interesting to emulate one feature you can't use through another.

2 Comments

First of all, I have been reading your blog and book updates continuously. Thanks for your great work. I try to practice some of your ideas in Farabi and Padre. I have a couple of comments:

- Any chance you could use a clearer Perl syntax highlighting scheme for the examples? More space on the top and bottom margin?
- Being a non-native English speaker, I am sometimes amazed with the blog's overall language complexity.

Thanks :)
And Keep the awesome work

You could replace

my $ref = do
        {
            no strict 'refs';
            \${ $package . '::' . $var };
        };

with

my $ref = Package::Stash->new($package)->get_symbol($var);

although it isn't really worth it in this case.

Modern Perl: The Book

cover image for Modern Perl: the book

The best Perl Programmers read Modern Perl: The Book.

sponsored by the How to Make a Smoothie guide

Categories

Pages

About this Entry

This page contains a single entry by chromatic published on October 10, 2012 9:37 PM.

BEGIN-time Initialization versus Testing was the previous entry in this blog.

Test::Class versus Modern Perl OO is the next entry in this blog.

Find recent content on the main index or look in the archives to find all content.


Powered by the Perl programming language

what is programming?