NAME

Sub::HandlesVia - alternative handles_via implementation

SYNOPSIS

package Kitchen {
  use Moo;
  use Sub::HandlesVia;
  use Types::Standard qw( ArrayRef Str );
  
  has food => (
    is          => 'ro',
    isa         => ArrayRef[Str],
    handles_via => 'Array',
    default     => sub { [] },
    handles     => {
      'add_food'    => 'push',
      'find_food'   => 'grep',
    },
  );
}

my $kitchen = Kitchen->new;
$kitchen->add_food('Bacon');
$kitchen->add_food('Eggs');
$kitchen->add_food('Sausages');
$kitchen->add_food('Beans');

my @foods = $kitchen->find_food(sub { /^B/i });

DESCRIPTION

If you've used Moose's native attribute traits, or MooX::HandlesVia before, you should have a fairly good idea what this does.

Why re-invent the wheel? Well, this is an implementation that should work okay with Moo, Moose, Mouse, and any other OO toolkit you throw at it. One ring to rule them all, so to speak. (Moose support is a TODO though!)

Also, unlike MooX::HandlesVia, it honours type constraints, plus it doesn't have the limitation that it can't mutate non-reference values.

Using with Moo

You should be able to use it as a drop-in replacement for MooX::HandlesVia.

package Kitchen {
  use Moo;
  use Sub::HandlesVia;
  use Types::Standard qw( ArrayRef Str );
  
  has food => (
    is          => 'ro',
    isa         => ArrayRef[Str],
    handles_via => 'Array',
    default     => sub { [] },
    handles     => {
      'add_food'    => 'push',
      'find_food'   => 'grep',
    },
  );
}

Using with Mouse

It works the same as Moo basically.

package Kitchen {
  use Mouse;
  use Sub::HandlesVia;
  use Types::Standard qw( ArrayRef Str );
  
  has food => (
    is          => 'ro',
    isa         => ArrayRef[Str],
    handles_via => 'Array',
    default     => sub { [] },
    handles     => {
      'add_food'    => 'push',
      'find_food'   => 'grep',
    },
  );
}

You are not forced to use Types::Standard. Mouse native types should work fine.

package Kitchen {
  use Mouse;
  use Sub::HandlesVia;
  
  has food => (
    is          => 'ro',
    isa         => 'ArrayRef[Str]',
    handles_via => 'Array',
    default     => sub { [] },
    handles     => {
      'add_food'    => 'push',
      'find_food'   => 'grep',
    },
  );
}

Sub::HandlesVia will also recognize MooseX::NativeTraits-style traits. It will jump in and handle them before MooseX::NativeTraits notices!

package Kitchen {
  use Mouse;
  use Sub::HandlesVia;
  
  has food => (
    is          => 'ro',
    isa         => 'ArrayRef[Str]',
    traits      => ['Array'],
    default     => sub { [] },
    handles     => {
      'add_food'    => 'push',
      'find_food'   => 'grep',
    },
  );
}

(If you have a mouse in your kitchen though, that might not be very hygienic.)

Using with Moose

It works the same as Mouse basically.

package Kitchen {
  use Moose;
  use Sub::HandlesVia;
  use Types::Standard qw( ArrayRef Str );
  
  has food => (
    is          => 'ro',
    isa         => ArrayRef[Str],
    handles_via => 'Array',
    default     => sub { [] },
    handles     => {
      'add_food'    => 'push',
      'find_food'   => 'grep',
    },
  );
}

You are not forced to use Types::Standard. Moose native types should work fine.

package Kitchen {
  use Moose;
  use Sub::HandlesVia;
  
  has food => (
    is          => 'ro',
    isa         => 'ArrayRef[Str]',
    handles_via => 'Array',
    default     => sub { [] },
    handles     => {
      'add_food'    => 'push',
      'find_food'   => 'grep',
    },
  );
}

Sub::HandlesVia will also recognize native-traits-style traits. It will jump in and handle them before Moose notices!

package Kitchen {
  use Moose;
  use Sub::HandlesVia;
  
  has food => (
    is          => 'ro',
    isa         => 'ArrayRef[Str]',
    traits      => ['Array'],
    default     => sub { [] },
    handles     => {
      'add_food'    => 'push',
      'find_food'   => 'grep',
    },
  );
}

(If you have a moose in your kitchen, that might be even worse than the mouse.)

Using with Anything

For Moose and Mouse, Sub::HandlesVia can use their metaobject protocols to grab an attribute's definition and install the methods it needs to. For Moo, it can wrap has and do its stuff that way. For other classes, you need to be more explicit and tell it what methods to delegate to what attributes.

package Kitchen {
  use Class::Tiny {
    food => sub { [] },
  };
  
  use Sub::HandlesVia qw( delegations );
  
  delegations(
    attribute   => 'food'
    handles_via => 'Array',
    handles     => {
      'add_food'    => 'push',
      'find_food'   => 'grep',
    },
  );
}

Setting attribute to "food" means that when Sub::HandlesVia needs to get the food list, it will call $kitchen->food and when it needs to set the food list, it will call $kitchen->food($value). If you have separate getter and setter methods, just do:

attribute   => [ 'get_food', 'set_food' ],

Or if you don't have any accessors and want Sub::HandlesVia to directly access the underlying hashref:

attribute   => '{food}',

Or maybe you have a setter, but want to use hashref access for the getter:

attribute   => [ '{food}', 'set_food' ],

Or maybe you still want direct access for the getter, but your object is a blessed arrayref instead of a blessed hashref:

attribute   => [ '[7]', 'set_food' ],

Or maybe your needs are crazy unique:

attribute   => [ \&getter, \&setter ],

The coderefs are passed the instance as their first argument, and the setter is also passed a value to set.

Really, I don't think there's any object system that this won't work for!

What methods can be delegated to?

The following table compares Sub::HandlesVia with Data::Perl, Moose native traits, and MouseX::NativeTraits. Mouse looks like it's ahead of the rest, but quite a few of their extra methods are just aliases for existing methods.

Array ===========================================
          accessor : SubHV  DataP  Moose  Mouse
               all : SubHV  DataP              
               any :                      Mouse
             apply :                      Mouse
             clear : SubHV  DataP  Moose  Mouse
             count : SubHV  DataP  Moose  Mouse
            delete : SubHV  DataP  Moose  Mouse
          elements : SubHV  DataP  Moose  Mouse
             fetch :                      Mouse
             first : SubHV  DataP  Moose  Mouse
       first_index : SubHV  DataP  Moose       
           flatten : SubHV  DataP              
      flatten_deep : SubHV  DataP              
          for_each :                      Mouse
     for_each_pair :                      Mouse
               get : SubHV  DataP  Moose  Mouse
              grep : SubHV  DataP  Moose  Mouse
            insert : SubHV  DataP  Moose  Mouse
          is_empty : SubHV  DataP  Moose  Mouse
              join : SubHV  DataP  Moose  Mouse
               map : SubHV  DataP  Moose  Mouse
          natatime : SubHV  DataP  Moose       
               pop : SubHV  DataP  Moose  Mouse
             print : SubHV  DataP              
              push : SubHV  DataP  Moose  Mouse
            reduce : SubHV  DataP  Moose  Mouse
            remove :                      Mouse
           reverse : SubHV  DataP              
               set : SubHV  DataP  Moose  Mouse
     shallow_clone : SubHV  DataP  Moose       
             shift : SubHV  DataP  Moose  Mouse
           shuffle : SubHV  DataP  Moose  Mouse
  shuffle_in_place : SubHV                     
              sort : SubHV  DataP  Moose  Mouse
           sort_by :                      Mouse
     sort_in_place : SubHV  DataP  Moose  Mouse
  sort_in_place_by :                      Mouse
            splice : SubHV  DataP  Moose  Mouse
             store :                      Mouse
              uniq : SubHV  DataP  Moose  Mouse
     uniq_in_place : SubHV                     
           unshift : SubHV  DataP  Moose  Mouse

Bool ============================================
               not : SubHV  DataP  Moose  Mouse
             reset : SubHV                     
               set : SubHV  DataP  Moose  Mouse
            toggle : SubHV  DataP  Moose  Mouse
             unset : SubHV  DataP  Moose  Mouse

Code ============================================
           execute : SubHV  DataP  Moose  Mouse
    execute_method : SubHV         Moose  Mouse

Counter =========================================
               dec : SubHV  DataP  Moose  Mouse
               inc : SubHV  DataP  Moose  Mouse
             reset : SubHV  DataP  Moose  Mouse
               set : SubHV         Moose  Mouse

Hash ============================================
          accessor : SubHV  DataP  Moose  Mouse
               all : SubHV  DataP              
             clear : SubHV  DataP  Moose  Mouse
             count : SubHV  DataP  Moose  Mouse
           defined : SubHV  DataP  Moose  Mouse
            delete : SubHV  DataP  Moose  Mouse
          elements : SubHV  DataP  Moose  Mouse
            exists : SubHV  DataP  Moose  Mouse
             fetch :                      Mouse
      for_each_key :                      Mouse
     for_each_pair :                      Mouse
    for_each_value :                      Mouse
               get : SubHV  DataP  Moose  Mouse
          is_empty : SubHV  DataP  Moose  Mouse
              keys : SubHV  DataP  Moose  Mouse
                kv : SubHV  DataP  Moose  Mouse
               set : SubHV  DataP  Moose  Mouse
     shallow_clone : SubHV  DataP  Moose       
       sorted_keys : SubHV                Mouse
             store :                      Mouse
            values : SubHV  DataP  Moose  Mouse

Number ==========================================
               abs : SubHV  DataP  Moose  Mouse
               add : SubHV  DataP  Moose  Mouse
               div : SubHV  DataP  Moose  Mouse
               get : SubHV                     
               mod : SubHV  DataP  Moose  Mouse
               mul : SubHV  DataP  Moose  Mouse
               set : SubHV         Moose       
               sub : SubHV  DataP  Moose  Mouse

String ==========================================
            append : SubHV  DataP  Moose  Mouse
             chomp : SubHV  DataP  Moose  Mouse
              chop : SubHV  DataP  Moose  Mouse
             clear : SubHV  DataP  Moose  Mouse
               get : SubHV                     
               inc : SubHV  DataP  Moose  Mouse
            length : SubHV  DataP  Moose  Mouse
             match : SubHV  DataP  Moose  Mouse
           prepend : SubHV  DataP  Moose  Mouse
           replace : SubHV  DataP  Moose  Mouse
  replace_globally : SubHV                Mouse
             reset : SubHV                     
               set : SubHV                     
            substr : SubHV  DataP  Moose  Mouse

Method Chaining

Say you have the following

handles_via => 'Array',
handles     => {
  'add_food'    => 'push',
  'find_food'   => 'grep',
  'remove_food' => 'pop',
},

Now $kitchen->remove_food will remove the last food on the list and return it. But what if we don't care about what food was removed? We just want to remove the food and discard it. You can do this:

handles_via => 'Array',
handles     => {
  'add_food'    => 'push',
  'find_food'   => 'grep',
  'remove_food' => 'pop...',
},

Now the remove_food method will return the kitchen object instead of returning the food. This makes it suitable for chaining method calls:

# remove the three most recent foods
$kitchen->remove_food->remove_food->remove_food;

Hand Waving

Sub::HandlesVia tries to be strict by default, but you can tell it to be less rigourous checking method arguments, etc using the ~ prefix:

handles_via => 'Array',
handles     => {
  'find_food'   => '~grep',
},

CodeRefs

You can delegate to coderefs:

handles_via => 'Array',
handles    => {
  'find_healthiest' => sub { my $foods = shift; ... },
}

Named Methods

Let's say "FoodList" is a class where instances are blessed arrayrefs of strings.

isa         => InstanceOf['FoodList'],
handles_via => 'Array',
handles     => {
  'find_food'             => 'grep',
  'find_healthiest_food'  => 'find_healthiest',
},

Now $kitchen->find_food($coderef) does this (which breaks encapsulation of course):

my @result = grep $coderef->(), @{ $kitchen->food };

And $kitchen->find_healthiest_food does this:

C<< $kitchen->food->find_healthiest >>

Basically, because find_healthiest isn't one of the methods offered by Sub::HandlesVia::HandlerList::Array, it assumes you want to call it on the arrayref like a proper method.

Currying Favour

All this talk of food is making me hungry, but as much as I'd like to eat a curry right now, that's not the kind of currying we're talking about.

handles_via => 'Array',
handles     => {
  'get_food'   => 'get',
},

$kitchen->get_food(0) will return the first item on the list. $kitchen->get_food(1) will return the second item on the list. And so on.

handles_via => 'Array',
handles     => {
  'first_food'   => [ 'get' => 0 ],
  'second_food'  => [ 'get' => 1 ],
},

I think you already know what this does. Right?

And yes, currying works with coderefs.

handles_via => 'Array',
handles     => {
  'blargy'       => [ sub { ... }, @curried ],
},

Pick and Mix

isa         => ArrayRef|HashRef,
handles_via => [ 'Array', 'Hash' ],
handles     => {
  the_keys     => 'keys',
  ship_shape   => 'sort_in_place',
}

Here you have an attribute which might be an arrayref or a hashref. When it's an arrayref, $object->ship_shape will work nicely, but $object->the_keys will fail badly.

Still, this sort of thing can kind of make sense if you have an object that overloads both @{} and %{}.

Sometime a method will be ambiguous. For example, there's a get method for both hashes and arrays. In this case, the array one will win because you listed it first in handles_via.

But you can be specific:

isa         => ArrayRef|HashRef,
handles_via => [ 'Array', 'Hash' ],
handles     => {
  get_foo => 'Array->get',
  get_bar => 'Hash->get',
}

BUGS

Please report any bugs to http://rt.cpan.org/Dist/Display.html?Queue=Sub-HandlesVia.

(There are known bugs for Moose native types that do coercion.)

SEE ALSO

Moose, MouseX::NativeTraits, Data::Perl, MooX::HandlesVia.

AUTHOR

Toby Inkster <tobyink@cpan.org>.

COPYRIGHT AND LICENCE

This software is copyright (c) 2020 by Toby Inkster.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.

DISCLAIMER OF WARRANTIES

THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.