While it's still true that lexical scope is the fundamental unit of encapsulation in Perl 5, dynamic scope is a powerful tool.
Consider this snippet from a Catalyst application controller:
sub activate :Chained('superget') :PathPart('activate') :Args(0)
{
my ($self, $c) = @_;
my $proj = $c->stash->{project};
try
{
$proj->activate_project( $c->stash->{user} );
$c->add_status( 'Activated project ' . $proj->project_name );
}
catch { $c->add_error( $_ ) };
$self->redirect_to_action( $c, 'view', '', [ $proj->id ] );
}
Ignore almost everything but the Try::Tiny code (the
try
/catch
blocks). The dynamic scope of this
exception handling code means any exception thrown from either method called
from the try
block or any other code they call, will result in the
activation of the catch
block. Outside of those two blocks, any
exceptions are the responsibility of something else.
That's easy to understand, but take it a step further and think of this dynamic scope as some sort of multiverse behavior. (If this metaphor doesn't work, that's fine. It's how I think of it.) Within a delimited scope representing program flow, not source code, the universe has changed.
Exceptions aren't the only use for dynamic scope, and even dynamic scopes
offer the possibility for encapsulation.I wrote some interesting code the other day while refactoring out
accumulated duplication and improving the test coverage of the controller. My
goal was to test the error handling if the activation failed. This presented a
design opportunity: how could I force a failure of the activation? As you can
see from the controller, the project object is already in the stash. I
could override Catalyst's dispatch mechanism or internals to create a
dummy project object which dies on activation. I could extract these controller
tests into tests which don't go through the web interface. I could even use an
environment variable to change the behavior of
$project->activate
to throw an error.
Instead, I used an injected monkeypatch. The resulting test looks like:
test_result_override
{
$ua->get_ok( 'http://localhost/projects/2/activate',
'failed activation' );
$ua->content_contains( 'Activation failed',
'... should contain error message' );
} Project => activate_project => sub { die 'Activation failed' };
$ua
is an instance of Test::WWW::Mechanize::Catalyst.
You can probably see where this is going. Within the block passed to
test_result_override
, the Project
object's
activate_project
method is the function passed as the final
argument—a function which throws an exception.
The monkeypatch injector was likewise relatively easy to write:
sub test_result_override(&@)
{
my ($test, $class, $subname, $sub) = @_;
no strict 'refs';
my $ref = "Appname::Schema::Result::${class}::${subname}";
local *{ $ref };
*{ $ref } = $sub;
$test->();
}
The function prototype of &@
tells the Perl 5 parser to treat
the block as a function reference. The rest of the code merely performs the
monkeypatching with local
(so that the monkeypatch will go away
when this function returns, however it returns).
Once I knew what this code should do (I wrote the code which used it first), it took two minutes to write, and maybe five minutes to use in the other places in this controller I needed it. This isn't always the best approach, but this sufficiently encapsulated monkeypatch injection is sufficiently powerful and useful for helping ensure that my code behaves as I intended.
This idiom is fairly common in Emacs lisp.
It's usually a macro that temporarily changes _something_ during the execution of a piece of code you pass in. The naming convention for this is with-*, e.g.
* with-temp-buffer
* with-output-to-string
* with-demoted-errors
I quite like it, and have used it in Perl to e.g. encapsulate the ugly setting of global %ENV variables that change behaviour elsewhere.
/J