Thu Jul 19 12:30:00 2012

Moo versus Any::Moose

Mouse was originally written by Sartak as an exercise in understanding metaprotocol implementation better so he could better contribute to Moose - but because of its faster startup time and capacity to install as pure perl became a common answer to the question "What do I use if I want something lighter weight than Moose?".

However, since Mouse is a separate package with its own metaprotocol, a solution was required that allowed use of Mouse while still being able to use the same libraries in Moose code; Any::Moose is that solution.

Moo, on the other hand, was built as an intentional answer to the question, so doesn't require any extra modules to be usable from Moose code. Herein an explanation of why that makes Moo an actively better choice.

How does Any::Moose work?

When you first load Any::Moose, it selects a backend to use for all subsequent invocations of itself. The decision process goes like this:

If the environment variable ANY_MOOSE is set, use what it tells you to.

If Moose is already loaded, use Moose.

If Mouse loads, use Mouse.

If Moose loads, use Moose.

Couldn't load anything; die screaming.

Once that decision is made, it's consistent for every use of Any::Moose for the rest of the lifetime of your perl process.

This means that in Moose using code, Any::Moose will hopefully detect Moose as already loaded and your Any::Moose classes and roles will use Moose, and in other code Any::Moose will use Mouse and you can get the startup benefits.

How does Moo work?

Moo has no metaprotocol, intentionally, since I don't find one less capable than the Moose metaprotocol useful and Moose already implements that very well.

However, as a Moo class is constructed, the information used to perform the construction is retained in data structures inside the Moo and Moo::Role packages.

If Moo detects that Moose has loaded, it creates a dummy metaclass for each Moo class and role using:

Class::MOP::store_metaclass_by_name(
  $name, bless({ name => $name }, 'Moo::HandleMoose::FakeMetaClass')
);

and this then provides an AUTOLOAD that means if any method is called on this, it's upgraded at that point to a full Moose metaclass/metarole:

sub AUTOLOAD {
  my ($meth) = (our $AUTOLOAD =~ /([^:]+)$/);
  require Moo::HandleMoose;
  Moo::HandleMoose::inject_real_metaclass_for((shift)->{name})->$meth(@_)
}

Since this upgrading happens on demand, you only pay the price of creating a Moose metaclass if you're actually using it, which is in keeping with Moo's "pay for what you use" philosophy.

So why is Moo better than Any::Moose?

Simple: Moo is not load order dependent. Any::Moose is.

Look at the decision process for Any::Moose again - if Any::Moose is loaded before the first Moose using module in your application, it will already have selected Mouse - and now there's no going back.

To see why this matters, try running the following script:

use IO::All;
io->dir('lib')->mkpath;
io->file('lib/MyClass.pm')->print(<<'END');
package MyClass;

use Moose;

with 'MyRole';

sub fnargle { 42 }

1;
END
io->file('lib/MyRole.pm')->print(<<'END');
package MyRole;

BEGIN {
  if ($ENV{"USE_ANY_MOOSE"}) {
    eval "use Any::Moose 'Role'";
  } elsif ($ENV{"USE_MOO"}) {
    eval "use Moo::Role";
  } else {
    die "Set one of the env vars damn you!";
  }
}

requires 'fnargle';

1;
END

Now you've got the files to play with, observe:

$ USE_MOO=1 perl -Ilib -MMyClass -E 'say MyClass->new->fnargle'
42
$ USE_ANY_MOOSE=1 perl -Ilib -MMyClass -E 'say MyClass->new->fnargle'
42

So far, so good. But what happens if MyRole got loaded first?

$ USE_MOO=1 perl -Ilib -MMyRole -MMyClass -E 'say MyClass->new->fnargle'
42

Moo works fine, because when MyClass loads Moose, Moo's moose interoperability goes back and creates the fake metaclass for MyRole, and then when MyClass uses MyRole it gets turned into a real Moose::Meta::Role and everything works.

Any::Moose, on the other hand, will have already picked Mouse, which means:

$ USE_ANY_MOOSE=1 perl -Ilib -MMyRole -MMyClass -E 'say MyClass->new->fnargle'
You can only consume roles, MyRole is not a Moose role at /home/matthewt/perl5/lib/perl5/x86_64-linux-gnu-thread-multi/Moose/Util.pm line 138
        Moose::Util::_apply_all_roles('Moose::Meta::Class=HASH(0xb67c18)', undef, 'MyRole') called at /home/matthewt/perl5/lib/perl5/x86_64-linux-gnu-thread-multi/Moose/Util.pm line 99
        Moose::Util::apply_all_roles('Moose::Meta::Class=HASH(0xb67c18)', 'MyRole') called at /home/matthewt/perl5/lib/perl5/x86_64-linux-gnu-thread-multi/Moose.pm line 67
        Moose::with('Moose::Meta::Class=HASH(0xb67c18)', 'MyRole') called at /home/matthewt/perl5/lib/perl5/x86_64-linux-gnu-thread-multi/Moose/Exporter.pm line 370
        Moose::with('MyRole') called at lib/MyClass.pm line 5
        require MyClass.pm called at -e line 0
        main::BEGIN() called at lib/MyClass.pm line 0
        eval {...} called at lib/MyClass.pm line 0
Compilation failed in require.
BEGIN failed--compilation aborted.

Oops.

For added bonus fun, loading order in a large codebase can vary between machines - in Catalyst or DBIx::Class applications their plugin loading code can load things in different orders depending on your filesystem and/or the hash key ordering of the perl5 VM on the machine.

So this sort of problem can show up in production but not in testing.

I don't fancy trying to debug that in the middle of a deployment. Do you?

In conclusion

Moo does have some limitations compared to Mouse - as yet, no native traits, a slightly less compatible surface syntax, and less optional XS (although we do use Class::XSAccessor if it's installed and more such optional speedups are planned).

However, if you want your code to be usable standalone with fast startup times, and to co-operate with downstream users who're likely to be using Moose, Moo is a far superior solution to Any::Moose.

Moo was designed from the ground up to answer the question "What do I use if I want something lighter weight than Moose?". I think it shows.

Here's hoping it's as good an answer for you as it is for me.

-- mst, out.