DEV Community

Cover image for Introducing Marlin
Toby Inkster
Toby Inkster

Posted on • Originally published at toby.ink

Introducing Marlin

Does the Perl world need another object-oriented programming framework?

To be honest, probably not.

But here’s why you might want to give Marlin a try anyway.

  • Most of your constructors and accessors will be implemented in XS and be really, really fast.

  • If you accept a few basic principles like “attributes should usually be read-only”, it can be really, really concise to declare a class and its attributes.

An example

  use v5.20.0;
  use experimental qw(signatures);

  # Import useful constants, types, etc.
  use Marlin::Util -all, -lexical;
  use Types::Common -all, -lexical;

  package Person {
    use Marlin
      'given_name!'   => NonEmptyStr,
      'family_name!'  => NonEmptyStr,
      'name_style'    => { enum => [qw/western eastern/], default => 'western' },
      'full_name'     => { is => lazy, builder => true },
      'birth_date?';

    sub _build_full_name ( $self ) {
      return sprintf( '%s %s', uc($self->family_name), $self->given_name )
        if $self->name_style eq 'eastern';
      return sprintf( '%s %s', $self->given_name, $self->family_name );
    }
  }

  package Payable {
    use Marlin::Role
      -requires => [ 'bank\_details' ];

    sub make_payment ( $self ) {
      ...;
    }
  }

  package Employee {
    use Marlin
      -extends => [ 'Person' ],
      -with    => [ 'Payable' ],
      'bank_details!' => HashRef,
      'employee_id!'  => Int,
      'manager?'      => { isa => 'Employee' };
  }

  my $manager = Employee->new(
    given_name    => 'Simon',
    family_name   => 'Lee',
    name_style    => 'eastern',
    employee_id   => 1,
    bank_details  => {},
  );

  my $staff = Employee->new(
    given_name    => 'Lea',
    family_name   => 'Simons',
    employee_id   => 2,
    bank_details  => {},
    manager       => $manager,
  );

  printf(
    "%s's manager is %s.\n",
    $staff->full_name,
    $staff->manager->full_name,
  ) if $staff->has_manager;
Enter fullscreen mode Exit fullscreen mode

Some things you might notice:

  • It supports most of the features of Moose… or most of the ones you actually use anyway.

  • Declaring an attribute is often as simple as listing it’s name on the use Marlin line.

  • It can be followed by some options, but if you’re happy with Marlin’s defaults (read-only attributes), it doesn’t need to be.

  • You can use the ! to quickly mark an attribute as required instead of the longer { required => true }.

  • You can use ? to request a predicate method instead of the longer { predicate => true }.

Benchmarks

My initial benchmarking shows that Marlin is fast.

Constructors

         Rate   Tiny  Plain    Moo  Moose Marlin   Core
Tiny   1317/s     --    -2%   -48%   -53%   -54%   -72%
Plain  1340/s     2%     --   -47%   -53%   -53%   -72%
Moo    2527/s    92%    89%     --   -11%   -12%   -47%
Moose  2828/s   115%   111%    12%     --    -2%   -40%
Marlin 2873/s   118%   114%    14%     2%     --   -39%
Core   4727/s   259%   253%    87%    67%    65%     --
Enter fullscreen mode Exit fullscreen mode

Only the new Perl core class keyword generates a constructor faster than Marlin’s. And it is significantly faster; there’s no denying that. However, object construction is only part of what you are likely to need.

Accessors

          Rate   Tiny  Moose  Plain   Core    Moo Marlin
Tiny   17345/s     --    -1%    -3%    -7%   -36%   -45%
Moose  17602/s     1%     --    -2%    -6%   -35%   -44%
Plain  17893/s     3%     2%     --    -4%   -34%   -44%
Core   18732/s     8%     6%     5%     --   -31%   -41%
Moo    27226/s    57%    55%    52%    45%     --   -14%
Marlin 31688/s    83%    80%    77%    69%    16%     --
Enter fullscreen mode Exit fullscreen mode

By accessors, I’m talking about not just standard getter and setters, but also predicate methods and clearers. Marlin and Moo both use Class::XSAccessor when possible, giving them a significant lead over the others. Marlin uses some sneaky tricks to squeeze out a little bit of extra performance by creating aliases for parent class methods directly in the child class symbol tables, allowing Perl to bypass a lot of the normal method resolution stuff.

I really expected class to do a lot better than it does. Its readers and writers are basically implemented in pure Perl currently, though I guess there’s scope to improve them in future releases.

Native Traits / Handles Via / Delegations

         Rate   Tiny   Core  Plain  Moose    Moo Marlin
Tiny    675/s     --   -56%   -57%   -59%   -61%   -61%
Core   1518/s   125%     --    -4%    -8%   -13%   -13%
Plain  1581/s   134%     4%     --    -4%    -9%   -10%
Moose  1642/s   143%     8%     4%     --    -5%    -6%
Moo    1736/s   157%    14%    10%     6%     --    -1%
Marlin 1752/s   160%    15%    11%     7%     1%     --
Enter fullscreen mode Exit fullscreen mode

If you don’t know what I mean by native traits, it’s the ability to create small methods like this:

  sub add_language ( $self, $lang ) {
    push $self->languages->@*, $lang;
  }
Enter fullscreen mode Exit fullscreen mode

As part of the attribute definition:

  use Marlin
    languages => {
      is           => 'ro',
      isa          => ArrayRef[Str],
      default      => [],
      handles_via  => 'Array',
      handles      => { add_language => 'push', count_languages => 'count' },
    };
Enter fullscreen mode Exit fullscreen mode

There’s not an awful lot of difference between the performance of most of these, but Marlin slightly wins. Marlin and Moose are also the only frameworks that include this out of the box without needing extension modules.

By the way, that default => [] was not a typo. You can set an empty arrayref or empty hashref as a default, and Marlin will assume you meant something like default => sub { [] }, but it cleverly skips over needing to actually call the coderef (slow), instead creating a reference to a new empty array in XS (fast)!

Combined

         Rate   Tiny  Plain   Core  Moose    Moo Marlin
Tiny    545/s     --   -48%   -56%   -58%   -60%   -64%
Plain  1051/s    93%     --   -16%   -19%   -22%   -31%
Core   1249/s   129%    19%     --    -4%    -8%   -18%
Moose  1304/s   139%    24%     4%     --    -4%   -14%
Moo    1355/s   148%    29%     8%     4%     --   -11%
Marlin 1519/s   179%    45%    22%    17%    12%     --
Enter fullscreen mode Exit fullscreen mode

A realistic bit of code that constructs some objects and calls a bunch of accessors and delegations on them. Marlin performs very well.

Lexical accessors and private attributes

Marlin has first class support for lexical methods!

  use v5.42.0;

  package Widget {
    use Marlin
      name        => { isa => Str },
      internal_id => { reader => 'my internal_id', storage => 'PRIVATE' };

    ...

    printf "%d: %s\n", $w->&internal_id, $w->name, 
  }

  # dies because internal_id is lexically scoped
  Widget->new->&internal_id;
Enter fullscreen mode Exit fullscreen mode

Support for the ->& operator was added in Perl 5.42. On older Perls (from Perl 5.12 onwards), lexical methods are still supported but you need to use function call syntax (internal_id($w)).

The storage => "PRIVATE" hint tells Marlin to use inside-out storage for that attribute, meaning that trying to access the internal_id by poking into the object’s internals ($obj->{internal_id}) won’t work.

This gives you true private attributes.

On Perl 5.18 and above, you can of course declare lexical methods using the normal my sub foo syntax, so you have private attributes as well as private methods.

Constant attributes

  package Person {
    use Marlin
      name         => { isa => Str, required => true },
      species_name => { isa => Str, constant => "Homo sapiens" };
  }
Enter fullscreen mode Exit fullscreen mode

Constant attributes are declared like regular attributes, but are always very read-only and illegal to pass to the constructor.

Like other attributes, they support delegations, provided the delegated method isn’t one which could change the value.

Perl version support

Although some of the lexical features need newer versions of Perl, Marlin runs on Perl versions as old as 5.8.8.

Future directions

Some ideas I’ve had:

  • If Moose is loaded, create meta object protocol stuff for Marlin classes and roles, like Moo does.

Top comments (0)