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

Marlin.

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.