muthm has asked for the wisdom of the Perl Monks concerning the following question:

Praised monks,

I have experienced an unexpected behavior of 'local'.
I am trying to attach the scope of the value of a global variable to the current lexical block (e.g. a function), meaning that upon exit of that block, the previous value is restored.
I think this is exactly the intended use case for local.

The reason why I am doing this is to create a module with subroutines for selective debugging output. Kind of like this:

#!/usr/bin/env perl use v5.20; use warnings; use feature 'signatures'; no warnings 'experimental::signatures'; # ------ Debugging function definition: our %debug; our $d_area = "(default)"; sub dsay( @args ) { return unless $debug{ uc $d_area } || $debug{'ALL'}; my $prefix = $d_area ne "(default)" ? "DEBUG $d_area: " : "DEBUG: "; say $prefix, @args; } # ------ Application program: sub common_sub { say "call to common sub (d_area is $d_area)"; dsay "debugging output from common_sub"; } sub one { say "call to one:"; local $d_area = "one"; dsay "debugging output from one"; common_sub; dsay "more debugging output from one"; } sub two { say "call to two:"; local $d_area = "two"; dsay "debugging output from two"; common_sub; dsay "more debugging output from two"; } # Switch on debugging for area 'two' # (I'll use command line switches with Getopt::Long later). $debug{TWO} = 1; say "hello from main"; dsay "debugging output from main"; one; two;
which creates this output:
hello from main call to one: call to common sub (d_area is one) call to two: DEBUG two: debugging output from two call to common sub (d_area is two) DEBUG two: debugging output from common_sub DEBUG two: more debugging output from two

This works perfectly.
Note that the debugging output of common_sub depends on the localized value of the $d_area variable.

But now I want to put the debugging output function and variables into a module, export everything correctly, like this:

# # Dsay.pm # package Dsay; use parent 'Exporter'; our @EXPORT = qw( %debug $d_area dsay ); our %debug; our $d_area = "(default)"; sub dsay( @args ) { return unless $debug{ uc $d_area } || $debug{'ALL'}; my $prefix = $d_area ne "(default)" ? "DEBUG $d_area: " : "DEBUG: "; say $prefix, @args; } 1;
and replace the definitions in the main program by a simple
use Dsay;
The result is that I don't get any debugging output anymore.

I understand that importing $d_area creates an entry in the symbol table of the importing module (here: main), which references the variable in the Dsay module.

And after a long while of contemplating (and debugging, and reading, and experimenting) I think I also understand that local $d_area = "..."; creates a new temporary variable to hold the new value, and — also temporarily — changes the $d_area symbol to be a reference to that new variable (and value).
The symbol that is changed is the one in the current package's symbol table, because $d_area is unqualified.
Which means that the symbol — and the value! — in the Dsay module is completely unaffected, and not substituted!
This explains the unexpected behavior.

Surely, to remedy this, I could use

local $Dsay::d_area = '...';
in all places, but I would have liked to avoid to use the fully qualified name, for easier typing and faster debugging.

After all, the unqualified $d_area refers to the variable within the Dsay module, which menas that any change done here is seen there, and any change done there is seen here.
But local cuts that correspondence!

Do the monks know of any incantation (or magic spell?) so that local $d_area modifies the original imported variable, not only the local one?
Could there be any benefit in adding an explicit import function to the Dsay module, and/or playing around with explicit variable references like \$Dsay::d_area to that effect?

Thank you for your enlightenment!

Replies are listed 'Best First'.
Re: 'local' for imported variables doesn't work as expected (4 updates)
by choroba (Cardinal) on Oct 04, 2024 at 09:57 UTC
    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]

      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
      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

      > 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
Re: 'local' for imported variables doesn't work as expected
by LanX (Saint) on Oct 04, 2024 at 10:41 UTC
    Hmm not sure what's wrong in your setting...

    I ran a quick test on my mobile, and this works for me.

    Probably an issue with Exporter ?

    use v5.12; use warnings; BEGIN{ package log; our $var= "default"; sub tst { say $var } # export *::var = *var; *::tst = *tst; } package main; our $var; { local $var = "foo"; tst(); } tst();

    perl local_import_var.pl foo default

    Anyway I'd rather recommend another design, like using caller to get the function's name or setting a regex to parse the message for keywords.

    HTH :)

    EDIT

    AHH, got it. What exporter does is to only export the scalar slot, which is causing your problem.

    *::var = \$var; # replace!

    Effect:

    perl local_import_var.pl default default

    So exporting the whole typeglob will fix it.:).

    In case exporter doesn't support * , use your own import

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

      LanX wrote:
      So exporting the whole typeglob will fix it.:).

      In case exporter doesn't support * , use your own import

      This perfectly does the job!

      I replaced

      our @EXPORT = qw( %debug $d_area dsay );
      by
      our @EXPORT = qw( %debug *d_area dsay );
      in Dsay.pm.

      I now can use

      sub one { local $d_area = "one"; dsay "debugging output from one"; } sub two { local $d_area = "two"; dsay "debugging output from two"; } $debug{TWO} = 1; one; two;
      and get debugging output from the 'two' area only. Debugging output stops as soon as the localized $d_area value drops out of scope.
      And this without the need of qualifying $Dsay::d_area anymore!

      Thank you so much! This really helps, and I have learned a lot!
        > Thank you so much!

        You're very welcome! :)

        > This really helps, and I have learned a lot!

        Great. It's nice to hear that once in a while ... :)

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

Re: 'local' for imported variables doesn't work as expected (updates 1)
by LanX (Saint) on Oct 04, 2024 at 11:13 UTC
    TIMTOWTDI

    instead of exporting the variable you can just check it directly in the caller's namespace. (If it even exists )

    use v5.12; use warnings; BEGIN{ package log; our $var= "default"; sub tst { my $pkg= (caller)[0]; no strict 'refs'; say ${"${pkg}::var"} } # export *Other::tst = \&tst; } package Other; our $var = __PACKAGE__; { local $var = "foo"; tst(); } tst();

    foo Other
    This might give your debugging interesting options...

    Update

    An alternative to messing with strict refs is to explicitly dig thru the stashes...

    say ${$::{"${pkg}::"}{"var"}};

    ... Tho beauty is in the eye of the beholder ;)

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

Re: 'local' for imported variables doesn't work as expected
by muthm (Sexton) on Oct 06, 2024 at 22:37 UTC
    Thank you, praised monks, for your valuable input!

    I have solved the issue by changing

    our @EXPORT = qw( %debug $d_area dsay );
    to
    our @EXPORT = qw( %debug *d_area dsay );
    in Dsay.pm.

    I now can use debugging code like

    sub one { local $d_area = "one"; dsay "debugging output from one"; }
    without the need of qualifying $Dsay::d_area anymore.

    Debugging output for the specified area stops as soon as the localized $d_area value drops out of scope.
    This is exactly what I wanted.

    Thank you for sharing your knowledge!