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

Hi,
This started out being a "how could I do this?"-style question. I've answered that question, and I'm now asking "should I do this?".

I'm using the AUTOLOADer to create class-accessor/mutator subroutines. The object attribute may or may not be editable by the calling code, so I create different subroutines depending on the editability. This is a bit like the get_/set_ style of functions, but I didn't fancy using those. (They annoy me).

Is there a better way of doing what I've done here? If you were the maintenance programmer coming after me, would you literally come after me?
Code that achieves the same thing:
Cheers

Update: Forgot to mention: This is similar-ish to what Damian Conway does in "Object Oriented Perl", p94.
See also: accessor/mutator with AUTOLOAD and How can I use AUTOLOAD to magically define get and set methods for my member data?

#!/usr/bin/perl use warnings; use strict; my $test = Foo->new(); print $test->read_only, "\n"; $test->read_only("cheese"); # Won't do anything print $test->read_only, "\n"; print $test->read_write, "\n"; $test->read_write("cheese"); print $test->read_write, "\n"; package Foo; use vars '$AUTOLOAD'; sub new { bless { read_only => { value => "foo", editable => 0, }, read_write => { value => "bar", editable => 1, } } }; sub AUTOLOAD { no strict "refs"; my ($self, $new_value) = @_; my $field_name = $AUTOLOAD; $field_name =~ s/.*:://; if(grep /^$field_name$/,keys(%$self)) { #Build a different autoloader depending on whether # the field is editable if($self->{$field_name}{editable}) { if($new_value) { $self->{$field_name}{value} = $new_val +ue; } *{$AUTOLOAD} = sub { if(2 == @_) { $self->{$field_name}{value} = +$_[1]; } return $self->{$field_name}{value}; } } else { #Really ought to make this complain if you try + setting a value. *{$AUTOLOAD} = sub { $self->{$field_name}{valu +e}; }; } return $self->{$field_name}{value}; } else { die "No such method $AUTOLOAD. Barfing\n"; } } sub DESTROY { 1; }

davis
It's not easy to juggle a pregnant wife and a troubled child, but somehow I managed to fit in eight hours of TV a day.

Replies are listed 'Best First'.
Re: Subroutines with differing behaviour created from AUTOLOAD
by hv (Prior) on Jun 08, 2004 at 14:10 UTC

    The general approach seems perfectly reasonable to me. However I think you have a problem with the implementation: the methods you are injecting will be closures over the current $self, so it won't work if you have more than one object in the class.

    I also think the code would be easier to read (and therefore maintain) if you create the method then goto it, rather than duplicating the functionality, something like (untested):

    die "No such method $AUTOLOAD. Barfing\n" unless exists $self->{$field}; if ($self->{$field}{editable}) { *$AUTOLOAD = sub { my $self = shift; $self->{$field}{value} = shift if @_; $self->{$field}{value}; }; } else { *$AUTOLOAD = sub { my $self = shift; die "Can't modify readonly field '$field'" if @_; $self->{$field}{value}; }; } goto &$AUTOLOAD;

    Hugo

      Untested is right. That method will DEFINITELY work with more than one object in the class. I should know - I use it in production. You are NOT "injecting"(?) closures over the current $self. Test it before you make pronouncements like that.

      However, the method will have issues for other reasons. The code implies that two difference instances might have the same field be editable or not at the instance level. If that's the case, then the first instance to access the field will set its editable-ness for all instances of the class.

      There are two solutions. If each instance needs to maintain editability, then you have to go to the OP's solution, which is to have AUTOLOAD evaluate everything on a case by case basis. (This can still be gotten around, but you'd have to attach the closures to the instance and have the class methods dispatch to the instance's method. Too much work!)

      If the fields are editable based on the class, then hoist the editability to the class level and deal with it there.

      my $class = ref($self); my %editable = do { no strict 'refs'; %{"${class}::__EDITABLE__"}; }; die "No such method $AUTOLOAD. Barfing\n" unless exists $editable{$field}; if ($editable{$field}) { *$AUTOLOAD = sub { my $self = shift; $self->{$field}{value} = shift if @_; $self->{$field}{value}; }; } else { *$AUTOLOAD = sub { my $self = shift; die "Can't modify readonly field '$field'" if @_; $self->{$field}{value}; }; } goto &$AUTOLOAD;

      ------
      We are the carpenters and bricklayers of the Information Age.

      Then there are Damian modules.... *sigh* ... that's not about being less-lazy -- that's about being on some really good drugs -- you know, there is no spoon. - flyingmoose

      I shouldn't have to say this, but any code, unless otherwise stated, is untested

        Untested is right. That method will DEFINITELY work with more than one object in the class. I should know - I use it in production. You are NOT "injecting"(?) closures over the current $self. Test it before you make pronouncements like that.

        With the original poster's code, this test:

        my $test = Foo->new(); my $test2 = Foo->new(); $test->read_write("water"); $test2->read_write("wine"); print "test: should be water, is ", $test->read_write, "\n"; print "test2: should be wine, is ", $test2->read_write, "\n";
        gives me:
        test: should be water, is wine test2: should be wine, is wine

        I'm confused as to what you're saying will definitely work - the OP's code has an AUTOLOAD sub which defines a lexical $self, in which he defines a couple of anonymous subs which refer to that $self. Nothing that can't be fixed by adding a my $self into the subs, as I did in my code and as you've done in yours.

        Hugo

        Can you explain what
        my %editable = do { no strict 'refs'; %{"${class}::__EDITABLE__"}; };
        does please? It looks to me like it's using a filehandle to populate a hash of the attributes that should be editable. Can you give an example of how to use this?
        Cheers

        davis
        It's not easy to juggle a pregnant wife and a troubled child, but somehow I managed to fit in eight hours of TV a day.
      the methods you are injecting will be closures over the current $self, so it won't work if you have more than one object in the class
      Wow, apparently not. Although I realised the subs were closures (by keeping the value $field), I never realised that this would break for more than one object instance.
      Does this mean that I should remove the assignment to the symbol table and live with the performance penalty?

      davis
      It's not easy to juggle a pregnant wife and a troubled child, but somehow I managed to fit in eight hours of TV a day.
        This is a case when eval saves the day: (warning untested)
        die "No such method $AUTOLOAD. Barfing\n" unless exists $self->{$field}; if ($self->{$field}{editable}) { *$AUTOLOAD = eval <<ENDSUB; sub { my \$self = shift; \$self->{$field}{value} = shift if \@_; \$self->{$field}{value}; } ENDSUB } else { *$AUTOLOAD = eval <<ENDSUB2; sub { my \$self = shift; die "Can't modify readonly field '$field'" if \@_; \$self->{$field}{value}; } ENDSUB2 } goto &$AUTOLOAD;

        --
        I'm Not Just Another Perl Hacker

        No, it means only that you should declare my $self in the subs you create, as in my example - without such a declaration, the $self in the anonymous subs refers to the closest enclosing declaration (the my $self of the AUTOLOAD routine), and that is what creates the unwanted closure.

        You might prefer to further reduce the potential for confusion by using a different variable name for the two: perhaps by renaming the outer $self to $object or somesuch.

        Hugo

Re: Subroutines with differing behaviour created from AUTOLOAD
by davis (Vicar) on Jun 08, 2004 at 18:01 UTC
    Here's the kind of thing I ended up using:
    #!/usr/bin/perl use warnings; use strict; my $test = Foo->new(); print $test->read_only, "\n"; #$test->read_only("cheese"); # Will error print $test->read_write, "\n"; $test->read_write("cheese"); print $test->read_write, "\n"; package Foo; use vars '$AUTOLOAD'; sub new { bless { read_only => { value => "foo", editable => 0, }, read_write => { value => "bar", editable => 1, } } }; sub AUTOLOAD { no strict "refs"; my ($self, $new_value) = @_; my $field = $AUTOLOAD; $field =~ s/.*:://; die "No such method $AUTOLOAD. Barfing\n" unless exists $self->{$field}; if ($self->{$field}{editable}) { *$AUTOLOAD = sub { my $self = shift; $self->{$field}{value} = shift if @_; $self->{$field}{value}; }; } else { *$AUTOLOAD = sub { my $self = shift; die "Can't modify readonly field '$field'" if +@_; $self->{$field}{value}; }; } goto &$AUTOLOAD; } sub DESTROY { 1; }

    davis
    It's not easy to juggle a pregnant wife and a troubled child, but somehow I managed to fit in eight hours of TV a day.