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;
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 Marlinline.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% --
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% --
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% --
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;
}
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' },
};
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% --
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;
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" };
}
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)