in reply to 'local' for imported variables doesn't work as expected

Exporter tells you what not to export:

> Exporting variables is not a good idea. They can change under the hood, provoking horrible effects at-a-distance that are too hard to track and to fix. Trust me: they are not worth it.

The right way is to export the accessors to the variable rather than the variable itself. This makes local useless, so we have to find a different way. For example, we can use Scope::Guard:

package Dsay; use warnings; use strict; use feature qw{ say }; use experimental qw( signatures ); use Scope::Guard qw{ guard }; use Exporter qw{ import }; our @EXPORT = qw( %debug set_darea get_darea dsay ); our %debug; my $d_area = '(default)'; # Not ours anymore! sub set_darea($value) { my $old_value = $d_area; $d_area = $value; return guard { $d_area = $old_value } } sub get_darea() { $d_area } # Etc.
and
sub common_sub { say 'call to common sub (d_area is ', get_darea(), ')'; dsay "debugging output from common_sub"; } sub one { say "call to one:"; my $_scope = set_darea('one'); dsay "debugging output from one"; common_sub; dsay "more debugging output from one"; } sub two { say "call to two:"; my $_scope = set_darea('two'); dsay "debugging output from two"; common_sub; dsay "more debugging output from two"; }
Note that if you don't store the result of set_area to a variable, the module will complain:

Can't create a Scope::Guard in void context

Aside from doing it the right way, you can create the alias to the package variable yourself:

use Dsay; package Dsay; our $d_area; package main;

or

*d_area = *Dsay::d_area;

which can be done by extending Exporter's import:

use Exporter; our @EXPORT = qw( %debug dsay $d_area ); our %debug; our $d_area = "(default)"; sub import { my $caller = caller; no strict 'refs'; *{ $caller . '::d_area' } = *Dsay::d_area; goto &Exporter::import }

Interestingly, using something other than a scalar works the way you wanted.

use Exporter qw{ import }; our @EXPORT = qw( %debug %opt dsay ); our %debug; our %opt = (d_area => "(default)"); sub dsay( @args ) { return unless $debug{ uc $opt{d_area} } || $debug{'ALL'}; my $prefix = $opt{d_area} ne "(default)" ? "DEBUG $opt{d_area}: " : "DEBUG: "; say $prefix, @args; }
and
sub one { say "call to one:"; local $opt{d_area} = "one"; dsay "debugging output from one"; common_sub; dsay "more debugging output from one"; } # Etc.

Update: The reasons for this are unclear to me (maybe local for hash values was implemented later than local for variables, so there are probably some differences in the implementation).

Update 2: Extending the import method added.

Update 3: In fact, what exporter does is quite similar:

*{"${callpkg}::$sym"} = $type eq '&' ? \&{"${pkg}::$sym"} : $type eq '$' ? \${"${pkg}::$sym"} : $type eq '@' ? \@{"${pkg}::$sym"} : $type eq '%' ? \%{"${pkg}::$sym"} : $type eq '*' ? *{"${pkg}::$sym"} : do { require Carp; Carp::croak("Can't export symbol: $type +$sym") };
and if you try the same instead in the redefined import method, it doesn't work either. There's something more than the scalar slot that needs to be aliased, not sure why. The following works, too:
*{ $caller . '::d_area' } = 'Dsay::d_area';

Update 4: In fact, adding the following line after Exporter::Heavy's block seems to fix the behaviour of Exporter without breaking anything I have tried:

*{"${callpkg}::$sym"} = "${pkg}::$sym" if '$' eq $type;

map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]

Replies are listed 'Best First'.
Re^2: 'local' for imported variables doesn't work as expected [4 updates]
by ikegami (Patriarch) on Oct 04, 2024 at 14:05 UTC

    The reasons for this are unclear to me

    In the hash case, local replaces the SV in one hash. You then access the SV via the same hash.

    In the scalar case, local replaces the SV in one package. You then access the SV found in a different package.

      Proof that it works the same for scalars in hashes as with scalars in packages:

      $mod::d_area = "(default)"; *d_area = \$mod::d_area; # Create alias in *main:: { local $d_area = "one"; # Replace scalar say $mod::d_area; # (default) $mod::d_area = "two"; say $d_area; # one } say $d_area; # two
      my %mod = ( d_area => "(default)" ); my %main; \$main{ d_area } = \$mod{ d_area }; # Create alias in %main { local $main{ d_area } = "one"; # Replace scalar say $mod{ d_area }; # (default) $mod{ d_area } = "two"; say $main{ d_area }; # one } say $main{ d_area }; # two
Re^2: 'local' for imported variables doesn't work as expected (1 update)
by LanX (Saint) on Oct 04, 2024 at 11:54 UTC
    Tl;Dr , but I don't think there is anything wrong with Exporter it's just the way local works. (See my other post which replicates the problem without Exporter)

    It's localizing the symbol NOT the referenced variable.

    Otherwise it would be possible to localize lexical variables too.

    Update

    Demo in the debugger

    DB<13> my $x = 42; *b = \$x; {local $b= 666; say "$b = $x"}; say "$b = + $x" 666 = 42 42 = 42

    Now exporting typeglobs works because the scalar slot is one level deeper, hence local will operate on the same slot

    Cheers Rolf
    (addicted to the Perl Programming Language :)
    see Wikisyntax for the Monastery

Re^2: 'local' for imported variables doesn't work as expected
by LanX (Saint) on Oct 04, 2024 at 22:10 UTC
    > maybe local for hash values was implemented later than local for variables

    Back in the days when I first learned that local also works for hash elements, I was very surprised. (How random! Why? ¹)

    But now that I realize that every local is actually operating on hash elements, this finally makes a lot of sense. :)

    (stash= symbol table hash, and a typeglob is very similar to a hash, just with a fixed number of slots)

    Cheers Rolf
    (addicted to the Perl Programming Language :)
    see Wikisyntax for the Monastery

    Updates

    ¹) And most confusingly, why does it suddenly work for lexical hashes too???

    DB<2> my %h=(a=>1); { local $h{a}=2; say %h}; say %h a2 a1