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.

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.