Timothy R. Butler
Timothy R. Butler

Reputation: 1139

Access Fully Qualified Variable Name from Variable in Perl Strict Mode

I have a series of modules that perform output functions for my scripts. Sometimes the module is called directly -- it is called View -- and sometimes a child class that extends it is used instead (View::ChildName). View declares our $THEMENAME = 'default'; when it is loaded, but the child declares its own specific $THEMENAME when it loads.

Problem: When new() is called on a child theme, it calls my $self = $class->next::method(%params); (using mro) to get some core things set by the parent class before extending it. One of the core bits is that the parent class sets $self->{'themeName'}. However, if it simply calls $THEMENAME, it gets the parent's setting: "default."

The only way I've reliably and successfully solved this is to turn off strict temporarily and do this:

my $packName = ref $self;

{ 
    no strict;
    $self->{'themeName'} = ${${packName} . "::THEMENAME"};
}   

This works, but in profiling the code, if objects are created frequently, this actually adds more overhead than I expected. I tried the alternative of always using the parent's package name, e.g. the child sets $View::THEMENAME. This works, but only if the theme name is set within new and not on the load of the module; if it is on load, there is erratic behavior if several different child objects (of different children) are created over the course of the script.

These options both seem less than ideal. Is there a good way to do this? The only thing I found was this old question and I think incorporating Moo would probably add more overhead than I'm trying to avoid by getting rid of my current no strict block. Has anything been added to more modern versions of Perl that might solve my issue?

The alternative is to dodge the issue all together and simply set $self->{'themeName'} within each child object's new method, although I'm trying to avoid that change since there's a fair number of legacy child classes that expect $THEMENAME to exist.


Minimal reproducible example of View.pm:

use strict;
package View;

our $THEMENAME = 'default';

sub new {
    my $class = shift;
    my $params = shift;
    my $self = { 'setting' => $params{'setting'} };

    bless $self, $class;

    $self->{'themeName'} = $THEMENAME;
    return $self;  
}

And of View/Child.pm:

use strict;
use mro;
package View::Child;

use parent 'View';

our $THEMENAME = 'child';

sub new {
    my $class = shift;
    my $params = shift;
    my $self = $class->next::method($params);

    bless $self, $class;

    say STDOUT $self->{'themeName'};
    # Prints 'default' not 'child'.

    return $self;  
}

Now a script to call it:

use View::Child;
my $object = View::Child->new();

If you added the first code block to View.pm, it gives the expected result instead, but seems to add about 9 ms to each call to new -- more than the time it takes for it to handle everything else I have in the much longer full length new method -- which adds up if the program runs many iterations:

use strict;
package View;

our $THEMENAME = 'default';

sub new {
    my $class = shift;
    my $params = shift;
    my $self = { };

    bless $self, $class;

    my $packName = ref $self;

    { 
         no strict;
         $self->{'themeName'} = ${${packName} . "::THEMENAME"};
    }  
    return $self;  
}

Upvotes: 3

Views: 206

Answers (3)

Timothy R. Butler
Timothy R. Butler

Reputation: 1139

My final solution follows what @plentyofcoffee and @ikegami outlined, but I wanted to see if I could come up with a way to set it automatically without each child module implementing it (keeping in mind legacy code). Assuming the child does want to set it, it passes $param{'themeName'} to the parent's constructor, which sets it to $self->{'themeName'}. If themeName is undefined, I came up with this regex in the parent class that extracts the name of the child as a fallback themeName:

unless ($self->{'themeName'}) {
    state $getThemeNameRegEx = qr#^SAFARI::(.*::)+(.*?)$#; 
    $class =~ /$getThemeNameRegEx/;
    $self->{'themeName'} = $2 // "default"; 
}

This sets to default if the name doesn't contain at least two levels below SAFARI, e.g. SAFARI::View is default (the parent module is in use without a child) and SAFARI::View::mysite is mysite.

Upvotes: 1

ikegami
ikegami

Reputation: 386481

The concept of class properties is one you should forget (in Perl). It's fine for the module to have constants and possibly variables, but they shouldn't be considered part of the class.

I see four approaches you could take:

  • Update the property in the child's constructor.
  • Provide the value as a parameter
  • Override the accessor
  • Provide the default as a method

Update the property in the child's constructor

# View.pm
sub new {
    my ($class, $params) = @_;
    my $self = bless({}, $class);

    $self->{ setting   } = $params->{ setting };
    $self->{ themeName } = 'default';

    return $self;  
}

sub themeName { $_[0]->{themeName} }  # Optional
# Child/View.pm
sub new {
    my ($class, $params) = @_;
    my $self = $class->next::method($params);

    $self->{ themeName } = 'child';

    return $self;
}

Provide the value as a parameter

# View.pm
sub new {
    my ($class, $params) = @_;
    my $self = bless({}, $class);

    $self->{ setting   } = $params->{ setting   };
    $self->{ themeName } = $params->{ themeName } // 'default';

    return $self;  
}

sub themeName { $_[0]->{themeName} }  # Optional
# Child/View.pm
sub new {
    my ($class, $params) = @_;
    my $self = $class->next::method({ themeName => 'child', %$params });
    return $self;
}

Override the accessor

# View.pm
sub new {
    my ($class, $params) = @_;
    my $self = bless({}, $class);

    $self->{ setting } = $params->{ setting };

    return $self;  
}

sub themeName { 'default' }
# Child/View.pm

# No need to override `new`.

sub themeName { 'child' }

Provide the default as a method

# View.pm
sub new {
    my ($class, $params) = @_;
    my $self = bless({}, $class);

    $self->{ setting   } = $params->{ setting };
    $self->{ themeName } = $class->defaultThemeName;

    return $self;  
}

sub defaultThemeName { 'default' }

sub themeName { $_[0]->{themeName} }  # Optional
# Child/View.pm

# No need to override `new`.

sub defaultThemeName { 'child' }

Upvotes: 3

plentyofcoffee
plentyofcoffee

Reputation: 488

One potential solution would be to add THEMENAME as an overridable method.

View.pm:

use strict;
package View;

our $THEMENAME = 'default';

sub THEMENAME {return 'default'}

sub new {
    my $class = shift;
    my $params = shift;
    my $self = { 'setting' => $params->{'setting'} };

    bless $self, $class;

    $self->{'themeName'} = $self->THEMENAME;
    return $self;  
}

View/Child.pm:

use strict;
use mro;
package View::Child;

use parent 'View';

our $THEMENAME = 'child';

sub THEMENAME {return 'child'}

sub new {
    my $class = shift;
    my $params = shift;
    my $self = $class->next::method($params);

    bless $self, $class;

    say STDOUT $self->{'themeName'};
    # Prints 'default' not 'child'.
    #
    return $self;  
}
# perl -Mlib=. -MView::Child -e 'View::Child->new()'
child

Upvotes: 2

Related Questions