NAME
Marlin::Manual::Comparison - comparing Moo, Moose, class, and Marlin
EXAMPLES
This section shows the same class hierarchy written with Moo, Moose, the Perl class keyword, and Marlin.
Moo
Here's a simple example of some classes and roles in Moo:
use v5.20.0;
use experimental 'signatures';
use Types::Common -lexical, -types;
package Local::Example::Moo::NamedThing {
use Moo;
use MooX::StrictConstructor -late;
use MooX::TypeTiny;
has name => ( is => 'ro', isa => Str, required => 1 );
}
package Local::Example::Moo::DoesIntro {
use Moo::Role;
requires 'name';
sub introduction ( $self ) {
return sprintf( "Hi, my name is %s!", $self->name );
}
}
package Local::Example::Moo::Person {
use Moo;
use MooX::StrictConstructor -late;
extends 'Local::Example::Moo::NamedThing';
with 'Local::Example::Moo::DoesIntro';
has age => ( is => 'ro', predicate => 1 );
}
package Local::Example::Moo::Employee {
use Moo;
use MooX::StrictConstructor -late;
extends 'Local::Example::Moo::Person';
has employee_id => ( is => 'ro', required => 1 );
}
package Local::Example::Moo::Employee::Developer {
use Moo;
use MooX::StrictConstructor -late;
use MooX::TypeTiny;
use Sub::HandlesVia;
extends 'Local::Example::Moo::Employee';
has _languages => (
init_arg => undef,
is => 'lazy',
reader => 'get_languages',
clearer => 'clear_languages',
isa => ArrayRef[Str],
default => [],
handles_via => 'Array',
handles => {
add_language => 'push',
all_languages => 'elements',
},
);
around introduction => sub ( $next, $self, @args ) {
my $orig = $self->$next( @args );
if ( my @lang = $self->all_languages ) {
return sprintf( "%s I know: %s.", $orig, join q[, ], @lang );
}
return $orig;
};
}
Moose
Note that Moose has its own built-in type constraints and native traits, so we'll use them instead of Type::Tiny and Sub::HandlesVia. Apart from that, it's very similar to Moo.
We need to remember to make each class immutable or the constructors will be extremely slow.
use v5.20.0;
use experimental 'signatures';
package Local::Example::Moose::NamedThing {
use Moose;
use MooseX::StrictConstructor;
has name => ( is => 'ro', isa => 'Str', required => 1 );
__PACKAGE__->meta->make_immutable;
}
package Local::Example::Moose::DoesIntro {
use Moose::Role;
requires 'name';
sub introduction ( $self ) {
return sprintf( "Hi, my name is %s!", $self->name );
}
}
package Local::Example::Moose::Person {
use Moose;
use MooseX::StrictConstructor;
extends 'Local::Example::Moose::NamedThing';
with 'Local::Example::Moose::DoesIntro';
has age => ( is => 'ro', predicate => 'has_age' );
__PACKAGE__->meta->make_immutable;
}
package Local::Example::Moose::Employee {
use Moose;
use MooseX::StrictConstructor;
extends 'Local::Example::Moose::Person';
has employee_id => ( is => 'ro', required => 1 );
__PACKAGE__->meta->make_immutable;
}
package Local::Example::Moose::Employee::Developer {
use Moose;
use MooseX::StrictConstructor;
extends 'Local::Example::Moose::Employee';
has _languages => (
init_arg => undef,
is => 'ro',
lazy => 1,
reader => 'get_languages',
clearer => 'clear_languages',
isa => 'ArrayRef[Str]',
default => sub ($self) { [] },
traits => [ 'Array' ],
handles => {
add_language => 'push',
all_languages => 'elements',
},
);
around introduction => sub ( $next, $self, @args ) {
my $orig = $self->$next( @args );
if ( my @lang = $self->all_languages ) {
return sprintf( "%s I know: %s.", $orig, join q[, ], @lang );
}
return $orig;
};
__PACKAGE__->meta->make_immutable;
}
The core class keyword
use v5.40.0;
use experimental 'class';
use Types::Common -lexical, -assert;
class Local::Example::Core::NamedThing {
field $name :reader :param = die "Name is required";
ADJUST {
assert_Str $name;
}
}
package Local::Example::Core::DoesIntro {
use Role::Tiny;
requires 'name';
sub introduction ( $self ) {
return sprintf( "Hi, my name is %s!", $self->name );
}
}
class Local::Example::Core::Person
:isa(Local::Example::Core::NamedThing) {
field $age :reader :param = undef;
use Role::Tiny::With;
with 'Local::Example::Core::DoesIntro';
method has_age () {
return defined $age;
}
}
class Local::Example::Core::Employee
:isa(Local::Example::Core::Person) {
field $employee_id :reader :param = die "Employee id is required";
}
class Local::Example::Core::Employee::Developer
:isa(Local::Example::Core::Employee) {
field $languages :reader(get_languages) = [];
method add_language ( @lang ) {
push $languages->@*, map { assert_Str $_ } @lang;
}
method all_languages () {
return $languages->@*;
}
method clear_languages () {
$languages = [];
}
method introduction ( @args ) {
my $orig = $self->next::method( @args );
if ( my @lang = $self->all_languages ) {
return sprintf( "%s I know: %s.", $orig, join q[, ], @lang );
}
return $orig;
};
}
Marlin
use v5.20.0;
use experimental 'signatures';
use Marlin::Util -lexical, -all;
use Types::Common -lexical, -types;
package Local::Example::Marlin::NamedThing {
use Marlin 'name!' => Str;
}
package Local::Example::Marlin::DoesIntro {
use Marlin::Role -requires => [ 'name' ];
sub introduction ( $self ) {
return sprintf( "Hi, my name is %s!", $self->name );
}
}
package Local::Example::Marlin::Person {
use Marlin
-extends => 'Local::Example::Marlin::NamedThing',
-with => 'Local::Example::Marlin::DoesIntro',
qw( age? );
}
package Local::Example::Marlin::Employee {
use Marlin
-extends => 'Local::Example::Marlin::Person',
qw( employee_id! );
}
package Local::Example::Marlin::Employee::Developer {
use Marlin
-extends => 'Local::Example::Marlin::Employee',
-modifiers,
_languages => {
is => lazy,
isa => ArrayRef[Str],
init_arg => undef,
reader => 'get_languages',
clearer => 'clear_languages',
default => [],
handles_via => 'Array',
handles => {
add_language => 'push',
all_languages => 'elements',
}
};
around introduction => sub ( $next, $self, @args ) {
my $orig = $self->$next( @args );
if ( my @lang = $self->all_languages ) {
return sprintf( "%s I know: %s.", $orig, join q[, ], @lang );
}
return $orig;
};
}
BENCHMARKS
I wrote three simple coderefs to test the constructor, accessors, and delegated methods, plus one that uses all of these kinds of method in combination.
Testing constructors:
my $person_class = "Local::Example::Marlin::Person";
my $dev_class = "Local::Example::Marlin::Employee::Developer";
sub {
for my $n ( 1 .. 100 ) {
my $o1 = $person_class->new( name => 'Alice', age => $n );
my $o2 = $dev_class->new( name => 'Carol', employee_id => $n );
}
}
Testing accessors:
my $dev_class = "Local::Example::Marlin::Employee::Developer";
my $dev_object = $dev_class->new( name => 'Bob', employee_id => 1 );
sub {
for my $n ( 1 .. 100 ) {
my $name = $dev_object->name;
my $id = $dev_object->employee_id;
my $lang = $dev_object->get_languages;
}
}
Testing delegated methods:
my $dev_class = "Local::Example::Marlin::Employee::Developer";
my $dev_object = $dev_class->new( name => 'Bob', employee_id => 1 );
sub {
for my $n ( 1 .. 100 ) {
$dev_object->add_language( $_ )
for qw/ Perl C C++ Ruby Python Haskell SQL Go Rust Java /;
my @all = $dev_object->all_languages;
@all == 10 or die;
$dev_object->clear_languages;
}
};
Testing a bit of everything:
my $person_class = "Local::Example::Marlin::Person";
my $dev_class = "Local::Example::Marlin::Employee::Developer";
sub {
for my $n ( 1 .. 25 ) {
my $person = $person_class->new( name => 'Alice', age => $n );
my $dev = $dev_class->new( name => 'Carol', employee_id => $n, age => 42 );
for my $n ( 1 .. 4 ) {
$dev->age == 42 or die;
$dev->name eq 'Carol' or die;
$dev->add_language( $_ )
for qw/ Perl C C++ Ruby Python Haskell SQL Go Rust Java /;
my @all = $dev->all_languages;
@all == 10 or die;
$dev->clear_languages;
}
}
}
Results
The full benchmarking script is included in the distribution tarball, so you can run it on your own machine. Exact speeds will depend on your hardware and environment.
[[ COMPLEX CONSTRUCTORS ]]
Rate Plain Tiny Moo Moose Core Mouse Marlin
Plain 1274/s -- -5% -49% -56% -73% -78% -81%
Tiny 1334/s 5% -- -47% -54% -72% -77% -80%
Moo 2511/s 97% 88% -- -13% -48% -58% -63%
Moose 2875/s 126% 115% 14% -- -40% -51% -57%
Core 4799/s 277% 260% 91% 67% -- -19% -28%
Mouse 5910/s 364% 343% 135% 106% 23% -- -12%
Marlin 6699/s 426% 402% 167% 133% 40% 13% --
[[ COMPLEX ACCESSORS ]]
Rate Moose Core Plain Tiny Moo Marlin Mouse
Moose 17965/s -- -8% -9% -11% -48% -58% -62%
Core 19504/s 9% -- -1% -3% -44% -54% -59%
Plain 19746/s 10% 1% -- -2% -43% -54% -58%
Tiny 20197/s 12% 4% 2% -- -42% -53% -58%
Moo 34600/s 93% 77% 75% 71% -- -19% -27%
Marlin 42666/s 137% 119% 116% 111% 23% -- -10%
Mouse 47574/s 165% 144% 141% 136% 37% 12% --
[[ COMPLEX DELEGATIONS ]]
Rate Tiny Mouse Plain Moo Marlin Core Moose
Tiny 792/s -- -30% -55% -55% -56% -56% -59%
Mouse 1131/s 43% -- -36% -36% -37% -38% -42%
Plain 1767/s 123% 56% -- -1% -2% -2% -9%
Moo 1777/s 124% 57% 1% -- -1% -2% -9%
Marlin 1801/s 128% 59% 2% 1% -- -1% -8%
Core 1810/s 129% 60% 2% 2% 1% -- -7%
Moose 1950/s 146% 72% 10% 10% 8% 8% --
[[ COMPLEX COMBINED ]]
Rate Tiny Mouse Plain Moo Moose Core Marlin
Tiny 621/s -- -42% -49% -57% -58% -59% -61%
Mouse 1067/s 72% -- -13% -26% -28% -30% -33%
Plain 1230/s 98% 15% -- -15% -17% -19% -23%
Moo 1450/s 133% 36% 18% -- -2% -5% -9%
Moose 1474/s 137% 38% 20% 2% -- -3% -8%
Core 1520/s 145% 43% 24% 5% 3% -- -5%
Marlin 1599/s 157% 50% 30% 10% 9% 5% --
XSUB versus Pure Perl
The following table shows which methods were accelerated via XS.
========================================================================
Method Moo Moose Mouse Tiny Core Plain Marlin
========================================================================
[ NamedThing ]
new PP PP XS pp XS PP XS
name XS PP XS PP PP PP XS
BUILDALL pp pp xs pp -- -- XS
DEMOLISHALL pp pp xs -- -- -- --
DESTROY -- PP XS pp -- -- --
[ Person ]
new PP PP XS pp XS PP XS
name xs pp xs pp pp pp XS
age XS PP XS PP PP PP XS
has_age XS PP XS PP PP PP XS
introduction PP PP PP PP PP PP PP
BUILDALL pp pp xs pp -- -- XS
DEMOLISHALL pp pp xs -- -- -- --
DESTROY -- PP XS pp -- -- --
[ Employee ]
new PP PP XS pp XS PP XS
name xs pp xs pp pp pp XS
age xs pp xs pp pp pp XS
has_age xs pp xs pp pp pp XS
employee_id XS PP XS PP PP PP XS
introduction pp pp pp pp pp pp PP
BUILDALL pp pp xs pp -- -- XS
DEMOLISHALL pp pp xs -- -- -- --
DESTROY -- PP XS pp -- -- --
[ Employee::Developer ]
new PP PP XS pp XS PP XS
name xs pp xs pp pp pp XS
age xs pp xs pp pp pp XS
has_age xs pp xs pp pp pp XS
employee_id xs pp xs pp pp pp XS
introduction PP PP PP PP PP PP PP
get_languages PP PP XS PP PP PP XS
all_languages XS PP PP PP PP PP XS
add_language XS PP PP PP PP PP XS
BUILDALL pp pp xs pp -- -- XS
DEMOLISHALL pp pp xs -- -- -- --
DESTROY -- PP XS pp -- -- --
========================================================================
Key: XS = XSUB, PP = Pure Perl, lowercase = via inheritance.
========================================================================
SEE ALSO
Marlin::Manual::Principles - general design principles of Marlin.
AUTHOR
Toby Inkster <tobyink@cpan.org>.
COPYRIGHT AND LICENCE
This software is copyright (c) 2025-2026 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.