The Perl Advent Calendar needs more articles for 2022. Submit your idea today!

NAME

MooseX::Extended - Extend Moose with safe defaults and useful features

VERSION

version 0.34

SYNOPSIS

    package My::Names {
        use MooseX::Extended types => [qw(compile Num NonEmptyStr Str PositiveInt ArrayRef)];
        use List::Util 'sum';

        # the distinction between `param` and `field` makes it easier to
        # see which are available to `new`
        param _name => ( isa => NonEmptyStr, init_arg => 'name' );
        param title => ( isa => Str,         required => 0 );

        # forbidden in the constructor
        field created => ( isa => PositiveInt, default => sub {time} );

        sub name ($self) {
            my $title = $self->title;
            my $name  = $self->_name;
            return $title ? "$title $name" : $name;
        }

        sub add ( $self, $args ) {
            state $check = compile( ArrayRef [ Num, 1 ] );    # at least one number
            ($args) = $check->($args);
            return sum( $args->@* );
        }

        sub warnit ($self) {
            carp("this is a warning");
        }
    }

DESCRIPTION

This module is BETA code. It's feature-complete for release and has no known bugs. We believe it's ready for production, but make no promises.

This is a quick overview. See MooseX::Extended::Manual::Tutorial for more information.

This class attempts to create a safer version of Moose that defaults to read-only attributes and is easier to read and write.

It tries to bring some of the lessons learned from the Corinna project, while acknowledging that you can't always get what you want (such as true encapsulation and true methods).

This:

    package My::Class {
        use MooseX::Extended;

        ... your code here
    }

Is sort of the equivalent to:

    package My::Class {
        use v5.20.0;
        use Moose;
        use MooseX::StrictConstructor;
        use feature qw( signatures postderef postderef_qq );
        no warnings qw( experimental::signatures experimental::postderef );
        use namespace::autoclean;
        use Carp;
        use mro 'c3';

        ... your code here

        __PACKAGE__->meta->make_immutable;
    }
    1;

It also exports two functions which are similar to Moose has: param and field.

A param is a required parameter (defaults may be used). A field is not intended to be passed to the constructor.

Note: the has function is still available, even if it's not needed. Unlike param and field, it still requires an is option.

Also, while your author likes the postfix block syntax, it's not required. You can even safely inline multiple packages in the same file:

    package My::Point;
    use MooseX::Extended types => 'Num';

    param [ 'x', 'y' ] => ( isa => Num );

    package My::Point::Mutable;
    use MooseX::Extended;
    extends 'My::Point';

    param [ '+x', '+y' ] => ( writer => 1, clearer => 1, default => 0 );

    sub invert ($self) {
        my ( $x, $y ) = ( $self->x, $self->y );
        $self->set_x($y);
        $self->set_y($x);
    }

    # MooseX::Extended will cause this to return true, even if we try to return
    # false
    0;

CONFIGURATION

You may pass an import list to MooseX::Extended.

    use MooseX::Extended
      excludes => [qw/StrictConstructor carp/],      # I don't want these features
      types    => [qw/compile PositiveInt HashRef/]; # I want these type tools

types

Allows you to import any types provided by MooseX::Extended::Types.

This:

    use MooseX::Extended::Role types => [qw/compile PositiveInt HashRef/];

Is identical to this:

    use MooseX::Extended::Role;
    use MooseX::Extended::Types qw( compile PositiveInt HashRef );

excludes

You may find some features to be annoying, or even cause potential bugs (e.g., if you have a croak method, our importing of Carp::croak will be a problem.

A single argument to excludes can be a string. Multiple excludes require an array reference:

        use MooseX::Extended excludes => [qw/StrictConstructor autoclean/];

You can exclude the following:

  • StrictConstructor

        use MooseX::Extended excludes => 'StrictConstructor';

    Excluding this will no longer import MooseX::StrictConstructor.

  • autoclean

        use MooseX::Extended excludes => 'autoclean';

    Excluding this will no longer import namespace::autoclean.

  • c3

        use MooseX::Extended excludes => 'c3';

    Excluding this will no longer apply the C3 mro.

  • carp

        use MooseX::Extended excludes => 'carp';

    Excluding this will no longer import Carp::croak and Carp::carp.

  • immutable

        use MooseX::Extended excludes => 'immutable';

    Excluding this will no longer make your class immutable.

  • true

        use MooseX::Extended excludes => 'true';

    Excluding this will require your module to end in a true value.

  • param

        use MooseX::Extended excludes => 'param';

    Excluding this will make the param function unavailable.

  • field

        use MooseX::Extended excludes => 'field';

    Excluding this will make the field function unavailable.

includes

Several optional features of MooseX::Extended make this module much more powerful. For example, to include try/catch and a method keyword:

        use MooseX::Extended includes => [ 'method', 'try' ];

A single argument to includes can be a string. Multiple includes require an array reference:

        use MooseX::Extended includes => [qw/method try/];

See MooseX::Extended::Manual::Includes for more information.

REDUCING BOILERPLATE

Let's say you've settled on the following feature set:

    use MooseX::Extended
      excludes => [qw/StrictConstructor carp/],
      includes => 'method',
      types    => ':Standard';

And you keep typing that over and over. We've removed a lot of boilerplate, but we've added different boilerplate. Instead, just create My::Custom::Moose and use My::Custom::Moose;. See MooseX::Extended::Custom for details.

IMMUTABILITY

Making Your Class Immutable

You no longer need to end your Moose classes with:

    __PACKAGE__->meta->make_immutable;

That prevents further changes to the class and provides some optimizations to make the code run much faster. However, it's somewhat annoying to type. We do this for you, via B::Hooks::AtRuntime. You no longer need to do this yourself.

Making Your Instance Immutable

By default, attributes defined via param and field are read-only. However, if they contain a reference, you can fetch the reference, mutate it, and now everyone with a copy of that reference has mutated state.

To handle that, we offer a new clone => $clone_type pair for attributes.

See the MooseX::Extended::Manual::Cloning documentation.

OBJECT CONSTRUCTION

Object construction for MooseX::Extended is identical to Moose because MooseX::Extended is Moose, so no changes are needed. However, in addition to has, we also provide param and field attributes, both of which are is => 'ro' by default.

The param is required, whether by passing it to the constructor, or using default or builder.

The field is forbidden in the constructor and is lazy if it has a builder, because that builder is often dependent on attributes set in the constructor (and why call it if it's not used?).

Here's a short example:

    package Class::Name {
        use MooseX::Extended types => [qw(compile Num NonEmptyStr Str)];

        # these default to 'ro' (but you can override that) and are required
        param _name => ( isa => NonEmptyStr, init_arg => 'name' );
        param title => ( isa => Str,         required => 0 );

        # fields must never be passed to the constructor
        # note that ->title and ->name are guaranteed to be set before
        # this because fields are lazy by default
        field name => (
            isa     => NonEmptyStr,
            default => sub ($self) {
                my $title = $self->title;
                my $name  = $self->_name;
                return $title ? "$title $name" : $name;
            },
        );
    }

See MooseX::Extended::Manual::Construction for a full explanation.

ATTRIBUTE SHORTCUTS

When using field or param, we have some attribute shortcuts:

    param name => (
        isa       => NonEmptyStr,
        writer    => 1,   # set_name
        reader    => 1,   # get_name
        predicate => 1,   # has_name
        clearer   => 1,   # clear_name
        builder   => 1,   # _build_name
    );

    sub _build_name ($self) {
        ...
    }

You can also do this:

    param name ( isa => NonEmptyStr, builder => sub {...} );

That's the same as:

    param name ( isa => NonEmptyStr, builder => '_build_name' );

    sub _build_name {...}

See MooseX::Extended::Manual::Shortcuts for a full explanation.

INVALID ATTRIBUTE DEFINITIONS

The following Moose code will print WhoAmI. However, the second attribute name is clearly invalid.

    package Some::Class {
        use Moose;

        has name   => ( is => 'ro' );
        has '-bad' => ( is => 'ro' );
    }

    my $object = Some::Class->new( name => 'WhoAmI' );
    say $object->name;

MooseX::Extended will throw a Moose::Exception::InvalidAttributeDefinition exception if it encounters an illegal method name for an attribute.

This also applies to various attributes which allow method names, such as clone, builder, clearer, writer, reader, and predicate.

Trying to pass a defined init_arg to field will also throw this exception, unless the init_arg begins with an underscore. (It is sometimes useful to be able to define an init_arg for unit testing.)

BUGS AND LIMITATIONS

You cannot (at this time) use multi subs with the debugger. This is due to a bug in Syntax::Keyword::MultiSub that should be fixed in the next release of that module.

If you must have multisubs and the debugger, the follow patch to Syntax::Keyword::MultiSub fixes the issue:

    --- old/lib/Syntax/Keyword/MultiSub.xs  2021-12-16 10:59:30 +0000
    +++ new/lib/Syntax/Keyword/MultiSub.xs  2022-08-12 10:23:06 +0000
    @@ -129,6 +129,7 @@
     redo:
         switch(o->op_type) {
           case OP_NEXTSTATE:
    +      case OP_DBSTATE:
             o = o->op_next;
             goto redo;

See also:

MANUAL

RELATED MODULES

TODO

Some of this may just be wishful thinking. Some of this would be interesting if others would like to collaborate.

Configurable Types

We provide MooseX::Extended::Types for convenience, along with the declare function. We should write up (and test) examples of extending it.

BEGIN::Lift

This idea maybe belongs in MooseX::Extended::OverKill, but ...

Quite often you see things like this:

    BEGIN { extends 'Some::Parent' }

Or this:

    sub serial_number; # required by a role, must be compile-time
    has serial_number => ( ... );

In fact, there are a variety of Moose functions which would work better if they ran at compile-time instead of runtime, making them look a touch more like native functions. My various attempts at solving this have failed, but I confess I didn't try too hard.

NOTES

There are a few things you might be interested to know about this module when evaluating it.

Most of this is written with bog-standard Moose, so there's nothing terribly weird inside, but you may wish to note that we use B::Hooks::AtRuntime and true. They seem sane, but caveat emptor.

SEE ALSO

  • Corinna

    The RFC of the new version of OOP planned for the Perl core.

  • MooseX::Modern

    MooseX::Modern - Precision classes for Modern Perl

  • Zydeco

    Zydeco - Jazz up your Perl

  • Dios

    Dios - Declarative Inside-Out Syntax

  • MooseX::AttributeShortcuts

    MooseX::AttributeShortcuts - Shorthand for common attribute options

AUTHOR

Curtis "Ovid" Poe <curtis.poe@gmail.com>

COPYRIGHT AND LICENSE

This software is Copyright (c) 2022 by Curtis "Ovid" Poe.

This is free software, licensed under:

  The Artistic License 2.0 (GPL Compatible)