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

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

Hi Monks! I'm looking for a bit of help, as someone who codes perl in isolation. Prefacing this with the fact i'm no programmer.

I've somewhat organically arrived upon my own little OO framework which i use for most things. It has grown in complexity a bit over the years and i'm now wondering if i'm just re-inventing the wheel, especially since i'm starting to get confused by my own code!

I can't use Moose or any of the more advanced frameworks, i'm stuck with pretty basic perl unfortunately

Essentially i have a structure of classes which allows me to dictate the way attributes are handled when creating downstream objects. I can explicitly define which attributes are allowed, what are valid values, and the behaviour around unknown/undefined attributes (drop the attribute, give a warning, error out etc

The way i'm doing this is starting to seem a bit messy, and i'm sure there's a better way. In the base class i maintain a configuration singleton. The singleton keeps a map of the derived class hierarchy, and when i set configuration options on a particular "level" of the hierarchy, it only affects from that point down, rather than reconfiguring the base class and hence the whole derived tree.

I'm probably going to struggle to illustrate it but here's a pseudocode example:

my::base # base class my::extended_trunk # extends base my::branch_A # extends "extended_trunk" my::branch_B # Also extends "extended_trunk" my::base->configure(options); # Options change in all Classe +s my::extended_trunk->configure(options); # Options only change in exten +ded_trunk and both branches my::branch_A->configure(options); # Options only change in branc +h_A

In essence, i'm doing all this to replicate a sort of "inheritable class variable". Is that something thats natively possible?

In other words, if my::extended_trunk has a class variable, can i change the value of it in my::branch_A, without it affecting the value in my::branch_B?

As always, i appreciate any help or guidance! Thanks!

Replies are listed 'Best First'.
Re: Reconfiguration of classes in an "inheritable" manner
by kcott (Archbishop) on Jun 24, 2022 at 11:40 UTC

    G'day Amblikai,

    "Is that something thats natively possible?"

    Yes. The code below requires a minimum Perl version of 5.10.0 (for the mro pragma).

    Update (correction): It actually needs a minimum version of 5.10.1 for the parent pragma; mro still needs a minimum of 5.10.0. I changed use 5.010; to use use 5.010_001; in the code.

    #!/usr/bin/env perl use 5.010_001; use strict; use warnings; package My::Base; use mro; our %conf; sub new { my ($class) = @_; bless {} => $class; } sub configure { my ($class, $opts) = @_; { no strict 'refs'; %{$class . '::conf'} = (%{$class . '::conf'}, %$opts); } $_->configure($opts) for @{mro::get_isarev($class)}; return; } package My::Trunk; use parent -norequire, 'My::Base'; our %conf; package My::Branch_A; use parent -norequire, 'My::Trunk'; our %conf; package My::Branch_B; use parent -norequire, 'My::Trunk'; our %conf; package main; # Everything after here is for demo purposes only. use Data::Dump; say '*** Normal instantiation'; say 'My::Base object: ', My::Base::->new(); say 'My::Trunk object: ', My::Trunk::->new(); say 'My::Branch_A object: ', My::Branch_A::->new(); say 'My::Branch_B object: ', My::Branch_B::->new(); say "\n*** My::Base::->configure({A => 1});"; My::Base::->configure({A => 1}); _show_configs(); say "\n*** My::Trunk::->configure({B => 2, C => 3});"; My::Trunk::->configure({B => 2, C => 3}); _show_configs(); say "\n*** My::Branch_A::->configure({A => 'me', D => 4});"; My::Branch_A::->configure({A => 'me', D => 4}); _show_configs(); say "\n*** My::Branch_B::->configure({B => 'me', E => 5});"; My::Branch_B::->configure({B => 'me', E => 5}); _show_configs(); sub _show_configs { say 'My::Base::conf'; dd \%My::Base::conf; say 'My::Trunk::conf'; dd \%My::Trunk::conf; say 'My::Branch_A::conf'; dd \%My::Branch_A::conf; say 'My::Branch_B::conf'; dd \%My::Branch_B::conf; }

    Note that all lowercase package names are generally reserved for pragmata (e.g. strict, warnings, etc.). I've replaced my::base with My::Base (and similarly for your other classes).

    Output:

    *** Normal instantiation My::Base object: My::Base=HASH(0x800003c98) My::Trunk object: My::Trunk=HASH(0x800003cc8) My::Branch_A object: My::Branch_A=HASH(0x800003c98) My::Branch_B object: My::Branch_B=HASH(0x800003cc8) *** My::Base::->configure({A => 1}); My::Base::conf { A => 1 } My::Trunk::conf { A => 1 } My::Branch_A::conf { A => 1 } My::Branch_B::conf { A => 1 } *** My::Trunk::->configure({B => 2, C => 3}); My::Base::conf { A => 1 } My::Trunk::conf { A => 1, B => 2, C => 3 } My::Branch_A::conf { A => 1, B => 2, C => 3 } My::Branch_B::conf { A => 1, B => 2, C => 3 } *** My::Branch_A::->configure({A => 'me', D => 4}); My::Base::conf { A => 1 } My::Trunk::conf { A => 1, B => 2, C => 3 } My::Branch_A::conf { A => "me", B => 2, C => 3, D => 4 } My::Branch_B::conf { A => 1, B => 2, C => 3 } *** My::Branch_B::->configure({B => 'me', E => 5}); My::Base::conf { A => 1 } My::Trunk::conf { A => 1, B => 2, C => 3 } My::Branch_A::conf { A => "me", B => 2, C => 3, D => 4 } My::Branch_B::conf { A => 1, B => "me", C => 3, E => 5 }
    "I can't use Moose or any of the more advanced frameworks, i'm stuck with pretty basic perl unfortunately"

    In that case, you may not have Data::Dump. To run the demo, change that to Data::Dumper and the dd ...; lines to print Dumper ...;.

    See also: "perlootut - Object-Oriented Programming in Perl Tutorial" and "perlobj - Perl object reference".

    — Ken

Re: Reconfiguration of classes in an "inheritable" manner
by NERDVANA (Deacon) on Jun 24, 2022 at 02:25 UTC

    Yes you're re-inventing things that Moose already solved, but only you can determine whether that is worth your time, because I do believe that there is room for improvement in all of Perl's object frameworks, and perhaps you enjoy exploring other options.

    As to your question, Perl's packages are each a data structure which you can inspect using this notation:

    my $pkg_stash= do { no strict 'refs'; \%{ $pkg_name . '::' } };

    To clarify, that is a global hash variable whose name starts with the package name and ends with '::'. (Once you get a reference to that stash, you can turn strict refs back on and keep using it.) Inside that hash, each value is a Perl Globref (if you're unfamiliar with globrefs, see perldoc perlref) You can walk that tree of stashes to find out which packages have a variable defined in them. Beware that on some early versions of perl, it might be impossible to tell the difference between a $Package::Foo that is assigned undef vs. a $Package::Foo that was never declared.

    It sounds like you might also have re-invented the inheritence tree, but the built-in perl way of doing that is with the @ISA list. Each package defines @MyPackage::ISA=(...) forming a tree (or directed graph, for multiple-inheritence). If you want to "traverse up the tree" checking for the first parent class to define a variable, you can use mro::get_linear_isa.

    my $package_hierarchy= mro::get_linear_isa($package_name); for my $p (@$package_hierarchy) { if (...) # check whether package $p has the variable defined }

    While you're at it, you should read the whole documentation of mro to understand the problem it solves, and decide whether you want to take advantage of that in your class system.

    When you call methods on Perl objects, it very efficiently navigates the @ISA inheritance tree to find the method. Unfortunately, there is no built-in efficient navigation of @ISA to look for anything other than methods. So, you are stuck with the performance hit of iterating it with get_linear_isa *unless* you wrap access to those variables with methods. So, for a class system, this means that if you want derived classes to be able to inherit class variables from the parent, but also be able to re-declare them, you probably want to generate an accessor method. This way the end user calls a method and the hierarchy lookup is as fast as a method.

    In your case, it looks like you want to modify derived classes from the parent class. This is the opposite direction from most of what I've described, but can make use of the same mechanisms. If you want 'configure' to be fast, it should probably just set options on the local class stash and let the attributes walk backward to find the inherited value. If you want attribute lookup to be fast, each attribute should be a generated method that reads a single package variable, and you should have a mapping of attribute name to all the derived classes that re-declare it, so that 'configure' can quickly update them all.

    Hope that helps.

Re: Reconfiguration of classes in an "inheritable" manner
by Fletch (Bishop) on Jun 23, 2022 at 21:02 UTC

    Maybe not reading it right, but I'd think you could just stash the config values in a class variable (so %my::base::_config or %my::branch_A::_config). You'd need something to walk up the @ISA to retrieve values if they're not present in the class of the instance (handwaving here and untested ):

    sub _fetch { my( $class, $key ) = @_; no strict q{refs}; my $opts = \%{ ref($class) . q{::_config} }; if( exists $opts->{ $key } ) { return $opts->{ $key }; } else { return SUPER->_fetch( $key ); } } sub configure { my( $class, %opts ) = @_; no strict q{refs}; my $opts = \%{ $class . q{::_config} }; for my ($k, $v) ( each %opts ) { $opts->{ $k } = $v; } return; }

    Edit: Adding stub configure

    The cake is a lie.
    The cake is a lie.
    The cake is a lie.

Re: Reconfiguration of classes in an "inheritable" manner
by perlfan (Vicar) on Jun 24, 2022 at 19:32 UTC
    > i'm stuck with pretty basic perl unfortunately

    You say that like it's a bad thing :-).

    > In essence, i'm doing all this to replicate a sort of "inheritable class variable". Is that something thats natively possible?

    I think you just need an our in the parent class and use parent in the base class.