2

How should I define a Moose object subroutine after its initialization?

I'm writing an object module using Moose and I plan to serialize (nstore) the created objects.

Examine the following (simplified!) example:

package MyObj 0.001;

use Moose;
use namespace::autoclean;

has 'size' => (
 is       => 'ro',
 isa      => 'Int',
 required => 1,
);

sub some_sub {
 my ($self, @more) = @_;
 if ($self->size() < 100) # do something;
 elsif (($self->size() < 500)) # do something else;
 elsif (($self->size() < 7500)) # do something else;
 # ...
}

1;

some_sub acts differently depending on size. Since size is read-only, it remains constant after the object has been initialized.

So, assuming I call some_sub zillion times, it's a pity that I have to go through all the ifs each time.

I'd better do this once after the object has been initialized, then set some_sub to be a simpler function with noifs at all.

But... how can I do that?

UPDATE

Perhaps I should add a lazy attribute of type subref that will hold a reference to the chosen subroutine. some_sub will then simply call $self->chosen_sub->(@_). What do you think?

David B
  • 29,258
  • 50
  • 133
  • 186

3 Answers3

5
has calculation_method => (is => 'ro', lazy_build => 1, init_arg => undef);

sub _build_calculation_method {
    my $self = shift;
    return '_calculate_small'  if $self->size < 100;
    return '_calculate_medium' if $self->size < 500;
    return '_calculate_large'  if $self->size < 7500;
    return '_calculate_enormous';
}

sub _calculate_small  { ... }
sub _calculate_medium { ... }
# etc.

sub calculate {
    my $self = shift;
    my $method = $self->calculation_method;
    return $self->$method(@_);
}

As a bonus, calculation_method is now serializable too.

hdp
  • 778
  • 3
  • 6
  • +1 yup, that's pretty much what my update was about. Just wanted to make sure it makes sense. – David B Oct 22 '10 at 16:27
  • You could also fold the `calculate()` method into the `calculation_method` attribute (and rename as `s/_method//`) by making it `isa => 'CodeRef', traits => ['Code'], handles => { calculate => 'execute_method' });` (as phaylon noted in draegtun's answer). – Ether Oct 22 '10 at 19:32
0

Perhaps another case for MooseX::SingletonMethod! (Sorry I'm reading your questions in reverse order!).

For eg:

use 5.012;
use warnings;

package MyObj 0.001;
use MooseX::SingletonMethod;
use namespace::autoclean;

has 'size' => (
    is       => 'ro',
    isa      => 'Int',
    required => 1,
);

sub _which_sub {
    my ($self) = @_;

    if    ($self->size < 100)  { return sub{ 'A' } }
    elsif ($self->size < 500)  { return sub{ 'B' } }
    elsif ($self->size < 7500) { return sub{ 'C' } } 
    return sub { 'D' };
}


package main;

my $obj = MyObj->new( size => 200 );

$obj->add_singleton_method( some_sub => $obj->_which_sub );

say $obj->some_sub;  # => B


And it should be possible to add this single method creation from inside your class. Have a look at this blog post for some guidance: Moose Singleton Method: Now without roles!. And also a hotchpotch of posts here

Community
  • 1
  • 1
draegtun
  • 22,441
  • 5
  • 48
  • 71
  • I don't like it so much, since I'd like this to be transparent to the user. This should all happen during the initialization of the object. Also see my update for another take on it. – David B Oct 22 '10 at 13:08
  • It could be made to be this transparent (the links provided give some insight into reblessing that needed for this). However if you are OK with `$self->chosen_sub->(@_)` (re: your update) then see my new answer for this. – draegtun Oct 22 '10 at 13:57
  • Is there a reason I shouldn't be OK with `$self->chosen_sub->(@_)`? – David B Oct 22 '10 at 15:23
  • Nope. Just referring to API, ie. happy with using `$obj->chosen_sub->(...)` over `$obj->chosen_sub(...)` – draegtun Oct 22 '10 at 15:25
  • BTW, I think a builder solution is the best approach for this. – draegtun Oct 22 '10 at 15:38
0

Regarding your update:

use 5.012;
use warnings;

package MyObj;
use Moose;
use namespace::autoclean;

has 'size' => (
    is       => 'ro',
    isa      => 'Int',
    required => 1,
);

has 'chosen_sub' => (
   is       => 'ro',
   isa      => 'CodeRef',
   lazy     => 1,
   builder  => '_build_chosen_sub',
   init_arg => undef, # unless want option of providing anon sub at construction?
);

sub _build_chosen_sub {
    my ($self) = @_;

    if    ($self->size < 100)  { return sub{ 'A' } }
    elsif ($self->size < 500)  { return sub{ 'B' } }
    elsif ($self->size < 7500) { return sub{ 'C' } } 
    return sub { 'D' };
}

package main;
my $obj = MyObj->new( size => 200 );
say $obj->chosen_sub->();  # => B
Evan Carroll
  • 78,363
  • 46
  • 261
  • 468
draegtun
  • 22,441
  • 5
  • 48
  • 71
  • Note that the native Code attribute trait has 'execute' and 'execute_method' delegates that can be used to easily set up a CodeRef attribute that acts like a common method. – phaylon Oct 22 '10 at 15:33
  • @phaylon: Cool, not seen that before. Have now!... http://search.cpan.org/dist/Moose/lib/Moose/Meta/Attribute/Native/Trait/Code.pm – draegtun Oct 22 '10 at 15:45
  • I think you can make 'chosen_sub` private (i.e. change to `_chosen_sub`) then add `sub mysub { my ($self) = @_; return $self->_chosen_sub()->(@_);}`. Now we can call `$obj->mysub();`. On second look, this is what hdp has written. – David B Oct 22 '10 at 16:25
  • Yes hdp answer is the best one (+1). I was thinking of writing the same but aimed to keep it closer to your update, ie. I thought you wanted/needed an anon sub in their. – draegtun Oct 22 '10 at 18:15