Skip to content

jjn1056/ParameterizedRolesAndMethodTraits

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

37 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TITLE

Parameterized Roles and Method Traits - Discussion on MooseX::Declare advanced code composition features

ARE YOU READY?

This is a discussion of some advanced features of Moose and MooseX::Declare. If you are new to Perl or Moose I recommend the following tutorials and web resources to get you up to speed. These should be your first stop if you are new to Moose or never heard of roles. This is far from an exhaustive list, but should get you started.

  • Learning Perl

See the web site "Learning Perl", http://learn.perl.org

  • Moose Tutorial

See the excellent Moose::Tutorial which is a good lead in to the general Moose documentation

  • MooseX::Declare documentation

MooseX::Declare's documentation is not as complete as the general Moose documentation (there's no tutorial, for example) but it does have a good number of examples and many test cases worth reviewing.

  • Moose Homepage

The http://moose.perl.org has a good collection of links to blogs, articles and presentations.

INTRODUCTION

You might already know I'm a big fan of MooseX::Declare, which takes all the programming ease and power of Moose, and adds a sweeter syntax which includes the most flexible method body signature system I've seen in any popular programming language. For the uninitiated you can go from:

package MooseClass;
use Moose;

sub my_method {
    my ($self, $name, $age) = @_;
    return "Hi $name, you are $age years old";
}

__PACKAGE__->meta->make_immutable;

To something like:

use MooseX::Declare;
class MooseXDeclareClass {
    method my_method(Str $name, Int $age) {
        return "Hi $name, you are $age years old";
    }
}

This gives you a more modern class and method declaration syntax which lets you dispense with all the boilerplate validation and mucking around with "@_". You get Moose type constraint checking (Moose::Util::TypeConstraints) in method signatures for free, as well as type contraint coercions which grant a level of declaritve polymorphism in your method declarations previously missing from Perl. Additionally this looks much more like what a modern programmer expects to see when creating classes, which should make it easier for people coming from other languages to get excited about modern Perl. The only (current) downsides are a performance penalty and sometimes if you have syntax errors the error messages can be a little cryptic. Both issues are a work in progress but if they really bother you you can always drop down to 'classic' Moose and use Method::Signatures::Simple to get the shiny 'method' keyword (although you loose the method body signature type constraint checking).

Anyway, you may not know it, but MooseX::Declare has some sweet syntax for declaring parameterized Roles (via MooseX::Role::Parameterized) and Method Traits. Both features are relatively undocumented in MooseX::Declare, so here's the basic idea. Hopefully this article can lead (with your feedback) to improved core MooseX::Declare documentation.

PARAMETERIZED ROLES

Think of a Parameterized Role as a sort of Role generator. It lets you make more generic roles by allowing you to specify role parameters at the time you include the role. These parameters (which are similar to standard Moose attributes) can be used to inform and influence how a role works before its applied to its consuming class. For example, MooseX::Role::BuildInstanceOf makes it easier to aggregate functionality into a class by automatically generating accessors for a target instance. However, the syntax for MooseX::Role::Parameterized in classic Moose is a bit verbose. With MooseX::Declare you can replace:

package MyParameterizableRole;
use MooseX::Role::Parameterized;

parameter target_method => (
    isa => "Str",
    required => 1,
);

parameter  prefix => (
    isa => "Str",
    required => 1,
    default => "test",
);

role {
    my $p = shift @_;
    requires $p->target_method;
    around $p->target_method => sub {
        my ($orig, $self, $string) = @_;
        return $p->prefix .":". $self->$orig($string);
    };
}


1;

With:

use MooseX::Declare;
role MyParameterizableRole(
    Str :$target_method!,
    Str :$prefix! = "test"
) {
    requires $target_method;
    around "$target_method" (Str $string) {
        return $self->$orig("$prefix: $string");
    }
}

That's a lot less code, much less boilerplate! Both versions would create a role that decorates a method of choice with some additional text prepended to to start of the method return. You might use it like:

use MooseX::Declare;
class MyDecoratedClass {
    method title(Str $string) {
        return "'$string' is the title";
    }
    with 'MyParameterizableRole' => {
        target_method => "title",
        prefix => "FAQ",
    };
}

You'd get a class with a decorated method. It would work like the following example:

my $object = MyDecoratedClass->new;
say $object->title("Using Moose");

## would output: "'FAQ:Using Moose' is the title"

The only thing that might confuse you is how the with keyword is located after the method declaration. I know you typically see the 'with' or 'extends' at the top of the class; this is possible when using classic Moose since if you declare a method with the 'sub' keyword that get's parsed at compile time while the 'method' keyword is runtime. This means that if the role you are aggregating requires a method (as this one does, via the 'requires' keyword), that method must be declared prior to including the role. You'll need to remember this when using roles with MooseX::Declare and the method keyword. I know this might seem strange at first but just remember the run-time versus compile-time thing and all will be well. In fact, I find that it tends to clean up and clarify the progression of activity in my classes, since instead of declaring all my roles at the top of my class, they get stuck near the point where they are being used. The addition benefit of adding the role and method late is that you can use runtime variables and control syntax, something not easy to do with compile time keywords. Generally speaking it makes the dynamic aspect of Perl even more so.

For more information about what a parameterized role is be sure to see the documentation for MooseX::Role::Parameterized.

METHOD TRAITS

In order to understand method traits, you need to remember that a method in Moose is represented internally by the MOP (Meta Object Protocol) by an instance of Moose::Meta::Method (itself a subclass of Class::MOP::Method) which mean that you can use roles to modify how a method works in the same you'd do with any class. For example, you can add a role to a method that automatically adds logging when a method is called. Your role can alter or wrap any aspect of how a method instance works, although one of the most typical and useful things to alter is how the code reference that the method executes performs its job. That's all that a method trait is, a role applied at runtime to a declared method. It can take parameters, but unlike a parameterized role, the parameters are applied to the method instance attributes. So a method trait is just a regular Moose role.

For example, here's a role that wraps the return of any method in your HTML tag of choice:

use MooseX::Declare;
role MyMethodTrait {
    use Scalar::Util 'weaken';
    has tag => (is=>'ro', isa=>'Str', required=>1, default=>'div');
    around wrap(ClassName $class: $code, %options) {
        my ($method_obj, $weak_method_obj);
        $method_obj = $weak_method_obj = $class->$orig(sub {
            my ($self, $string) = @_;
            my $title = $self->$code($string);
            my $tag = $weak_method_obj->tag;
            return "<$tag>$title</$tag>";
        }, %options);
        weaken($weak_method_obj);
        return $method_obj;
    }
}

So thats a bit complicated so I'll step through the code to make sure you understand what's going on. Right now method traits are a relatively new feature in MooseX::Declare so there's not a lot of helper functions to make the more common cases easy to do; perhaps in the future as people create more method traits we will be able to modify how this works in order to reduce some boilerplate.

use MooseX::Declare;
role MyMethodTrait {

This just sets you up to use MooseX::Declare and then declares that you are going to be making a Moose::Role.

    use Scalar::Util 'weaken';

You'll need the weaken function later in order to prevent the possibility of a circular reference causing a memory leak.

    has tag => (is=>'ro', isa=>'Str', required=>1, default=>'div');

Here you add a new attribute called 'tag' which will be applied to the consuming class. This will hold the name of the HTML tag which we intent to wrap the return of the function with. I set a sane default.

    around wrap(ClassName $class: $code, %options) {

The method wrap is a class method which is responsible for actually building the instance of your method class. Since it is a class method you need to inform MooseX::Declare that the invocant is a ClassName and not an Object as it normally is. That's why we redefine the invocant of the method signature. You'll need to do this anytime your MooseX::Declare methods are called this way.

wrap recieves two positional parameters, a CodeRef which is the actual body of the method (the stuff inside the "{ ... }" for your method) and a Hash of options. Don't worry about %options for now, just realize that if you are going to wrap wrap you'll need to pass them on to the underlying class.

        my ($method_obj, $weak_method_obj);
        $method_obj = $weak_method_obj = $class->$orig(sub {

So if you are already familiar with Moose method modifiers and how they work in MooseX::Declare this should not present too much difficulty. If you are not familiar with this you really need to take a look at Moose::Manual::MethodModifiers and the MooseX::Declare documentation. Basically we are going to invoke the wrapped method with an alternative CodeRef that changes the behavior of method body. We assign the result to $method_obj and $weak_method_obj which have been predeclared so that we can close over $weak_method_obj. We need the closure since until we call the original method (via $orig) we don't yet have a method instance, thus we don't know the value of the tag attribute. However, we jump through a few hoops here to avoid a circular reference, that's why we doubly assign the result, since we will need one unweaked reference to return at the end of the method.

        $method_obj = $weak_method_obj = $class->$orig(sub {
            my ($self, $string) = @_;
            my $title = $self->$code($string);
            my $tag = $weak_method_obj->tag;
            return "<$tag>$title</$tag>";
        }, %options);

Now inside the replacement method we will get access to $self, which is going to be the actual instance of the object which contains the method (see MyParameterizedAndTrait class definition below) and of course we get $string which is the argument we are going to be wrapping in the HTML tag of choice. Now, by the time this code reference is invoked, $weak_method_obj is going to be a real instance, so we can use it; in this case we are getting the tag attribute. Also, we have closed over $code so that we can call it ourselves. Finally we return the modified results.

Okay, so I'm not going to a lot of trouble to make sure the tag attributes is a valid HTML tag, but adding that bit would be easy with any number of CPAN modules. You'd use the method trait like so:

use MooseX::Declare;
class MyParameterizedAndTrait {
    method title(Str $string)
    does MyMethodTrait(tag=>"p")
    {
        return "'$string' is the title";
    }
    with 'MyParameterizableRole' => {
        target_method => "title",
        prefix => "FAQ",
    };
}

That 'does' (and its alias 'is') is similar to the way 'with' works on the class level, it applies the role at runtime to the instance representing the method. Parameters basically get passed down at construction time. So, if you constructed the above class and called the method 'title' like so, you get the following:

my $obj = MyParameterizedAndTrait->new;
say $obj->title("Hello World!");

## output is: "<p>'FAQ: Hello' is the title</p>"

So, right now its a bit tricky to write the method trait, although actually using is is pretty clean and straightforward. Sometimes you want to hide complexity this way, in which case a method trait is a good solution.

PARAMETERIZABLE ROLES VERUS METHOD TRAITS

So far the given examples are a bit contrived, so let's try seeing how we'd implement the same job using both techniques. Lets say you commonly need to log method calls to STDERR (like via a warn say) and you are tired of adding the following all the time:

method mymethod($arg) {
    warn "mymethod was called with $arg";
}

Also, you'd like to regularize your approach and interface so that if in the future you wanted to move to a logger with more features, such as Log::Dispatch, you can change that in one place, and not have to modified tons of code. This is a trivial, yet not toy, bit of logic. So, how might we do this with a parameterized role? Here's my take:

use MooseX::Declare;
role HasLogging(Str :$target_method!) {
    require Data::Dumper;
    requires $target_method;
    before "$target_method" {
        my ($self, @args) = @_;
        warn "$target_method was called with " . Data::Dumper::Dumper \@args;
    }
}

And here's how we might use it in a real class

use MooseX::Declare;
class LoggedMethodsParameterizableRole {
    method my_method(Str $name, Int $age) {
        return "Hi $name, you are $age years old";
    }
    with HasLogging => {target_method=>'my_method'};
}

So this is pretty straightforward, however if you want to apply it to a bunch of methods code get a bit verbose, although it would probably not be hard to change this to take an array of target methods. How might we do this with a method trait instead? Here's one way:

use MooseX::Declare;
role HasLoggingMethodTrait {
    use Scalar::Util 'weaken';
    require Data::Dumper;
    around wrap(ClassName $class: $code, %options) {
        my ($method_obj, $weak_method_obj);
        $method_obj = $weak_method_obj = $class->$orig(sub {
            my ($self, @args) = @_;
            my $target_method = $weak_method_obj->name;
            warn "$target_method was called with " . Data::Dumper::Dumper(\@args);
            return $self->$code(@args);
        }, %options);
        weaken($weak_method_obj);
        return $method_obj;
    }
}

So that's quite a bit more code, a lot of it boilerplate, but as I said this functionality is relatively new and there's a lot of opportunity for other people to jump in and improve or simplify the process. In any case you'd use it like:

use MooseX::Declare;
class LoggedMethodsMethodTrait {
    method my_method(Str $name, Int $age)
    does HasLoggingMethodTrait {
        return "Hi $name, you are $age years old";
    }
}

So two ways to solve basically the same problem. Which one to use? Personally I'd take the Parameterized role version for this particular problem, since logging is something I might want to configure and change often, and with a role I can apply late, perhaps even in configuration, or with something like MooseX::Traits. Currently you can't do this with a method trait. However for cases where the trait is very tightly tied to the desired behavior of the method, a method trait is probably the better choice.

Best practices continue to evolve and there's plenty of room open for discussion, examples and contributions. Lets here what you think!

CONCLUSION

MooseX::Declare is more than just a sweeter syntax for Moose, it aggregates and simplifies use of some valuable methods for building better Object Oriented code. Both parameterized roles and method traits offer techniques for reusing behavior and logic that can be semantically more meaningful than alternatives.

Have Fun!

GET THIS CODE

All the code listed in the above article can be checked out from Github, over at: http://github.com/jjn1056/ParameterizedRolesAndMethodTraits. That include the raw copy of this document and all the code examples broken out into individual classes with test cases and a Makefile.PL of all the required dependencies. A prepared distribution installable with the standard Perl toolchain (CPAN, App::cpanminus) will follow shortly.

Free free to fork on Github and submit your changes or corrections!

THANKS

Thanks to the members of IRC #moose and Doy in particular for assistance to checking the code for this article.

COPYRIGHT AND LICENSE

This software is copyright (c) 2010 by John Napiorkowski.

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

About

code and text for blog post on using MooseX::Declare, Parameterized Roles and Method Traits

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages