How to auto generate a bunch of setters / getters tied to a network service in Moose?
Asked Answered
Z

5

5

By way of teaching myself Moose, I'm working on a Moose object that interfaces to a particular piece of hardware. Said hardware takes a number of different commands that set various properties of the hardware, all of the form PROPERTYNAME=VALUE for a setter, and PROPERTYNAME? for a getter (note that these 'setters' and 'getters' are on the network interface to the hardware). What I want to do is create an object where all of these properties of the hardware are implemented with an attribute-like interface. Since getting and setting the various properties takes the same form for all properties, is there a way to automatically generate the setters and getters from a list of those properties?

I.E.: Rather than this:

Package MyHardware;
use Moose;
has property1 => (
    'is' => 'rw',
    'reader' => 'set_property1',
    'writer' => 'get_property1',
);

has property2 => (
    'is' => 'rw',
    'reader' => 'set_property2',
    'writer' => 'get_property2',
);

# ...

has propertyN => (
    'is' => 'rw',
    'reader' => 'set_propertyN',
    'writer' => 'get_propertyN',
);

Is there something I can do like this:

Package MyHardware;
use Moose;

attributes => (
    'is' => 'rw',
    'names' => [qw/property1 property2 ... propertyN/],
    'reader' => sub {
        my $self = shift;
        my $property = shift;
        return $self->_send_command("$property?");
    },
    'writer' => sub {
        my $self = shift;
        my $property = shift;
        my $value = shift;
        return $self->_send_command("$property=$value");
    },
);

EDIT: Here's what I want to happen:

# CALLER:
my $hw = MyHardware->new();
$hw->property1('foo');
print $hw->property2 . "\n";

And "under the hood":

$hw->property1('foo');
# Becomes 
sub { return $hw->_send_command('property1=foo'); }

# And

$hw->property2();
# Becomes
sub { return $hw->_send_command('property2?'); }
Zepeda answered 28/1, 2013 at 20:48 Comment(0)
Z
2

Figured it out. I realize that I shouldn't be using attributes at all to do this. Instead, I'll dynamically generate methods using Class::MOP::Class like so:

my $meta = Class::MOP::Class->initialize(__PACKAGE__);
foreach my $prop (qw/property1 property2 property3/) {
    $meta->add_method(qq/set_$prop/, sub { 
            my $self = shift;
            my $value = shift;
            return $self->_send_command(qq/$prop=$value/);
        }
    );
    $meta->add_method(qq/get_$prop/, sub { 
            my $self = shift;
            return $self->_send_command(qq/$prop?/);
        }
    );
}

Doing it with calls to has() would have effectively put the object state in two places - on the hardware and in the instance - and I only want it in one.

Zepeda answered 29/1, 2013 at 17:32 Comment(7)
See OP's update for clarification on desired result. (He wants an accessor, not a getter+setter.) You should stick to Moose (my $meta = __PACKAGE__->meta();) since that's what he's using.Angelicangelica
@Angelicangelica I got that out of the Moose docs, actually. Seems like it's a good habit to get into.Zepeda
Are you really replaying to my comment? Cause it makes no sense. Both things I suggested are more "moosy". 1) Moose code normally uses accessors instead of setters and getters. If anything, the Moose way is accessors like you asked. (But it's really personal preference, and mine is setters and getters.) 2) my $meta = __PACKAGE__->meta(); would be the Moose way. (e.g. You've probably seen __PACKAGE__->meta()->make_immutable(); a million times.) Accessing Class::MOP directly looks really really wrong, and might even be harmful in a Moose module.Angelicangelica
@ikegami: per Moose::Manual::MOP: "You can also use Class::MOP::Class->initialize($name) to get a metaclass object for any class. This is safer than calling $class->meta when you're not sure that the class has a meta method." Admittedly, I know that my class, being Moose, will have a meta method, but as C::M::C->initialize() will return the existing meta if present, it seems like getting into the habit of doing it that way is good practice.Zepeda
No, mixing MOP with Moose in case you're not using Moose not only sounds like bad habit to me, it doesn't even make sense to me.Angelicangelica
I really need to remember that Class::MOP::Class::add_method exists. My answer was going to involve Sub::Install, and that would have been embarrassing.Lenz
@Angelicangelica As I understand it, Moose is built on top of MOP, so you're really using MOP no matter what. But regardless, it's not a big deal to me, so I'll wait for Conway to weigh in on it in the next PBP. :)Zepeda
I
5

How about looping over the properties?

use strict;
use warnings;

use Moose;

foreach my $prop ( qw( property1 property2 property3 property4 ) ) { 
    has $prop => (
        is => 'rw',
        isa => 'Str',
        reader => "get_$prop",
        writer => "set_$prop",
    );  
}

1;
Institute answered 28/1, 2013 at 21:15 Comment(2)
Remember that has looks like syntax but it's really just an ordinary function call, executed at run-time. There is no problem using it inside a control structure or even inside another function. And there is no problem passing variable arguments to it.Campos
That's awfully close to what I want, but (and maybe this should be a separate question) is there any way to pass arguments to the writer? Is this a situation where I should be using around()?Zepeda
A
2

You don't store any value, so you don't want attributes.

You don't don't even want two subs since you want a single name for both getting and setting.

for my $prop (qw( property1 property2 property3  )) { 
   my $accessor = sub {
      my $self = shift;
      if (@_) {
         $self->_send_command("$prop=$value");
      } else {
         return $self->_send_command("$prop?");
      }
   };

   no strict 'refs';
   *$prop = $accessor;
}
Angelicangelica answered 29/1, 2013 at 1:56 Comment(4)
That would be great, except I don't think you can specify a closure as a value for 'reader' or 'writer'. I try it, and I get: "bad accessor/reader/writer/predicate/clearer format, must be a HASH ref at D:/Perl64/lib/Class/MOP/Attribute.pm line 366."Zepeda
@Kit Peters, oh I know why I was confused. His getter and his setter are neither. They're just plain boring ordinary subs.Angelicangelica
I have replaced my answer.Angelicangelica
I assume you mean "*$prop" in place of "*$prog". :) I did something like that (see my answer below) except I used Class::MOP::Class to add the accessors.Zepeda
Z
2

Figured it out. I realize that I shouldn't be using attributes at all to do this. Instead, I'll dynamically generate methods using Class::MOP::Class like so:

my $meta = Class::MOP::Class->initialize(__PACKAGE__);
foreach my $prop (qw/property1 property2 property3/) {
    $meta->add_method(qq/set_$prop/, sub { 
            my $self = shift;
            my $value = shift;
            return $self->_send_command(qq/$prop=$value/);
        }
    );
    $meta->add_method(qq/get_$prop/, sub { 
            my $self = shift;
            return $self->_send_command(qq/$prop?/);
        }
    );
}

Doing it with calls to has() would have effectively put the object state in two places - on the hardware and in the instance - and I only want it in one.

Zepeda answered 29/1, 2013 at 17:32 Comment(7)
See OP's update for clarification on desired result. (He wants an accessor, not a getter+setter.) You should stick to Moose (my $meta = __PACKAGE__->meta();) since that's what he's using.Angelicangelica
@Angelicangelica I got that out of the Moose docs, actually. Seems like it's a good habit to get into.Zepeda
Are you really replaying to my comment? Cause it makes no sense. Both things I suggested are more "moosy". 1) Moose code normally uses accessors instead of setters and getters. If anything, the Moose way is accessors like you asked. (But it's really personal preference, and mine is setters and getters.) 2) my $meta = __PACKAGE__->meta(); would be the Moose way. (e.g. You've probably seen __PACKAGE__->meta()->make_immutable(); a million times.) Accessing Class::MOP directly looks really really wrong, and might even be harmful in a Moose module.Angelicangelica
@ikegami: per Moose::Manual::MOP: "You can also use Class::MOP::Class->initialize($name) to get a metaclass object for any class. This is safer than calling $class->meta when you're not sure that the class has a meta method." Admittedly, I know that my class, being Moose, will have a meta method, but as C::M::C->initialize() will return the existing meta if present, it seems like getting into the habit of doing it that way is good practice.Zepeda
No, mixing MOP with Moose in case you're not using Moose not only sounds like bad habit to me, it doesn't even make sense to me.Angelicangelica
I really need to remember that Class::MOP::Class::add_method exists. My answer was going to involve Sub::Install, and that would have been embarrassing.Lenz
@Angelicangelica As I understand it, Moose is built on top of MOP, so you're really using MOP no matter what. But regardless, it's not a big deal to me, so I'll wait for Conway to weigh in on it in the next PBP. :)Zepeda
C
1

I would recommend using a has rather than an individual attribute for each of your properties.

Package MyHardware;
use Moose;
has properties => (
'is' => 'rw',
'isa' => 'HashRef',
'lazy_build' => 1,
);

sub _build_properties {
    my $self = shift;
    return {
        'property1' => '',
        'property2' => '',
    };
}

print $self->properties->{property1};
Callean answered 28/1, 2013 at 23:56 Comment(1)
This is the easier way of doing this, I'm sure. However, I'm doing this as an exercise in figuring out how Moose works, so I'd like to see if what I want is possible.Zepeda
I
1

Generate getters and setters for instance data

BEGIN 
{
    my @attr = qw(prop1 prop2 prop3 prop4);
    no strict 'refs';
    for my $a (@attr)
    {
        *{__PACKAGE__ . "::get_$a"} = sub { $_[0]->{$a}         };
        *{__PACKAGE__ . "::set_$a"} = sub { $_[0]->{$a} = $_[1] };
    }
}

Indisposition answered 15/3, 2013 at 19:27 Comment(1)
No Moose required? Very nice indeed!Zepeda

© 2022 - 2024 — McMap. All rights reserved.