Introducing Marlin

Published: (December 18, 2025 at 04:44 PM EST)
6 min read
Source: Dev.to

Source: Dev.to

Cover image for Introducing Marlin

Toby Inkster

Introduction

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;

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 its name on the use Marlin line.
  • It can be followed by options, but if you’re happy with Marlin’s defaults (read‑only attributes), you don’t need them.
  • Use ! to quickly mark an attribute as required instead of { required => true }.
  • Use ? to request a predicate method instead of { 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%     --

Only the new Perl core class keyword generates a constructor faster than Marlin’s, and it is significantly faster. 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%     --

By “accessors” I mean not just standard getters 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 also creates aliases for parent‑class methods directly in the child’s symbol table, allowing Perl to bypass a lot of the normal method‑resolution overhead.

I expected class to perform better; its readers and writers are currently implemented in pure Perl, though there is room for improvement 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%     --

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

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

As part of the attribute definition

use Marlin
  languages => {
    is  => 'ro',
    isa => ArrayRef[Str],
    default => sub { [] },
    handles => {
        add_language => 'push',
        all_languages => 'elements',
    },
  };

Code Example

=> ArrayRef[Str],
   default      => [],
   handles_via  => 'Array',
   handles      => { add_language => 'push', count_languages => 'count' },
};

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.

Note: The default => [] is intentional. You can set an empty arrayref or hashref as a default, and Marlin will treat it as if you wrote default => sub { [] }. It cleverly skips calling the coderef (which is slow) and instead creates a reference to a new empty array in XS (fast)!

Combined Benchmark

RateTinyPlainCoreMooseMooMarlin
Tiny545/s-48%-56%-58%-60%-64%
Plain1051/s93%-16%-19%-22%-31%
Core1249/s129%19%-4%-8%-18%
Moose1304/s139%24%4%-4%-14%
Moo1355/s148%29%8%4%-11%
Marlin1519/s179%45%22%17%12%

A realistic snippet that constructs objects and calls a bunch of accessors and delegations shows Marlin performing 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;
  • The ->& operator was added in Perl 5.42. On older Perls (5.12+), lexical methods are still supported but must be called with function‑call syntax, e.g., internal_id($w).
  • storage => "PRIVATE" tells Marlin to use inside‑out storage for that attribute, so accessing it directly via $obj->{internal_id} will not work. This provides true private attributes.
  • On Perl 5.18 and newer you can also declare lexical methods with the normal my sub foo syntax, giving you 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" };
}
  • Constant attributes are declared like regular ones but are always read‑only and cannot be passed to the constructor.
  • They support delegations, provided the delegated method does not attempt to change the value.

Perl Version Support

Although some lexical features require newer Perl versions, Marlin runs on Perl 5.8.8 and later.

Future Directions

Some ideas I’ve had:

  • If Moose is loaded, create meta‑object‑protocol stuff for Marlin classes and roles, similar to what Moo does.
Back to Blog

Related posts

Read more »

The Cost of a Closure in C

Article URL: https://thephd.dev/the-cost-of-a-closure-in-c-c2y Comments URL: https://news.ycombinator.com/item?id=46228597 Points: 7 Comments: 0...