NAME
Marlin::Manual::QuickStart - a Marlin quick start for Moose/Moo users
DESCRIPTION
This guide is for developers who already know Perl OO and have substantial experience with Moose and/or Moo. It skips OO fundamentals and focuses on how to map common Moose/Moo patterns to concise Marlin syntax.
The examples assume Perl 5.20+ and subroutine signatures.
The mental model
If you're coming from Moose/Moo, a useful shortcut is:
use Marlinloads your class framework and constructor.attributes are still attributes; Marlin just optimizes for common defaults.
familiar concepts like
extends,with, type constraints, defaults, builders, laziness, handles, method modifiers, and strict constructors are all there.
Marlin's main difference is that it treats verbosity as optional.
Defining a class with attributes
A straightforward Moose/Moo class:
package Local::QuickStart::Demo::User {
use Moose;
has username => ( is => 'ro', required => 1 );
has email => ( is => 'ro' );
has active => ( is => 'ro', default => sub { 1 } );
}
In Marlin, the same shape is typically:
package Local::QuickStart::User {
use Marlin qw( username! email? active? );
}
Then use constructor key-value pairs as normal:
my $u = Local::QuickStart::User->new(
username => 'alice',
email => 'alice@example.net',
);
Attribute shortcuts (required, rwp, rw, etc)
Marlin allows a compact attribute DSL directly in the use Marlin list. The common suffixes are:
!- required?- optional, and generates a predicate method (has_...)=- read-mostly (public reader plus private writer)==- read/write (public accessor).- cannot be passed to the constructor
Example:
package Local::QuickStart::Session {
use Marlin
'token!', # required, read-only
'user_id!', # required, read-only
'expires?', # optional, read-only
'seen_at=', # optional, public reader + private writer
'note==?', # optional, read/write + predicate
'cache_key.'; # not accepted by constructor
}
You can combine symbols as needed:
# combinations are allowed
use Marlin qw( id!= profile==? checksum. );
This gives you Moose/Moo-style control with less declaration boilerplate.
Type constraints and defaults
Marlin does not export a has keyword. Instead, you pass attribute names to use Marlin, optionally followed by type constraints, defaults, or full option hashrefs.
package Local::QuickStart::Event {
use Types::Common -types, -lexical;
use Marlin::Util qw( true false );
use Marlin
'id!' => Int,
'kind!' => Enum[qw(create update delete)],
'payload' => { isa => HashRef, default => {} },
'created_at' => { isa => Int, default => sub { time } },
'tags' => { isa => ArrayRef[Str], default => [] };
}
That keeps Moose/Moo-like expressiveness while staying in Marlin's native declaration style.
Inheritance and roles
Use -extends and -with options at class declaration time:
package Local::QuickStart::Model::Admin {
use Marlin
qw( permissions! audit_log? ),
-extends => 'Local::QuickStart::User',
-with => [
'Local::QuickStart::Role::CanImpersonate',
'Local::QuickStart::Role::Auditable',
];
}
For role packages, use Marlin::Role:
package Local::QuickStart::Role::Auditable {
use Marlin::Role qw( created_by! updated_by? );
}
This aligns with extends / with habits from Moose/Moo, just with less ceremony.
Porting example: a moderately complex Moose class
Suppose you start with this Moose class:
package Local::QuickStart::Demo::Job {
use Moose;
use Types::Common -types, -lexical;
extends 'Local::QuickStart::Demo::Entity';
with 'Local::QuickStart::Demo::Role::Loggable',
'Local::QuickStart::Demo::Role::Serializable';
has id => (
is => 'ro',
isa => Int,
required => 1,
);
has name => (
is => 'ro',
isa => Str,
required => 1,
);
has status => (
is => 'rw',
isa => Enum[qw(pending running done failed)],
default => 'pending',
);
has retries => (
is => 'rw',
isa => Int,
default => 0,
);
has max_retries => (
is => 'ro',
isa => Int,
default => 3,
);
has metadata => (
is => 'ro',
isa => HashRef,
default => sub { {} },
);
has warnings => (
is => 'ro',
isa => ArrayRef[Str],
default => sub { [] },
traits => ['Array'],
handles => {
add_warning => 'push',
},
);
has finished_at => (
is => 'rw',
isa => Maybe[Int],
);
before run => sub {
my ($self) = @_;
$self->log_debug('starting run');
};
sub run {
my ($self) = @_;
...
}
around as_hashref => sub {
my ($orig, $self) = @_;
my $h = $self->$orig;
$h->{status} = uc $h->{status};
return $h;
};
}
A Marlin port can stay expressive while becoming much shorter:
package Local::QuickStart::Job {
use Types::Common -types, -lexical;
use Marlin
'id!' => Int,
'name!' => Str,
'status=?' => {
isa => Enum[qw(pending running done failed)],
default => 'pending',
},
'retries=' => { isa => Int, default => 0 },
'max_retries' => { isa => Int, default => 3 },
'metadata' => { isa => HashRef, default => {} },
'warnings' => {
isa => ArrayRef[Str],
default => [],
handles_via => 'Array',
handles => { add_warning => 'push' },
},
'finished_at=' => { isa => Maybe[Int] },
-extends => 'Local::QuickStart::Entity',
-with => [
'Local::QuickStart::Role::Loggable',
'Local::QuickStart::Role::Serializable',
],
-modifiers;
before run => sub ( $self ) {
$self->log_debug('starting run');
};
sub run ( $self ) {
return 'ok';
}
around as_hashref => sub ( $next, $self ) {
my $h = $self->$next;
$h->{status} = uc $h->{status};
return $h;
};
}
Notes for Moose/Moo users:
You can still use rich type constraints and coderef defaults.
You can still use inheritance, roles, delegates, and method modifiers.
You can mix terse declarations (symbol suffixes) with explicit attribute option hashrefs where detail matters.
Most classes become noticeably smaller without losing intent.
SEE ALSO
Marlin::Manual::Beginning, Marlin::Manual::BetterAttributes, Marlin::Manual::BetterMethods, Marlin::Manual::ClassOptions, Marlin::Manual::Comparison, Marlin::Manual::Principles.
AUTHOR
Toby Inkster <tobyink@cpan.org>.
COPYRIGHT AND LICENCE
This software is copyright (c) 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.