http://qs1969.pair.com?node_id=535859

While trying to embed a domain-specific language into Perl, I came across an interesting problem involving local subroutines. In this meditation, I will build up to the problem and then explain how I solved it. If you can think of a better solution, please let me know.

One trick that is useful in certain limited circumstances is to define (or redefine) a subroutine locally, in effect overriding any original definition that may exist. For example, let's say I have the following subroutines:

use warnings; use strict; sub a { "unchanged" }; sub print_a { print a(), "\n" };
I can temporarily override the definition of a like so:
no strict 'refs'; no warnings 'redefine'; { local *a = sub { "changed" }; print_a(); # prints "changed" }
The extent of the change is limited to the dynamic scope of the block in which the local-ized assignment is made. If I call print_a from outside of the block, its call to a will invoke the original "unchanged" definition:
print_a(); # prints "unchanged"
Let's say I want to do this kind of temporary overriding frequently. I can create a helper subroutine to make the process more convenient:
sub localize_a_and_call_fn(&@) { my ($fn, @args) = @_; local *a = sub { "changed" }; $fn->(@args); }
Now I can run any code I want within the scope where a is temporarily overridden:
localize_a_and_call_fn( \&print_a ); # prints "changed" localize_a_and_call_fn { print "a() => ", a(), "\n"; }; # prints "a() => changed"
That's great.

But let's say I want to take it one step further (which, in fact, I did). Let's say I want to write a more general helper that lets me temporarily override any given list of subroutines – say a, b, and c. My first attempt went like this:

sub localize_and_call_fn { my ($locals, $fn, @args) = @_; local *$_ = sub { "changed" } for @$locals; $fn->(@args); }
That seems simple enough. Unfortunately, the code does not work:
localize_and_call_fn( [qw(a b c)], \&print_a ); # prints "unchanged"
The problem seems to be the for modifier on the simple statement that attempts to localize the given subroutines. Even though perlsyn does not say so, it appears that the simple statement to which the modifier is attached is evaluated within an implicit block, at least as far as local is concerned. It's as if the statement had been written like this:
# for (@$locals) { # local *$_ = sub { "changed" }; # }
None of the local changes can escape the for loop, and thus by the time the helper subroutine invokes $fn->(@args), the original definitions of a, b, and c have been restored. The invoked subroutine will never see the changes.

I could not think of any way to use a simple loop to make local changes for a given list of symbols. By using nested anonymous subroutines, however, I was able to do it. (One could also use explicit recursion.) Here's the code I used:

sub localize_and_call_fn_2 { my ($locals, $fn, @args) = @_; for my $sym (@$locals) { my $f = $fn; $fn = sub { local *$sym = sub { "changed" }; $f->(@_); } } $fn->(@args); } localize_and_call_fn_2( [qw(a b c)], \&print_a ); # prints "changed"
The for loop in the new helper function wraps anonymous subroutines around the seed of code given in $fn. Each of the wrappers overrides a single symbol's definition and then passes control the next wrapper. The last wrapper invokes the original seed of code. In effect, the call to localize_and_call_fn_2 above gets converted into the following code:
# (sub { # local *c = sub { "changed" }; # (sub { # local *b = sub { "changed" }; # (sub { # local *a = sub { "changed" }; # (\&print_a)->(@_); # })->(@_) # })->(@_) # })->();
It seems like a roundabout way to accomplish what ought to be easy, but it works. Can you think of a better way?