Stories
Slash Boxes
Comments
NOTE: use Perl; is on undef hiatus. You can read content, but you can't post it. More info will be forthcoming forthcomingly.

All the Perl that's Practical to Extract and Report

use Perl Log In

Log In

[ Create a new account ]

masak (6289)

masak
  (email not shown publicly)
http://masak.org/carl

Been programming Perl since 2001. Found Perl 6 somewhere around 2004, and fell in love. Now developing November (a Perl 6 wiki), Druid (a Perl 6 board game), pls (a Perl 6 project installer), GGE (a regex engine), and Yapsi (a Perl 6 implementation). Heavy user of and irregular committer to Rakudo.

Journal of masak (6289)

Saturday July 03, 2010
08:28 PM

Dreaming in mixins

[ #40434 ]

Working with pls (a next-gen project installer for the Perl 6 ecosystem), I had a few classes with code like this:

class POC::Tester does App::Pls::Tester {
    method test($project --> Result) {
        my $target-dir = "cache/$project<name>";
        if "$target-dir/Makefile" !~~ :e {
            return failure;
        }
        unless run-logged( relative-to($target-dir, "make test"),
                           :step('test'), :$project ) {
            return failure;
        }

        return success;
    }
}

(success and failure are Result enum values defined elsewhere. They felt like pleasant documentation, and when return type checking works, they'll even help catch errors!)

Now, I wanted to add super-simple progress diagnostics to this method. I wanted an announce-start-of('test', $project); at the start of the module, and either an announce-end-of('test', success); or an announce-end-of('test', failure);, depending on the success or failure of the method.

I have a low threshold for boilerplate. After realizing that I'd have to manually add those calls in the beginning of the method, and before each return — and not only in this method, but in several others — I thought "man, I shouldn't have to tolerate this. This is Perl 6, it should be able to do better!"

So I thought about what I really wanted to do. I wanted some sort of... method wrapper. Didn't really want a subclass, and a regular role wouldn't cut it (because class methods override same-named role methods).

Then it struck me: mixins. Did those already work in Rakudo? Oh well, try it and see. So I created this role:

role POC::TestAnnouncer {
    method test($project --> Result) {
        announce-start-of('test', $project&lt;name&gt;);
        my $result = callsame;
        announce-end-of('test', $result);
        return $result;
    }
}

And then, later:

POC::Tester.new() does POC::TestAnnouncer

And it worked! On the first attempt! jnthn++!

(If you're wondering what in the above method that does the wrapping — it's the callsame call in the middle. It delegates back to the overridden method. Note that with this tactic, I get to write my announce-start-of and announce-end-of calls exactly once. I don't have to go hunting for all the various places in the original code where a return is made.)

I guess this counts as using mixins to do Aspect-Oriented Programming. This way of working certainly makes the code less scattered and tangled.

So, in this file, I currently have a veritable curry of dependency injection, behavior-adding roles, lexical subs inside methods, AOP-esque mixins, and a MAIN sub. They mix together to create something really tasty. And it all runs, today, under Rakudo HEAD.

As jnthn said earlier today, it's pretty cool that a script of 400 LoC, together with a 230-LoC module, make up a whole working installer. With so little code, it almost doesn't feel like coding.

The Fine Print: The following comments are owned by whoever posted them. We are not responsible for them in any way.
 Full
 Abbreviated
 Hidden
More | Login | Reply
Loading... please wait.
  • With Perl 5 that would look like either:

    package POC::TestAnnouncer;

    use Moose::Role;

    before test => sub {
      my $project = $_[1];
      announce-start-of('test', $project{name});
    };

    after test => sub {
      my $project = $_[1];
      announce-end-of('test', $project{name});
    };

    And then there's the more powerful around:

    package POC::TestAnnouncer;

    use Moose::Role;

    around test => sub {
      my $fn = shift;
      my $self = shift;
      my $project = $_[0];
      announce-start-of('test', $project{name});
    --
    --fREW
    http://blog.afoolishmanifesto.com
    • Thanks. I was only aware of how Moose did it to the extent that people come into the #perl6 channel sometimes and ask "what's the equivalent to Moose's before/after/around in Perl 6?" and we answer "[call|next][same|with]".

      (The "call-" methods are returning calls, and the "next-" methods are tailcalls. "-same" sends along the original arguments, and "-with" allows you to send new ones.)

      What's really neat, and what I still haven't quite gotten my head wrapped around, is that these four routines are used in t

      • Oh, huh, and I should probably add that mixins may look like they do a variant of wrapping in Perl 6, but they really work by creating an anonymous subclass to the class of the object, and re-blessing the object to that subclass. So, they work by inheritance, not wrapping.

        The fact that both of these use "callsame;" to delegate to the original routine means that I can use mixins and think they are wrappers, and they'll still behave as I expect. That's why I like the above unification.

        • The Moose version Frew posted uses Roles. So applying this "mixin" at Runtime to an instance will also derive an anonymous subclass with the Role applied and re-bless an instance into that subclass.

        • Also playing about some I came up with something nearly identical using MooseX::Declare's syntax.


          role POC::TestAnnouncer {
                  use mro;

                  method test ($project) {
                      announce-start-of('test', $project{name});
                      my $result = $self->maybe::next::method($project); # callsame
                      announce-end-of('test', $result);
                 

          • I mean $.callsame rather than .callsame. Thanks to TimToady and TiMBuS for clarifying things at least enough that I understand that much.