Skip to content

Porting from Moose or Moo

Ovid edited this page Feb 23, 2022 · 17 revisions

Please leave feedback here.

Preface

This is a work in progress. Some of the examples won't be exact mirrors of one another because I don't want to keep repeating some core assumptions of Corinna (such as true encapsulation). Also, for now, we'll try to stick with features which will be in released in the early versions of Corinna, not the full spec. Further, for now, this document is woefully incomplete.

For those new to Corinna, here's a quick guide on how to convert from Moose or Moo (Moo/se) to Corinna. We will cover some of the basics.

Assume all Corinna examples begin with:

use feature 'class';

Assume all Moose examples end with:

__PACKAGE->meta->make_immutable;

Internally, __PACKAGE__->meta->make_immutable makes the Moose metaclass definition immutable, not the object itself. However, it has the benefit of a significant performance improvement. There is no analog to make_immutable in Corinna because it is not necessary (it's also not necessary in Moo, but it's supported for those who wish to make it easier to upgrade from Moo to Moose.

Attributes

Instance Attributes

In Moo/se, object attributes hold state for the object. In Corinna, they are called 'fields' and are declared with the field keyword.

An attribute in Corinna is declared with the field keyword. Inside the class, you can access its value directly in methods:

class Point {
    field ( $x, $y ) :param;

    sub  move_x ($dx) {
        $x += $dx; # no method call needed
    }

    sub  move_y ($dy) {
        $y += $dy; # no method call needed
    }
}

Read-write attributes

Using the first example from the Moose attribute POD:

package Person;
 
use Moose;
 
has 'first_name' => ( is => 'rw' );

In Corinna, that would be:

class Person {
    field $first_name :reader :writer :param;
}

field $first_name simply declared an instance variable. By default, they are not exposed outside the class without a :reader or :writer attribute (or both).

:reader allows one to call $person->first_name to get the first name.

:writer allows one to set the first name via $person->set_first_name("Bob"); The "writer" is set_first_name, not first_name (this may change). Because Perl does not, at the present time, allow multimethods, we have preferred to not build in a special case for this.

Again, this may change.

:param says "you can pass this value as a parameter to the constructor."

Read-only attributes

Moose:

has name => ( is => 'ro' );

Corinna:

field $name :reader :param;

Private attributes

Moose:

# you can also use `is => 'bare'`, but in practice, no one
# does because it's painful to use
has _private => ( is => 'rw' );

Corinna:

field $private;

Note that the Corinna version is really private. The Moose version still lets you call $object->_private. If you really don't want the method in Moose, you can set is => 'bare' (which we won't go into here), but the developer can still use $object->{_private} to get at the data. This is not possible in Corinna (though the developer can jump through a few hoops using the MOP to get at that data).

Non-constructor attributes

You may wish to declare instance data that cannot be set via the constructor. By default, with Moo/se, everything can be set via the constructor and you must ask with init_arg for it not to be set.

Moose:

has some_data => {
    is       => 'ro',
    init_arg => undef, # not set via constructor
);

Corinna:

Simply omit the :param modifier.

field $some_data :reader;

Default values

In Moose, you set a default value via the default setting in the has option list, or via a builder (there are a number of ways of doing this, but we'll use a common idiom):

has _cache => (
    is       => 'ro',
    init_arg => undef,
    builder  => '_build__cache',
);

sub _build__cache {
    return Some::Cache->new;
}

Corinna:

field $cache { Some::Cache->new };

Note that one significant difference between these two styles is in in Moo/se, a subclass can override _build__cache and provide a different value. That is not the case for Corinna. I explain why here. If Perl had a decent type system, this would be safer, but for now, it does not.

That being said, sometimes you do want to make it easy to override a parent's "build" method for an attribute. For the first version of Corinna, a common approach would be this:

field $cache { $self->_build_cache };

method _build_cache () { Some::Cache->new }

However, don't do this by default. Do it on a case-by-case basis where you have no choice in the matter. It's much safer that way. The common Moo/se builder idiom is sometimes useful, but we should use it only when needed, not just because it's easy.

Note that this is one of the many features we may revisit after the MVP.

Class Attributes

Class data is shared across all instances of a class. If it's changed, it's changed for all instances of a class. As such, it's "global" to that class. For this reason, its use is generally discouraged. However, if you need it ...

Moose:

package My::Class;
 
use Moose;
use MooseX::ClassAttribute;
 
class_has 'Cache' => (
    is      => 'rw',
    isa     => 'HashRef',
    default => sub { My::Cache->new },
); 

For Corinna, use the :common modifier:

class My::Class {
    field $cache :common { My::Cache->new };
}

Object Construction

In Moo/se, by default, every attribute you list can be passed to the constructor unless you pass init_arg => undef in the attribute definition.

Constructor Arguments

Moose:

package Customer {
    use Moose;
    has name => ( is => 'ro' );
}
my $cust1 = Customer->new( name => 'bob' );

By contrast, for Corinna. you must opt-in to allowing a field to be passed to the constructor. You do this by using the :param modifier:

class Customer {
    field $name :reader :param;
}

Constructor Argument Formatting

In Moose, you may pass either an even-sized list or a hashref.

package Customer {
    use Moose;
    has name => ( is => 'ro' );
}

# the following two lines are equivalent
my $cust1 = Customer->new( name => 'bob' );
my $cust2 = Customer->new( { name => 'bob' } );

This can lead to subtle bugs:

my $cust2 = Customer->new( \%customer );

If you've built up the %customer hash programatically, a duplicate key will overwrite a previous one. To be fair, we don't hear this complaint often.

In Corinna, you must always pass an even-sized list:

class Customer {
    field $name :reader :param;
}

# Valid
my $cust1 = Customer->new( name => 'bob' );

# runtime error
my $cust2 = Customer->new( { name => 'bob' } );

Note that in Corinna, we also test that the list is really even-sized and, when treated as a hash, we verify that the keys are valid strings and not references. We try to have a standard, safe syntax.

Required and Optional Arguments

In Moose, if you want an argument to the constructor be required, you pass required => 1 to the attribute definition (but it's not necessarily required if you have a default or builder defined, but we'll ignore that for now). In our above examples, failing to pass name to the constructor is fine and results in an undefined name.

package Customer {
    use Moose;
    has name => ( is => 'ro', required => 1 );
}

# fatal
my $cust1 = Customer->new;

In Corinna, any field declare with :param is defined by required by default. Any field with :param is optional in the constructor if you pass a field initialize block.

class Customer1 {
    field $name :param { 'no name' };
}

class Customer2 {
    field $name :param;
}

# allowed
my $cust1 = Customer1->new;

# fatal
my $cust2 = Customer2->new; 

MooseX::StrictConstructor

In Moose, any unknown arguments passed to the constructor are silently discarded by default:

package Customer {
    use Moose;
    has name => ( is => 'ro' );
}
my $cust1 = Customer->new( name => 'bob', age => 26 );
use Data::Dumper;
print Dumper($cust1);
__END__
$VAR1 = bless( {
                 'name' => 'bob'
               }, 'Customer' );

Note that the age argument is not present.

If you wish to avoid this, you must use the MooseX::StrictConstructor module (or write your own).

package Customer {
    use Moose;
    use MooseX::StrictConstructor;
    has name => ( is => 'ro' );
}

# fatal error
my $cust1 = Customer->new( name => 'bob', age => 26 );

By contrast, with Corinna, any unknown arguments to the constructor are fatal, even if there is a corresponding field.

class Customer {
    field $name :reader :param;
    field $age;    # no :param modifier!
}

# fatal error
my $cust1 = Customer->new( name => 'bob', age => 26 );

BUILD

In Moose, the BUILD method is used to modify the object after construction, but before it's returned to the caller.

Moose:

package Customer {
    use Moose;
    has name         => ( is => 'ro' );
    has country_code => ( is => 'ro' );
    has ssn          => ( is => 'ro' );

    sub BUILD {
        my $self = shift;

        if ( $self->country_code eq 'us' ) {
            die 'All US residents must have an SSN'
                unless $self->ssn;
        }
    }
}

In Corinna, BUILD is named ADJUST.

class Customer {
    field $name         :param :reader;
    field $country_code :param :reader;
    field $ssn          :param :reader;

    ADJUST {
        if ( $country_code eq 'us' ) {
            die 'All US residents must have an SSN'
                unless $ssn;
        }
    }
}

BUILDARGS

There is currently no analog to BUILDARGS in Corinna. Instead, use an alternate constructor using :common to identify it as a class method.

Here's one way to munge arguments in Moose. We have a Box class which, if you instantiate it with a single argument, uses that argument for the height, width, and depth.

package Box {
    use Moose;
    has [qw/height width depth/] => ( is => 'ro', required => 1 );
    has volume => (
        is       => 'ro',
        init_arg => undef,
        lazy     => 1, # must be built after height, width, depth
        default  => sub {
            my $self = shift;
            return $self->height * $self->width * $self->depth;
        }
    );

    around 'BUILDARGS' => sub {
        my ( $orig, $class, @args ) = @_;
        if ( 1 == @args && !ref $args[0] ) {
            my $length = shift @args;
            @args = ( height => $length, width => $length, depth => $length );
        }
        return $class->$orig(@args);
    };
}

my $cube = Box->new(3);

Here's that code in Corinna using an alternate constructor:

class Box {
    field ( $height, $width, $depth ) :reader :param;
    field $volume :reader { $height * $width * $depth };

    method new_cube :common ($length) {
        return $class->new( height => $length, width => $length, depth => $length );
    }
}

my $cube = Box->new_cube(3);

To be fair, here's that class in Moose using an alternate constructor:

package Box {
    use Moose;
    has [qw/height width depth/] => ( is => 'ro', required => 1 );
    has volume => (
        is       => 'ro',
        init_arg => undef,
        lazy     => 1, # must be built after height, width, depth
        default  => sub {
            my $self = shift;
            return $self->height * $self->width * $self->depth;
        }
    );

    sub new_cube {
        my ( $class, $length ) = @_;
        return $class->new( height => $length, width => $length, depth => $length );
    };
}

Methods

Inheritance

Roles

Types