NAME
Marlin::Manual::Comparison - comparing Moo, Moose, and class with Marlin
EXAMPLES
This section shows the same class hierarchy written with Moo, Moose, the Perl class keyword, Marlin, and old-school blessed hashrefs.
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 -strict, 'name!' => Str;
}
package Local::Example::Marlin::DoesIntro {
use Role::Tiny;
requires 'name';
sub introduction ( $self ) {
return sprintf( "Hi, my name is %s!", $self->name );
}
}
package Local::Example::Marlin::Person {
use Marlin
-strict,
-extends => [ 'Local::Example::Marlin::NamedThing' ],
-with => [ 'Local::Example::Marlin::DoesIntro' ],
qw( age? );
}
package Local::Example::Marlin::Employee {
use Marlin
-strict,
-extends => [ 'Local::Example::Marlin::Person' ],
qw( employee_id! );
}
package Local::Example::Marlin::Employee::Developer {
use Marlin
-strict,
-extends => [ 'Local::Example::Marlin::Employee' ],
-modifiers,
_languages => {
is => lazy,
isa => ArrayRef[Str],
init_arg => undef,
reader => 'get_languages',
clearer => 'clear_languages',
default => sub ($self) { [] },
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;
};
}
Old-School Perl
This is effectively the same thing in old-school Perl, using bless, etc. In practice, most people writing Perl classes without an OO framework would leave a lot of this out, especially a lot of the correctness checks in the constructors. It would be rare to implement BUILD methods. This also includes a homegrown basic implementation of roles.
package Local::Example::Plain::NamedThing {
use mro 'c3';
sub new ( $invocant, @args ) {
my $class = ref($invocant) || $invocant;
my %args = ( @args==1 and ref($args[0]) eq 'HASH' )
? %{shift(@args)}
: @args;
my $self = bless( {}, $class );
die "Expected name" if !exists $args{name};
die if ( !defined $args{name} or ref $args{name} );
$self->{name} = $args{name};
if ( $class eq __PACKAGE__ ) {
unless ( $args{__no_BUILD__} ) {
our $BUILD_CACHE ||= do {
no strict 'refs';
my $linear_isa = mro::get_linear_isa($class);
[
map { ( *{$_}{CODE} ) ? ( *{$_}{CODE} ) : () }
map { "$_\::BUILD" }
reverse @$linear_isa
];
};
$_->( $self, \%args ) for $BUILD_CACHE->@*;
}
my @unknown = grep !/\A(?:name)\z/, keys %args;
die "Unknown parameters: @unknown" if @unknown;
}
return $self;
}
sub name ( $self ) {
return $self->{name};
}
}
package Local::Example::Plain::DoesIntro {
sub introduction ( $self ) {
return sprintf( "Hi, my name is %s!", $self->name );
}
sub WITH ( $role, $target=undef ) {
no strict 'refs';
$target //= caller;
*{"$target\::$_"} = \&{"$role\::$_"} for qw/introduction/;
my $next = $target->can('DOES');
*{"$target\::DOES"} = sub ( $self, $query ) {
$query eq $role or $self->$next( $query );
};
return;
}
}
package Local::Example::Plain::Person {
use mro 'c3';
use parent -norequire, 'Local::Example::Plain::NamedThing';
Local::Example::Plain::DoesIntro->WITH;
sub new ( $invocant, @args ) {
my $class = ref($invocant) || $invocant;
my %args = ( @args==1 and ref($args[0]) eq 'HASH' )
? %{shift(@args)}
: @args;
my $self = $invocant->SUPER::new( %args, __no_BUILD__ => 1 );
$self->{age} = $args{age} if exists $args{age};
if ( $class eq __PACKAGE__ ) {
unless ( $args{__no_BUILD__} ) {
our $BUILD_CACHE ||= do {
no strict 'refs';
my $linear_isa = mro::get_linear_isa($class);
[
map { ( *{$_}{CODE} ) ? ( *{$_}{CODE} ) : () }
map { "$_\::BUILD" }
reverse @$linear_isa
];
};
$_->( $self, \%args ) for $BUILD_CACHE->@*;
}
my @unknown = grep !/\A(?:name|age)\z/, keys %args;
die "Unknown parameters: @unknown" if @unknown;
}
return $self;
}
sub age ( $self ) {
return $self->{age};
}
sub has_age ( $self ) {
return exists $self->{age};
}
}
package Local::Example::Plain::Employee {
use mro 'c3';
use parent -norequire, 'Local::Example::Plain::Person';
sub new ( $invocant, @args ) {
my $class = ref($invocant) || $invocant;
my %args = ( @args==1 and ref($args[0]) eq 'HASH' )
? %{shift(@args)}
: @args;
my $self = $invocant->SUPER::new( %args, __no_BUILD__ => 1 );
die "Expected employee_id" if !exists $args{employee_id};
$self->{employee_id} = $args{employee_id};
if ( $class eq __PACKAGE__ ) {
unless ( $args{__no_BUILD__} ) {
our $BUILD_CACHE ||= do {
no strict 'refs';
my $linear_isa = mro::get_linear_isa($class);
[
map { ( *{$_}{CODE} ) ? ( *{$_}{CODE} ) : () }
map { "$_\::BUILD" }
reverse @$linear_isa
];
};
$_->( $self, \%args ) for $BUILD_CACHE->@*;
}
my @unknown = grep !/\A(?:name|age|employee_id)\z/, keys %args;
die "Unknown parameters: @unknown" if @unknown;
}
return $self;
}
sub employee_id ( $self ) {
return $self->{employee_id};
}
}
package Local::Example::Plain::Employee::Developer {
use mro 'c3';
use parent -norequire, 'Local::Example::Plain::Employee';
sub new ( $invocant, @args ) {
my $class = ref($invocant) || $invocant;
my %args = ( @args==1 and ref($args[0]) eq 'HASH' )
? %{shift(@args)}
: @args;
my $self = $invocant->SUPER::new( %args, __no_BUILD__ => 1 );
if ( $class eq __PACKAGE__ ) {
unless ( $args{__no_BUILD__} ) {
our $BUILD_CACHE ||= do {
no strict 'refs';
my $linear_isa = mro::get_linear_isa($class);
[
map { ( *{$_}{CODE} ) ? ( *{$_}{CODE} ) : () }
map { "$_\::BUILD" }
reverse @$linear_isa
];
};
$_->( $self, \%args ) for $BUILD_CACHE->@*;
}
my @unknown = grep !/\A(?:name|age|employee_id)\z/, keys %args;
die "Unknown parameters: @unknown" if @unknown;
}
return $self;
}
sub get_languages ( $self ) {
$self->{_languages} //= [];
}
sub clear_languages ( $self ) {
delete $self->{_languages};
}
sub add_language ( $self, @langs ) {
for my $lang ( @langs ) {
die if ( !defined $lang or ref $lang );
}
push $self->get_languages->@*, @langs;
}
sub all_languages ( $self ) {
return $self->get_languages->@*;
}
sub introduction ( $self, @args ) {
my $orig = $self->SUPER::introduction( @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.
[[ CONSTRUCTORS ]]
Rate Plain Moose Moo Marlin Core
Plain 1101/s -- -52% -55% -64% -77%
Moose 2274/s 107% -- -6% -26% -52%
Moo 2423/s 120% 7% -- -21% -49%
Marlin 3061/s 178% 35% 26% -- -35%
Core 4741/s 331% 108% 96% 55% --
[[ ACCESSORS ]]
Rate Core Moose Plain Moo Marlin
Core 16444/s -- -9% -11% -43% -50%
Moose 18056/s 10% -- -3% -38% -45%
Plain 18561/s 13% 3% -- -36% -44%
Moo 29074/s 77% 61% 57% -- -12%
Marlin 33091/s 101% 83% 78% 14% --
[[ DELEGATIONS ]]
Rate Plain Core Moose Moo Marlin
Plain 1597/s -- -2% -9% -10% -16%
Core 1622/s 2% -- -7% -9% -15%
Moose 1746/s 9% 8% -- -2% -8%
Moo 1779/s 11% 10% 2% -- -7%
Marlin 1907/s 19% 18% 9% 7% --
[[ COMBINED ]]
Rate Plain Core Moose Moo Marlin
Plain 1143/s -- -17% -17% -21% -27%
Core 1374/s 20% -- -1% -5% -12%
Moose 1381/s 21% 1% -- -4% -12%
Moo 1441/s 26% 5% 4% -- -8%
Marlin 1562/s 37% 14% 13% 8% --
XSUB versus Pure Perl
The following table shows which methods were accellerated via XS.
===================================================================
Method Moo Moose Core Plain Marlin
===================================================================
--- [ NamedThing ] ------------------------------------------------
new PP PP XS PP XS
name XS PP PP PP XS
--- [ Person ] ----------------------------------------------------
new PP PP XS PP XS
name xs pp pp pp XS
age XS PP PP PP XS
has_age XS PP PP PP XS
introduction PP PP PP PP PP
--- [ Employee ] --------------------------------------------------
new PP PP XS PP XS
name xs pp pp pp XS
age xs pp pp pp XS
has_age xs pp pp pp XS
employee_id XS PP PP PP XS
introduction pp pp pp pp PP
--- [ Employee::Developer ] ---------------------------------------
new PP PP XS PP XS
name xs pp pp pp XS
age xs pp pp pp XS
has_age xs pp pp pp XS
employee_id xs pp pp pp XS
introduction PP PP PP PP PP
get_languages PP PP PP PP PP
all_languages PP PP PP PP PP
add_language PP PP PP PP PP
===================================================================
Key: XS = XSUB, PP = Pure Perl, lowercase = via inheritance.
===================================================================
SEE ALSO
AUTHOR
Toby Inkster <tobyink@cpan.org>.
COPYRIGHT AND LICENCE
This software is copyright (c) 2025 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.