Dreaming in mixins

masak on 2010-07-04T01:28:38

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";
        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<name>);
        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.


And with Moose in Perl 5

frew on 2010-07-04T17:15:14

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});
  my $ret = $self->$fn(@_);
  announce-end-of('test', $project{name});
  return $ret
};

The former is probably preferred because you don't have to worry about calling the original method; of course you can't munge arguments with the former, but that's a feature.

Re:And with Moose in Perl 5

masak on 2010-07-05T05:05:28

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 three quite different situations:

  • Walking along the candidate list in of matching multi subs or methods.
  • Walking up the inheritance tree in a class hierarchy.
  • Unwrapping the candy wrapping of wrapped routines.

That's a way cool generalization, but the interactions between these three (or at least the first two) are still not very well-understood. It is thought that a class-hierarchy dispatcher will constitute the "outer loop", and a multi candidate dispatcher will form the "inner loop". We're still trying that model out for size.

Re:And with Moose in Perl 5

masak on 2010-07-07T22:37:05

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.

Re:And with Moose in Perl 5

perigrin on 2010-07-08T02:29:24

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.

Re:And with Moose in Perl 5

perigrin on 2010-07-08T03:03:36

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);
            return $ret;
        }
}

I'm however unsure if callsame is implicitly a method dispatch or not. masak's example makes it seem like it is but the documentation is spares and semantically it seems like it should at *least* be .callsame if it's got an invocant.

Re:And with Moose in Perl 5

perigrin on 2010-07-08T04:26:05

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

Re:And with Moose in Perl 5

masak on 2010-07-08T10:11:39

Yes, it's $.callsame. See this post for a slightly deeper "why" answer.