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 Marlin loads 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.