Defining a MX::Declare method as a attribute trigger - perl

The following code works as I'd expect. The cached lazy attribute gets cleared and rebuilt when the foo attribute it depends on is changed.
use MooseX::Declare;
use 5.010;
class Test {
has foo => ( isa => 'Str', is => 'rw', trigger => sub {my $self = shift; $self->clearer}, default => '' );
has lazy => ( isa => 'Str', is => 'ro', lazy => 1, clearer => 'clearer',
default => method { say 'building lazy'; return "foo is '".$self->foo."'"; },
);
method say ( ) {
say $self->lazy;
}
}
my $inst = Test->new( foo => 'baz' );
$inst->say;
$inst->say;
say $inst->foo();
$inst->foo('bar');
$inst->say;
output:
building lazy
foo is 'baz'
foo is 'baz'
baz
building lazy
foo is 'bar'
How do I, however, use the MX::Declare sugar for the trigger subroutine? Defining foo as:
has foo => ( isa => 'Str', is => 'rw', trigger => method {$self->clearer}, default => '' );
Results in the class dying on compilation (below). Am I doing something wrong with my anonymous method declaration?
Trigger must be a CODE ref on attribute (foo) at
C:/Strawberry/perl/site/lib/Moose/Meta/Attribute.pm line 423
Moose::Meta::Attribute::_process_trigger_option('Moose::Meta::Attribute',
'foo', 'HASH(0x2a5d14c)') called at C:
/Strawberry/perl/site/lib/Moose/Meta/Attribute.pm line 299
Moose::Meta::Attribute::_process_options('Moose::Meta::Attribute',
'foo', 'HASH(0x2a5d14c)') called at C:/Strawb
erry/perl/site/lib/Moose/Meta/Attribute.pm line 88
Moose::Meta::Attribute::new('Moose::Meta::Attribute', 'foo', 'trigger', 'MooseX::Method::Signatures::Meta::Metho
d=HASH(0x39a421c)', 'isa', 'Str', 'definition_context',
'HASH(0x3452184)', 'default', '', 'is', 'rw') called at C:/Straw
berry/perl/site/lib/Moose/Meta/Attribute.pm line 114
Moose::Meta::Attribute::interpolate_class_and_new('Moose::Meta::Attribute',
'foo', 'trigger', 'MooseX::Method::S
ignatures::Meta::Method=HASH(0x39a421c)', 'isa', 'Str', 'default', '',
'definition_context', 'HASH(0x3452184)', 'is', 'r w') called at
C:/Strawberry/perl/site/lib/Moose/Meta/Class.pm line 704
Moose::Meta::Class::_process_new_attribute('Moose::Meta::Class=HASH(0x38c79d4)',
'foo', 'trigger', 'MooseX::Meth
od::Signatures::Meta::Method=HASH(0x39a421c)', 'isa', 'Str',
'default', '', 'definition_context', 'HASH(0x3452184)', 'is ', 'rw')
called at C:/Strawberry/perl/site/lib/Moose/Meta/Class.pm line 697
Moose::Meta::Class::_process_attribute('Moose::Meta::Class=HASH(0x38c79d4)',
'foo', 'trigger', 'MooseX::Method::
Signatures::Meta::Method=HASH(0x39a421c)', 'isa', 'Str', 'default',
'', 'definition_context', 'HASH(0x3452184)', 'is', ' rw') called at
C:/Strawberry/perl/site/lib/Moose/Meta/Class.pm line 566
Moose::Meta::Class::add_attribute('Moose::Meta::Class=HASH(0x38c79d4)',
'foo', 'trigger', 'MooseX::Method::Signa
tures::Meta::Method=HASH(0x39a421c)', 'isa', 'Str', 'default', '',
'definition_context', 'HASH(0x3452184)', 'is', 'rw') called at
C:/Strawberry/perl/site/lib/Moose.pm line 77
Moose::has('Moose::Meta::Class=HASH(0x38c79d4)', 'foo', 'isa', 'Str', 'is', 'rw', 'trigger', 'MooseX::Method::Si
gnatures::Meta::Method=HASH(0x39a421c)', 'default', '') called at
C:/Strawberry/perl/site/lib/Moose/Exporter.pm line 356
Moose::has('foo', 'isa', 'Str', 'is', 'rw', 'trigger', 'MooseX::Method::Signatures::Meta::Method=HASH(0x39a421c) ',
'default', '') called at mx_declare.pl line 5
main::ANON() called at C:/Strawberry/perl/site/lib/MooseX/Declare/Syntax/MooseSetup.pm line
81
MooseX::Declare::Syntax::MooseSetup::ANON('CODE(0x38c3a94)')
called at mx_declare.pl line 13

The method keyword returns an instance of the MooseX::Method::Signatures::Meta::Method class, which is a subclass of Moose::Meta::Method, which is a subclass of Class::MOP::Method.
Moose allows a method object for default, but not for trigger, which must be a regular coderef.
If you really want to use the method keyword there, you could probably do:
trigger => method { $self->clearer }->body,
But it's probably easier (and saner) to do what #cjm suggests and just use a regular coderef:
trigger => sub { shift->clearer },

You can't. method returns an object, not a plain coderef. However, you can write this even more concisely than method would allow:
has foo => ( isa => 'Str', is => 'rw', trigger => sub {shift->clearer}, default => '' );
That's 3 characters shorter than method {$self->clearer}. And it has less overhead.

Related

Initializing a CodeRef field of a Moose class

I have a Moose class Person
package Person;
use Moose;
has 'first_name' => (
is => 'rw',
isa => 'Str',
);
has 'last_name' => (
is => 'rw',
isa => 'Str',
);
has 'check' => (
is => 'rw',
isa => 'CodeRef',
);
no Moose;
__PACKAGE__->meta->make_immutable;
I am initializing a new Person object in another file like so
use Person;
my $user = Person->new(
first_name => 'Example',
last_name => 'User',
check => sub {
print "yo yo\n";
},
);
print "here\n";
$user->check();
print "here\n";
The two here debug statements are printing but the debug message in the subroutine is not.
I'd like to know the correct way for me to pass a function to the constructor such that I can pass an anonymous sub routine to the object.
$user->check() is equivalent to $user->check. It just returns the value of the check attribute (i.e, the coderef) without doing anything with it -- just like any other accessor would. The fact that this attribute holds a coderef doesn't change that.
If you want to retrieve the coderef, then call it, you need another arrow:
$user->check->()
An alternative is to use the trait Code implemented by Moose::Meta::Attribute::Native::Trait::Code, and then define a handle with a different name.
package Person;
use Moose;
has 'check' => (
is => 'rw',
isa => 'CodeRef',
traits => ['Code'],
handles => {
run_check => 'execute',
},
);
And then call it like this
my $user = Person->new(
first_name => 'Example',
last_name => 'User',
check => sub {
print "yo yo\n";
},
);
print "here\n";
$user->run_check;
print "here\n";
This allows you to separate the accessor for the code-ref from the functionality it fulfills.

Attribute is => 'Maybe[SomeSubtype]' returns Attribute () does not pass type constraint

I've created subtype Birth_d with coercion as shown below, and I'm trying to use it in combination with the built-in Maybe type, per Moose::Manual::Types.
I'm getting the error You cannot coerce an attribute (birth_d) unless its type (Maybe[Birth_d]) has a coercion. Here's complete test code:
package Student;
use Moose;
use Moose::Util::TypeConstraints;
use DateTime::Format::MySQL;
class_type 'Birth_d', { class => 'DateTime' };
coerce 'Birth_d',
from 'Str',
via { DateTime::Format::MySQL->parse_date( $_ ) };
has 'name' => (
isa => 'Str',
is => 'ro',
);
has 'birth_d' => (
isa => 'Maybe[Birth_d]', # This works: isa => 'Birth_d'
coerce => 1,
is => 'ro',
);
package main;
use Test::More;
my $student = Student->new(
name => 'Johnnie Appleseed',
birth_d => '2015-01-01'
);
is ( $student->birth_d->ymd(), '2015-01-01' );
my $student2 = Student->new(
name => 'Foo Bar',
birth_d => undef
);
is( $student2->birth_d, undef );
Replacing isa => 'Maybe[Birth_d]' with isa => 'Birth_d' works, but is not what is needed. I need to make the birth_d optional, and if not supplied, should be undef.
I should add, I tried using MooseX::Types to tuck this Birth_d type away in a separate place, but found its cavalier use of barewords a bit unorthodox, so I slowly backed away. I'm open to reconsidering it, if it makes sense to do so.
Moose does not do any chaining of coercions, in other words you have to tell it explicitly how to convert to a Maybe[Birth_d].
You can do this by reusing the existing coercion to Birth_d:
package Student;
use Moose;
use Moose::Util::TypeConstraints;
use DateTime::Format::MySQL;
# save the Moose::Meta::TypeConstraint object
# you can also get it with find_type_constraint('Birth_d')
my $birth_d = class_type 'Birth_d', { class => 'DateTime' };
coerce 'Birth_d',
from 'Str',
via { DateTime::Format::MySQL->parse_date( $_ ) };
subtype 'MaybeBirth_d',
as 'Maybe[Birth_d]';
coerce 'Maybe[Birth_d]',
from 'Str|Undef',
via { $birth_d->coerce($_) };
has 'name' => (
isa => 'Str',
is => 'ro',
);
has 'birth_d' => (
isa => 'Maybe[Birth_d]',
coerce => 1,
is => 'ro',
predicate => 'has_birth_d', # as per your comment
);
package main;
use Test::More;
my $student = Student->new(
name => 'Johnnie Appleseed',
birth_d => '2015-01-01'
);
is ( $student->birth_d->ymd(), '2015-01-01' );
my $student2 = Student->new(
name => 'Foo Bar',
birth_d => undef
);
is( $student2->birth_d, undef );
ok( $student2->has_birth_d );
done_testing;
I would find it more useful to not have a Maybe[Birth_d] type, but simply declare the attribute with the Birth_d type, and no "required" set.
That way, if a valid String is passed in, it will be accepted, an invalid String will lead to an error, and nothing just does not need to be passed in.
However, you can coerce to a maybe type:
subtype 'MaybeBirth_d',
as maybe_type(class_type('DateTime'));
coerce 'MaybeBirth_d',
from 'Str',
via { DateTime::Format::MySQL->parse_date( $_ ) };
has 'birth_d' => (
isa => 'MaybeBirth_d',
coerce => 1,
is => 'ro',
);
I just do not see the value of being able to pass in undef for a birthdate - how is that better than not setting it?
I would also like to suggest using no Moose::Util::TypeConstraints; and no Moose; at the end of your package, or namespace::autoclean; at the beginning, as well as __PACKAGE__->meta->make_immutable; at the end of your Student class.

using localtime inside moose default values

What's wrong with the code below ? When run, I get: "Use of uninitialized value in concatenation (.) or string at ./main.pl line 14"
#!/usr/bin/perl
package Test;
use Moose;
has 'message' => (isa => 'HashRef', is => 'ro', default => sub{{(localtime)[2] => {(localtime)[3] => "hello"}}});
# the one below works fine
#has 'message' => (isa => 'HashRef', is => 'ro', default => sub{{"18" => {"16" => "hello"}}});
sub show {
my $self = shift;
print("Test: " . $self->message->{(localtime)[2]}->{(localtime)[3]} . "\n");
}
my $o = Test->new();
$o->show();
If I do not use localtime() then it works fine. Also localtime[2] and [3] do not change very often (2 is hours, 3 is month day) so the problem is not that. If I run the script with a debugger, I get:
x $self
0 Test=HASH(0x3597300)
'message' => HASH(0x3597618)
16 => 'hello'
So it looks like I 'lose' one level of indirection, not really sure why... Any idea ?
The outer {} do not parse as a hashref. Add an explicit return:
has 'message' => (isa => 'HashRef', is => 'ro', default => sub{ return {(localtime)[2] => {(localtime)[3] => "hello"}} });
A + to force this works, too.
has 'message' => (isa => 'HashRef', is => 'ro', default => sub{ +{(localtime)[2] => {(localtime)[3] => "hello"}} });

Why does modifying a Moose class in BUILD cause this error?

I'm having trouble with this Moose-related error when using BUILD. When I change to BUILDALL it appears to work. Note the use of Class::MOP::load_class
Using BUILD
Perl version: 5.012002
Class::MOP::Version: 1.11
Moose::Version: 1.24
Applying fixup GV::WebServer::Fixups::Development
aflott-g3 at a.pl line 83.
Error: trying to call refresh() in GV::WebServer::Fixups::Development produced: The 'add_attribute' method cannot be called on an immutable instance at /opt/cidc-perl/perl-5.12.2/lib/perl5/x86_64-linux/Class/MOP/Class/Immutable/Trait.pm line 32
Class::MOP::Class::Immutable::Trait::_immutable_cannot_call('add_attribute') called at /opt/cidc-perl/perl-5.12.2/lib/perl5/x86_64-linux/Class/MOP/Class/Immutable/Trait.pm line 37
Class::MOP::Class:::around('CODE(0x13a2e028)', 'Class::MOP::Class::Immutable::Moose::Meta::Class=HASH(0x13d58...', 'architecture', 'is', 'ro', 'isa', 'Str', 'lazy', 1, ...) called at /opt/cidc-perl/perl-5.12.2/lib/perl5/x86_64-linux/Class/MOP/Method/Wrapped.pm line 159
Class::MOP::Method::Wrapped::__ANON__('Class::MOP::Class::Immutable::Moose::Meta::Class=HASH(0x13d58...', 'architecture', 'is', 'ro', 'isa', 'Str', 'lazy', 1, 'default', ...) called at /opt/cidc-perl/perl-5.12.2/lib/perl5/x86_64-linux/Class/MOP/Method/Wrapped.pm line 89
Class::MOP::Class::Immutable::Moose::Meta::Class::add_attribute('Class::MOP::Class::Immutable::Moose::Meta::Class=HASH(0x13d58...', 'architecture', 'is', 'ro', 'isa', 'Str', 'lazy', 1, 'default', ...) called at a.pl line 47
Amethyst::SystemInfo::BUILD('Amethyst::SystemInfo=HASH(0x13e83010)', 'HASH(0x13e50cc0)') called at generated method (unknown origin) line 147
Amethyst::SystemInfo::new('Amethyst::SystemInfo') called at a.pl line 92
GV::WebServer::Fixups::AutoSet::set() called at a.pl line 84
GV::WebServer::Fixups::Development::refresh('GV::WebServer::Fixups::Development') called at a.pl line 114
main::__ANON__() called at /opt/cidc-perl/perl-5.12.2/lib/perl5/Try/Tiny.pm line 76
eval {...} called at /opt/cidc-perl/perl-5.12.2/lib/perl5/Try/Tiny.pm line 67
Try::Tiny::try('CODE(0x13e82fe0)', 'Try::Tiny::Catch=REF(0x13e8cd50)') called at a.pl line 118
Using BUILDALL:
Perl version: 5.012002
Class::MOP::Version: 1.11
Moose::Version: 1.24
Applying fixup GV::WebServer::Fixups::Development
aflott-g3 at a.pl line 71.
aflott-g3364136 at a.pl line 81.
Full error
From this code:
package Amethyst::SystemInfo;
use v5.10;
use Moose;
use Sys::Hostname qw();
use Sys::HostIP;
use Try::Tiny;
has '_host_ip' => ('is' => 'ro', 'isa' => 'Sys::HostIP', 'default' => sub { Sys::HostIP->new });
has 'eth0_ipv4' => ('is' => 'rw', 'isa' => 'Str',);
has 'ethernet_interfaces' => ('is' => 'rw', 'isa' => 'HashRef',);
has 'hostname' => ('is' => 'ro', 'isa' => 'Str', 'default' => sub { Sys::Hostname::hostname });
sub BUILD {
my ($self) = #_;
$self->ethernet_interfaces($self->_host_ip->interfaces);
if ($self->ethernet_interfaces->{'eth0'}) {
$self->eth0_ipv4($self->ethernet_interfaces->{'eth0'});
}
foreach my $attrib (
qw(architecture domain fqdn kernel kernelrelease kernelversion memorytotal operatingsystem processor processorcount swap)
) {
$self->meta->add_attribute(
$attrib => (
'is' => 'ro',
'isa' => 'Str',
'lazy' => 1,
'default' => sub { return $self->_load_value($attrib) }
)
);
}
$self->meta->make_immutable;
return;
}
sub _load_value {
my ($self, $module_name) = #_;
try {
Class::MOP::load_class("Pfacter::$module_name");
}
catch {
warn("Failed to load Pfacter::$module_name");
};
my $value = "Pfacter::$module_name"->pfact({'pfact' => {'kernel' => 'Linux'}});
unless (defined($value)) {
warn("finding value for $module_name returned undef");
}
chomp($value);
return $value;
}
no Moose;
package GV::WebServer::Fixups::Development;
use v5.10;
sub refresh {
warn Amethyst::SystemInfo->new->hostname;
return GV::WebServer::Fixups::AutoSet::set();
}
package GV::WebServer::Fixups::AutoSet;
use v5.10;
sub set {
my $sysinfo = Amethyst::SystemInfo->new;
warn $sysinfo->hostname, ' ', $sysinfo->swap;
}
package main;
use v5.10;
use Class::MOP;
use Try::Tiny;
my $module_name = "GV::WebServer::Fixups::Development";
say('Perl version: ', $]);
say('Class::MOP::Version: ', $Class::MOP::VERSION);
say('Moose::Version: ', $Moose::VERSION);
say("Applying fixup $module_name");
Class::MOP::load_class($module_name);
my $ret;
try {
$ret = $module_name->refresh;
}
catch {
warn("Error: trying to call refresh() in $module_name produced: " . shift);
};
You are modifying the class every time you construct an object of that class. That makes no sense. Just move your class construction code out of BUILD and BUILDARGS and place it with the rest of the class construction code.
package Amethyst::SystemInfo;
use v5.10;
use Moose;
use Sys::Hostname qw();
use Sys::HostIP;
use Try::Tiny;
has '_host_ip' => ('is' => 'ro', 'isa' => 'Sys::HostIP', 'default' => sub { Sys::HostIP->new });
has 'eth0_ipv4' => ('is' => 'rw', 'isa' => 'Str',);
has 'ethernet_interfaces' => ('is' => 'rw', 'isa' => 'HashRef',);
has 'hostname' => ('is' => 'ro', 'isa' => 'Str', 'default' => sub { Sys::Hostname::hostname });
foreach my $attrib (qw(
architecture domain fqdn kernel kernelrelease kernelversion
memorytotal operatingsystem processor processorcount swap
)) {
has $attrib => (
'is' => 'ro',
'isa' => 'Str',
'lazy' => 1,
'default' => sub { return $_[0]->_load_value($attrib) },
);
}
sub BUILD {
my ($self) = #_;
$self->ethernet_interfaces($self->_host_ip->interfaces);
if ($self->ethernet_interfaces->{'eth0'}) {
$self->eth0_ipv4($self->ethernet_interfaces->{'eth0'});
}
}
sub _load_value {
...
}
no Moose;
__PACKAGE__->meta->make_immutable;
1;
Kudos to phaylon and bvr.

Argument for builder subroutine in a moose object

I'm currently delegating the builder method to all of the objects that extend one of my base classes. The problem that I'm facing is I need all of the objects to either read an attribute of itself or be passed in a value.
# In Role:
has 'const_string' => (
isa => 'Str',
is => 'ro',
default => 'test',
);
has 'attr' => (
isa => 'Str',
is => 'ro',
builder => '_builder',
);
requires '_builder';
# In extending object - desired 1
sub _builder {
my ($self) = shift;
# $self contains $self->const_string
}
# In extending object - desired 2
sub _builder {
my ($arg1, $arg2) = #_;
# $args can be passed somehow?
}
Is this currently possible or am I going to have to do it some other way?
You can't pass arguments to attribute build methods. They are called automatically by Moose internals, and passed only one argument -- the object reference itself. The builder must be able to return its value based on what it sees in $self, or anything else in the environment that it has access to.
What sort of arguments would you be wanting to pass to the builder? Can you instead pass these values to the object constructor and store them in other attributes?
# in object #2:
has other_attr_a => (
is => 'ro', isa => 'Str',
);
has other_attr_b => (
is => 'ro', isa => 'Str',
);
sub _builder
{
my $self = shift;
# calculates something based on other_attr_a and other_attr_b
}
# object #2 is constructed as:
my $obj = Class2->new(other_attr_a => 'value', other_attr_b => 'value');
Also note that if you have attributes that are built based off of other attribute values, you should define them as lazy, otherwise the builders/defaults will run immediately on object construction, and in an undefined order. Setting them lazy will delay their definition until they are first needed.
You can do something like this:
has 'attr' => (
isa => 'Str',
is => 'ro',
builder => '_pre_builder',
);
sub pre_builder {
_builder(#_, 'your_arg');
}