-
Notifications
You must be signed in to change notification settings - Fork 19
Porting from Moose or Moo
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.
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
}
}
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."
Moose:
has name => ( is => 'ro' );
Corinna:
field $name :reader :param;
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).
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;
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 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 };
}
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.
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;
}
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.
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;
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 );
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;
}
}
}
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 );
};
}
Corinna—Bringing Modern OO to Perl