5

It's a long title, but I'm afraid I can't take a single word out without losing the true meaning of the question. I'll give a quick description of what I'm trying to achieve first, then a long rambling on why I want it done this way. Skip the rambling if you intend to directly answer abiding with the question title :-)

Quick description

Assuming the existence of a lexicalize builtin that does just what I want, here goes:

package Whatever;
{
  lexicalize $Other::Package::var;
  local $var = 42;
  Other::Package->do_stuff; # depends on that variable internally
}

Whys and whynots

For a bit of context, I'm whitebox-testing a third-party module.

I want the variable localized because I want it changed for a limited time only, before the tests move on to something else. local is the best way I found to do this without depending on knowing the module's choice for an initial value.

I want a lexical binding for two main reasons:

  1. I don't want to pollute the test file's higher-level namespace.
  2. I want something shorter than the fully-qualified name. It's not in the sample code for brevity, but I use the identifier a lot more than what's shown, with calculations and updates relative to its previous value.

I couldn't decently use our because it won't grab a variable from another package: “No package name allowed for variable $Other::Package::var in "our".” Cheating to temporarily enter Other::Package's scope doesn't cut it: if I use a block ({ package Other::Package; our $var }) then the binding doesn't last long enough to be useful; and if I don't (package Other::Package; our @var; package main) then I need to know and copy the previous package's name, which prevents moving that piece of code around too much.

While doing my homework before asking a previous form of this question, I discovered Lexical::Var, which seemed like it would be exactly what I needed. Alas: “Can't localize through a reference.”

I've also tried my best on my gut feeling of my *var-based forms, but kept bumping into syntax errors. I've learned more than I cared to about scoping and binding today :-)

I can't think of a reason why what I want shouldn't be possible, but I can't find the right incantation. Am I asking for an unfortunate unimplemented edge case?

JB.
  • 40,344
  • 12
  • 79
  • 106

4 Answers4

3

If I get you right, all you need is our. Forgive me if I dont.

Here's working example:

#!/usr/bin/env perl
use strict;
use warnings;

package Some;

our $var = 42;
sub do_stuff { return $var }

package main;

{
    local $Some::var = 43;
    print "Localized: " . Some->do_stuff() . "\n";
};

print "Normal: " . Some->do_stuff() . "\n";

But if you try to localize private (my) variable of some package, you probably doing something wrong.

yko
  • 2,710
  • 13
  • 15
  • It works indeed: I'm currently already using a variation of this, for lack of something better. Where this fails to meet my own requirements is that it doesn't create a new (shorter) lexical binding: inside the block, we're stuck to using the fully-qualified `$Some::var` form of the variable instead of a nicer plain `$var`. My actual package name is quite a bit longer than that; this is what prompted my question in the first place. – JB. Oct 17 '11 at 20:41
  • Side note: this might be a side effect of your copy-pasting multiple files in a single code sample, but your `our $var = 42` line near the top will likely pollute anything you'd want to try in the target block later on. Better wrap package `Some` in a block. – JB. Oct 17 '11 at 20:45
3

there are several ways to do this:

  • given:

    {package Test::Pkg;
        our $var = 'init';
        sub method {print "Test::Pkg::var = $var\n"}
    }
    
  • alias a package variable from the current package to the target package.

    {
        package main;
        our $var;
    
        Test::Pkg->method; #     Test::Pkg::var = init
    
        local *var = \local $Test::Pkg::var;  
            # alias $var to a localized $Test::Pkg::var
    
        $var = 'new value';
    
        Test::Pkg->method; #    Test::Pkg::var = new value
    }
    
    Test::Pkg->method;     #    Test::Pkg::var = init
    
  • localize through a glob reference:

    {
        package main;
    
        my $var = \*Test::Pkg::var;  # globref to target the local
    
        Test::Pkg->method; #     Test::Pkg::var = init
    
        local *$var = \'new value';  # fills the scalar slot of the glob
    
        Test::Pkg->method; #    Test::Pkg::var = new value
    }
    
    Test::Pkg->method;     #    Test::Pkg::var = init
    
  • bind a the lexical in the target package, and let it spill into the current package:

    {
        package Test::Pkg;  # in the target package
        our $var;           # create the lexical name $var for $Test::Pkg::var
    
        package main;       # enter main package, lexical still in scope
    
        Test::Pkg->method; #     Test::Pkg::var = init
    
        local $var = 'new value';
    
        Test::Pkg->method; #    Test::Pkg::var = new value
    }
    
    Test::Pkg->method;     #    Test::Pkg::var = init
    

the last example uses a slightly awkward double package declaration to create a lexical in one package and get it into another. This trick is useful with local in other scenarios:

 no strict 'refs';
 local ${'Some::Pkg::'.$name} = 'something';
 use strict 'refs';
Eric Strom
  • 39,821
  • 2
  • 80
  • 152
  • Reading your answer after hobbs's (SO order), the same comment applies. Thanks a lot for your help! – JB. Oct 17 '11 at 21:59
  • @JB. => added a few more examples that don't pollute the namespace. – Eric Strom Oct 18 '11 at 04:53
  • Your second one, in addition to reminding me things I didn't want to remember on typeglobs (a leitmotiv in this question), seems like a sane basis for what I want to do. Tweaking a bit... – JB. Oct 18 '11 at 09:16
  • YES! Minor tweak on your second answer: `my $ref = \*Other::Package::var; local *$ref = \(my $var = f($$$ref));` (I need the localized variable non-constant, accessible for easy modifying as `$var`, and initialized as a function of the original value) This seems to fit all of my expressed and non-expressed (asking questions is *hard*) requirements. Congratulations! PS would you mind editing to bring that part of the answer to the top? (first answer non-lexical leak; third answer requires knowledge of enclosing package name) PPS glad to have found a mostly reasonable use for a `$$$form`. – JB. Oct 18 '11 at 09:34
2

I am not quite sure if I understand you correctly. Does this help?

#!/usr/bin/env perl

{
    package This;
    use warnings; use strict;
    our $var = 42;

    sub do_stuff {
        print "$var\n";
    }
}

{
    package That;
    use warnings; use strict;

    use Lexical::Alias;
    local $This::var;

    alias $This::var, my $var;

    $var = 24;
    print "$var\n";

    This->do_stuff;
}

package main;

use warnings; use strict;

This->do_stuff;
Sinan Ünür
  • 116,958
  • 15
  • 196
  • 339
  • Lexical::Alias... hasn't found that one while looking around. Seems promising. Trying it out, I'll let you know shortly. – JB. Oct 17 '11 at 21:21
  • Interesting stuff. You've just made me understand just a bit more of the internals than I wanted to. As far as my formulation of the question goes, it's valid (so I'll upvote and accept). Because I've been stupid and omitted a bit too much, it's not exactly what I wanted, but I'm only blaming myself now. – JB. Oct 17 '11 at 21:36
  • 1
    What I omitted: my `local` line is actually `local $pkg::var = f($pkg::var)`. `local` has to be done before the aliasing or it breaks it, so I'm left writing the huge fully-qualified name three times, which is a bit over what I was targetting. – JB. Oct 17 '11 at 21:44
2
package Other::Package;

$var = 23;

sub say_var {
  my $label = shift;
  print "$label: $Other::Package::var\n";
}

package Whatever;
Other::Package::say_var("before");
{
  local $Other::Package::var = 42;
  local *var = \$Other::Package::var;
  Other::Package::say_var("during");
  print "\$Whatever::var is $var inside the block\n";
}
Other::Package::say_var("after");
print "\$Whatever::var is ",
    defined($var) ? "" : "un", "defined outside the block\n";

Output:

before: 23
during: 42
$Whatever::var is 42 inside the block
after: 23
$Whatever::var is undefined outside the block

local $Other::Package::var makes the original value temporarily 42 for the duration of the block, and local *var = \$Other::Package::var makes $var in the current package an alias for $Other::Package::var temporarily for the duration of the block.

Naturally some of this isn't strict-compliant, but I avoided using our because our obfuscates the issue if both packages are in the same file (as they might be if you copy-pasted this sample) — the our declaration can leak out of the package it's used in, into a following package in the same file :)

hobbs
  • 223,387
  • 19
  • 210
  • 288
  • Thank you for your answer! It's not precisely exactly what I was looking for since it leaks the aliasing to *Whatever::var outside of the lexical scope (I'm thinking helper functions in Whatever). Thanks for taking the time to try and help me out, much appreciated. – JB. Oct 17 '11 at 21:58
  • @JB. ah, in that case Lexical::Alias is what you want (as Sinan said). I was trying to avoid that, since it's sort of a tricky beast. – hobbs Oct 17 '11 at 23:54