4

More easily explained with an example:

my $o = SpecialEffects->new( "config" => 'a' );
my $p = SpecialEffects->new( "config" => 'b' );

$o->sound(); # aliased to fizz(); same as $o->fizz()
$p->sound(); # aliased to clonk(); same as $p->clonk()

Is it possible to do this in Perl? Perhaps using some typeglob or coderef trickery?

I'm trying to keep the SpecialEffects interface simple. I'd prefer not to start building an object hierarchy. The sound() method is what's exposed, only its behaviour can be configured slightly.

I already know that you can alias with *sound = \&fizz; but that is a global thing as far as I know, and I'd like it encapsulated in the object.

Mike G
  • 4,232
  • 9
  • 40
  • 66
glts
  • 21,808
  • 12
  • 73
  • 94

3 Answers3

5

The simple, easy, non-wizardry way is to just store a method name in your SpecialEffects object, set it according to whatever you want to happen, and call it from sound().

package SpecialEffects;

sub new {
    my $type = shift;
    my %options = @_;
    my $self = {};
    bless $self, $type;
    if($options{config} eq 'a') {
        $self->{sound_method} = 'fizz';
    } elsif($options{config} eq 'b') {
        $self->{sound_method} = 'clonk';
    }
    return $self;
}

sub sound {
    my $self = shift;
    my $method_name = $self->{sound_method};
    $self->$method_name();
}

sub fizz {
    print "fizz\n";
}

sub clonk {
    print "clonk\n";
}

You can store and use coderefs about as easily as method names, if you want to be more wizardly.

package SpecialEffects;

sub new {
    my $type = shift;
    my %options = @_;
    my $self = {};
    bless $self, $type;
    if($options{config} eq 'a') {
        $self->{sound_code} = $self->can('fizz');
    } elsif($options{config} eq 'b') {
        $self->{sound_code} = $self->can('clonk');
    }
    return $self;
}   

sub sound {
    my $self = shift;
    my $code = $self->{sound_code};
    $self->$code();
}
chaos
  • 122,029
  • 33
  • 303
  • 309
  • 1
    You shouldn't be using prototypes, especially not on methods. Perl doesn't even do prototype checking on method calls, so they're completely useless. – cjm Jul 17 '12 at 00:34
  • @cjm: Is there something actively *wrong* with them? It hadn't occurred to me that there would be a reason to object to using them in simply documentary form, with an eye to the possibility that call checking might catch up sometime. – chaos Jul 17 '12 at 00:38
  • 1
    See [this question](http://stackoverflow.com/q/297034/8355) for reasons you shouldn't use them (except in special circumstances). If you want to document the parameters to your method, use a comment (or POD). – cjm Jul 17 '12 at 00:56
  • 1
    f you want documentation then write documentation. Adding code that is bad practice and has no effect isn't helpful. Prototypes don't make Perl do type checking - they enforce a *context* on the run time parameters. – Borodin Jul 17 '12 at 01:02
  • Mmmkay. I HOPE YOU'RE ALL HAPPY NOW. – chaos Jul 17 '12 at 02:49
2

AFAIK you can't create per-instance methods without storing references in a hash ($o->{sound}() can be done).

Methods are bound to classes, which correspond to perl packages. So you should alialise *SpecialEffects::sound = &SpecialEffects::fizz. This applies to all objects of one class. Sorry this is not javascript etc, I hate this myself...

(You could do some semi-wizadry and create a package SpecialEffects::clonky on the fly (during runtime), which just includes the sound alias for clonk and subclasses SpecialEffects. Then rebless your reference. This is effectively your unwanted hierarchy, but you don't have to actually make the .pm files

EDIT: I'm not sure how exactly this is done, but it goes along these lines ↓ If it works, this is a semi-elegant solution (it will mess up your name space)

sub whatSoundShallIMake {
  my ($self, $sound) = @_;
  no strict 'refs';
  my $newPackageName = "SpecialEffects::make$sound";
  *{"$newPackageName\::sound"} = &{"SpecialEffects::$sound"}; # make the alias
  @{"$newPackageName\::ISA"} = qw(SpecialEffects); # subclass
  return bless $self, $newPackageName; # rebless
}

say ref $o;
# prints "SpecialEffects"
$o = whatSoundShallIMake($o, "fizz");
say ref $0;
# prints  "SpecialEffects::makefizz"

)

amon
  • 57,091
  • 2
  • 89
  • 149
  • I was afraid that a coderef in the object hash would be the only way to go. I would have preferred to keep the method call plain and direct. Your wizardry sounds interesting though I do not understand it ... – glts Jul 16 '12 at 19:34
  • 1
    I added some example code for dynamic packages, but @chaos' solution using `&sound` to look up the effective sub in the object's hash is faster, more adaptable, safer and more powerful. – amon Jul 16 '12 at 20:39
1

Using Moose:

package SpecialEffects;
use Moose;

has '_method' => (is => 'ro', isa => 'CodeRef');

around BUILDARGS => sub {
    my ($orig, $class, %args) = @_;
    my %map = (a => 'fizz', b => 'clonk');
    if (my $config = delete $args{config}) {
        $args{_method} = $class->can($map{$config});
    }
    return $class->$orig(%args);
};

sub sound {shift->_method->(@_)}
sub fizz  {return 'fizz';}
sub clonk {return 'clonk';}
1;

use Test::More;
use SpecialEffects;

my $o = SpecialEffects->new(config => 'a');
is($o->sound, 'fizz');
my $p = SpecialEffects->new(config => 'b');
is($p->sound, 'clonk');

done_testing;

Code credits go to omega in #moose, commenting:

<omega> but not really "clean" :p

<omega> and not that Moosey

<omega> Probably better to apply a role

Community
  • 1
  • 1
daxim
  • 39,270
  • 4
  • 65
  • 132
  • That's neat! Probably too heavyweight for my tiny application. But I'm going to take a look at Moose now, thanks. – glts Jul 17 '12 at 12:59