Passing code reference to external module - perl

I use an external module (say Foo.pm) that I don't have control over. The way to use it is like below, which works fine:
use Foo ();
my %config = (
MODE => 'NORMAL',
ERROR => \&my_error, # error handling routine
);
Foo::init(%config);
sub my_error {
my ($message) = #_;
...
}
However I'm having trouble to pass in my_error() to the external module when I'm writing in OO style as the first parameter to my_error() is now $self:
package MyPackage;
use Foo ();
sub new {
my $self = bless {
environment => 'TEST',
config => {
MODE => 'NORMAL',
ERROR => \&my_error, # WRONG ??!
},
}, __PACKAGE__;
Foo::init( %{$self->{config}} );
}
sub my_error {
my ($self, $message) = #_;
...
}
How do I solve it? Passing &{ $self->my_error } does not seem to work.
Thanks!

If you need a sub when you don't have one, you need to make one. You can make an anonymous one.
sub { $self->my_error(#_) }
So that means
my $self = bless {
environment => 'TEST',
config => {
MODE => 'NORMAL',
ERROR => sub { $self->my_error(#_) },
},
}, $class;
But there are complications. In your code, $self hasn't been declared yet when you try to capture it. Fix:
my $self = bless({}, $class);
%$self = (
environment => 'TEST',
config => {
MODE => 'NORMAL',
ERROR => sub { $self->my_error(#_) },
},
);
But that creates a memory leak. The sub captures $self, which references a hash that contains a reference to the sub. Fix:
use Scalar::Util qw( weaken );
my $self = bless({}, $class);
{
weaken( my $self = $self );
%$self = (
environment => 'TEST',
config => {
MODE => 'NORMAL',
ERROR => sub { $self->my_error(#_) },
},
);
}
As simbabque points out, the curry::weak module can simplify(?) this a little.
use curry::weak qw( );
my $self = bless({}, $class);
%$self = (
environment => 'TEST',
config => {
MODE => 'NORMAL',
ERROR => $self->curry::weak::my_error(),
},
);
But I think it'll just add confusion.

A good alternative to the final part of ikegami's excellent and detailed answer is to use curry::weak.
use curry::weak;
my $self = bless({}, $class);
%$self = (
environment => 'TEST',
config => {
MODE => 'NORMAL',
ERROR => $self->curry::weak::my_error(),
},
);
mst, the author of curry, gives a reasonably understandble explanation of how that works in this lightning talk.

Related

Better way to get all attributes from a Moose class as a hash

I want to get all attributes back from a class as a hash.
Is there any better way to do it than this ?
Ideally(?) I would like to be able to say something like:
my $hash = \%{ Diag->new( {range =>1, code => 'AB'} ) };
But will settle for:
my $d = Diag->new( {range =>1, code => 'AB'} );
my $hash = $d->hash;
package Diag;
use Moose;
my #attrs = qw/range code severity source message/;
has 'range' => ( is => 'rw', isa => 'Int' );
has 'code' => ( is => 'rw', isa => 'String' );
has 'severity' => ( is => 'rw', isa => 'Int' );
has 'source' => ( is => 'rw', isa => 'String' );
has 'message' => ( is => 'rw', isa => 'String' );
sub hash {
my $self = shift;
my $hash = {};
for (#attrs) {
$hash->{$_} = $self->$_;
}
return $hash;
}
no Moose;
1;
EDIT Hash with string output for pack/unpack:
# Combining this attribute and the record_format would be great.
# if $self->record->format worked that would be cool.
has 'record' => (
is => 'ro',
isa => 'HashRef',
default => sub {
{
foo => 'A5',
foo2 => 'A16',
}
);
sub record_format
{
my $self = shift;
my #fields = qw( foo foo2 );
return _build_format_string($self->record, \#fields);
}
sub _build_format_string {
return join '', map { $_[1]->{$_} } #{ $_[2] };
}
EDIT2
I found that if I created an Attribute Trait I could make this a little nicer. This way the hash order is with the attribute and only one format method is needed.
package Order;
use Moose::Role;
has order => (
is => 'ro',
isa => 'ArrayRef',
predicate => 'has_order',
);
Moose::Util::meta_attribute_alias('Order');
1;
package Record;
use Moose;
has 'record' => (
traits => [qw/Order/],
is => 'ro',
isa => 'HashRef',
default => sub {
{
foo => 'A5',
foo2 => 'A16',
},
;
},
order => [qw(foo foo2)]
);
sub format {
my ( $self, $attr ) = #_;
my $fields = $self->meta->get_attribute($attr)->order();
return join '', map { $self->{$attr}{$_} } #$fields;
}
1;
my $r = Record->new();
print $r->format("record");
Outputs: A5A16
I would much rather pack that into a method, but your "ideal" case is almost there
my $data = { %{ Diag->new( {range =>1, code => 'AB'} ) } };
The %{...} returns a (key,value,...) list so you want {} to make a hashref out of it, not \ (which curiously turns it back into an object).
But really, that should be tucked away in a method
my $data = Diag->new(...)->get_data;
package Diag;
...
sub get_data { return { %{$_[0]} } };
...
1;
For purely presentational purposes – to print them out – consider using a module, so you don't have to worry about (or know) which attributes have what reference as a value. I use Data::Dump for conciseness of its output
my $obj = Diag->new(...);
say $obj->stringify(); # whole object serialized
say for $obj->stringify('attr1', 'attr1', ...); # serialized values for each
package Diag;
...
use Data::Dump qw(pp);
...
sub stringify {
my $self = shift;
return map { pp $self->{$_} } #_ if #_;
return { pp %$self } }
}
If native OO is used and not Moo/Moose also overload "" for say $obj; use
use overload q("") => sub { return shift->stringify() }
In Moo and Moose the stringification of object under "" (implied in prints as well) is provided.
By further clarifications the code below doesn't address the actual problem. I'll edit but I am leaving this for now as it was deemed generally useful.
It has come up in comments and question edit that a part of the intent is to be able to retrieve values for attribute(s) as well, and packed. The added code does that, but as there is explicit dereferencing a check with ref should be added so to correctly retrieve all values, from either of arrayref, hashref, or string/number. For example
sub record_format {
my ($self, #attrs) = #_;
#attrs = qw(attr1 attr2 ...) if not #attrs; # default list
my $packed;
foreach my $attr (#attrs) {
my $val = $self->{$attr};
my $rv = ref $val;
if (not $rv) { $packed .= $val }
elsif ($rv eq 'HASH') { $packed .= join '', values %$val }
elsif ($rv eq 'ARRAY') { $packed .= join '', #$val }
}
return $packed;
}
This packs values of the passed attributes or of the listed defaults.
The desired $self->record->format can't work nicely since $self->record doesn't return an object so you can't string another method call. You can write an accessor but if you made it return an object under any circumstances that would likely be a surprising behavior, thus not good design.

How to return the correct object attribute in perl?

I have a package I created that is meant to return the object's specified attribute (shortened code)
package vendor_info;
my $vars;
sub new{
my $class = shift;
$vars = {
_servers => shift,
_locations => shift,
_favorite => shift,
_default_selection => shift,
_database => shift,
_DB => shift};
bless $vars, $class;
return $vars;
}
sub get_locations{
return $vars->{_locations};
}
sub get_database{
return $vars->{_database};
}
sub get_DB{
return $vars->{_DB};
}
My perl file receives an input parsed from the terminal but in this case, the variable $vendor is hard coded for testing. I have a list of objects in a hash, and I want to return the correct attribute according to the object. Some of the variables have been removed with placeholders.
$vendor = "atrena";
my %vendor_hash = (
"atrena" => new vendor_info("Variable_server","Variable_location","Advanced_CDC|CDC dftso|DFT|gui|GUI|adv_checker|Lint|spl-view-only|view-only-GUI","adv_checker","database","DB"),
"ansys" => new vendor_info("Variable","Location","agppi|agppi|ane3fl|ane3fl|ansys|ansys|ensemble_gui|ensemble_gui|hfss_desktop|hfss_desktop|hfss_gui|hfss_gui|hfss_solve|hfss_solve|hfsshpc_pack|hfsshpc_pack|optimetrics|optimetrics|q3d_desktop|q3d_desktop|rdacis|rdacis|struct|struct","ane3fl","database", "db"),
"coventor" => new vendor_info("var","location","COV_ZsplatViewer|Viewer|COV_VoxelModeler|Voxel-Modeler|MEMSp_Import_Package|Import-Package|MEMSp_Innovator_Plugin|Innovator-Plugin|MEMSp_MATLAB_Simulation|MATLAB-Simulation|MEMSp_Platform|Platform|MTI_AutoBuilder|Auto-Builder|MTI_Catapult|Catapult|MTI_CoventorWare|Coventor-Ware|MTI_Memcap|Memcap|MTI_PreProcessor|PreProcessor","database","db","db")
);
$vendor_object = $vendor_hash{$vendor};
print Dumper( $vendor_object);
$foodb = $vendor_object -> get_database();
The dumper is printing the correct information, however, when I call get_database(), the database called is always the attribute from the last object in the list, which in this case is coventor. The same could be said for any of the sub routine getters.
How do I call the correct attribute for the correct object?
You have $vars as a lexical variable which is scoped to the file which contains your package. So there is only one instance of this variable and it will always contain the data for the last object that was set up.
I'm not sure where you picked up that approach, but it's not how Perl objects work at all. $vars should be scoped to only exist within your constructor and your accessors should be using the object that is passed to them as their first argument (traditionally called $self).
# Only pragmas should start with lower-case letters
package VendorInfo;
sub new{
my $class = shift;
my $vars = {
_servers => shift,
_locations => shift,
_favorite => shift,
_default_selection => shift,
_database => shift,
_DB => shift
};
return bless $vars, $class;
}
# Just one example accessor...
sub get_database{
my $self = shift;
return $self->{_database};
}
One more point, please use Class->new() instead of the potentially problematic new Class syntax that you are using in your code.
Dave Cross already answered your immediate question.
This is an example of a more idiomatic version of your code:
{
package VendorInfo;
use Moo;
for my $attr (qw(
servers
locations
favorite
default_selection
database
DB
)) {
has $attr => (
is => 'ro',
required => 1,
);
}
}
# main program
use strict;
use warnings;
use Data::Dumper;
my %vendor_hash = (
"atrena" => VendorInfo->new(
servers => "Variable_server",
locations => "Variable_location",
favorite => "Advanced_CDC|CDC dftso|DFT|gui|GUI|adv_checker|Lint|spl-view-only|view-only-GUI",
default_selection => "adv_checker",
database => "database",
DB => "DB",
),
"ansys" => VendorInfo->new(
servers => "Variable",
locations => "Location",
favorite => "agppi|agppi|ane3fl|ane3fl|ansys|ansys|ensemble_gui|ensemble_gui|hfss_desktop|hfss_desktop|hfss_gui|hfss_gui|hfss_solve|hfss_solve|hfsshpc_pack|hfsshpc_pack|optimetrics|optimetrics|q3d_desktop|q3d_desktop|rdacis|rdacis|struct|struct",
default_selection => "ane3fl",
database => "database",
DB => "db",
),
"coventor" => VendorInfo->new(
servers => "var",
locations => "location",
favorite => "COV_ZsplatViewer|Viewer|COV_VoxelModeler|Voxel-Modeler|MEMSp_Import_Package|Import-Package|MEMSp_Innovator_Plugin|Innovator-Plugin|MEMSp_MATLAB_Simulation|MATLAB-Simulation|MEMSp_Platform|Platform|MTI_AutoBuilder|Auto-Builder|MTI_Catapult|Catapult|MTI_CoventorWare|Coventor-Ware|MTI_Memcap|Memcap|MTI_PreProcessor|PreProcessor",
default_selection => "database",
database => "db",
DB => "db",
),
);
my $vendor = "atrena";
my $vendor_object = $vendor_hash{$vendor};
print Dumper($vendor_object);
print "The database is: ", $vendor_object->database, "\n";
Things of note:
I renamed vendor_info to VendorInfo. Lowercase module names are (informally) reserved for pragmata.
I used Moo as a helper module for writing classes.
Moo provides a has helper function for declaring attributes. It also generates a constructor for me, so I don't have to write any boilerplate myself.
Moo automatically enables warnings/strict, so I don't have to do that either.
Indirect object syntax (method $object or method class, in your case new vendor_info) is a bad idea because of its syntactic ambiguity. class->method (here: VendorInfo->new) is much better.
The constructor created by Moo takes its arguments in the form of key-value pairs, not a long list (which is a good idea anyway if your sub takes more than 3 arguments).
Every attribute I declared gets a (read-only (because I used 'ro')) accessor, so client code can simply use $object->database.
In addition to other people's points, you will need to add a true statement at the end of your .pm file
This is how I would write your application
VendorInfo.pm
package VendorInfo;
use strict;
use warnings 'all';
sub new {
my $class = shift;
my $self;
#{$self}{qw/
_servers
_locations
_favorite
_default_selection
_database _DB
/} = #_;
return bless $self, $class;
}
sub get_locations {
my $self = shift;
return $self->{_locations};
}
sub get_database {
my $self = shift;
return $self->{_database};
}
sub get_DB {
my $self = shift;
return $self->{_DB};
}
1;
main.pl
use strict;
use warnings 'all';
use VendorInfo;
use Data::Dumper;
my $vendor = 'atrena';
my %vendor_hash = (
atrena => VendorInfo->new(
'Variable_server',
'Variable_location',
'Advanced_CDC|CDC dftso|DFT|gui|GUI|adv_checker|Lint|spl-view-only|view-only-GUI',
'adv_checker',
'database',
'DB',
),
ansys => VendorInfo->new(
'Variable',
'Location',
'agppi|agppi|ane3fl|ane3fl|ansys|ansys|ensemble_gui|ensemble_gui|hfss_desktop|hfss_desktop|hfss_gui|hfss_gui|hfss_solve|hfss_solve|hfsshpc_pack|hfsshpc_pack|optimetrics|optimetrics|q3d_desktop|q3d_desktop|rdacis|rdacis|struct|struct',
'ane3fl',
'database',
'db',
),
coventor => VendorInfo->new(
'var',
'location',
'COV_ZsplatViewer|Viewer|COV_VoxelModeler|Voxel-Modeler|MEMSp_Import_Package|Import-Package|MEMSp_Innovator_Plugin|Innovator-Plugin|MEMSp_MATLAB_Simulation|MATLAB-Simulation|MEMSp_Platform|Platform|MTI_AutoBuilder|Auto-Builder|MTI_Catapult|Catapult|MTI_CoventorWare|Coventor-Ware|MTI_Memcap|Memcap|MTI_PreProcessor|PreProcessor',
'database',
'db',
'db',
),
);
my $vendor_object = $vendor_hash{$vendor};
print Dumper $vendor_object;
my $foodb = $vendor_object->get_database;
print $foodb, "\n";
output
$VAR1 = bless( {
'_servers' => 'Variable_server',
'_default_selection' => 'adv_checker',
'_locations' => 'Variable_location',
'_database' => 'database',
'_DB' => 'DB',
'_favorite' => 'Advanced_CDC|CDC dftso|DFT|gui|GUI|adv_checker|Lint|spl-view-only|view-only-GUI'
}, 'VendorInfo' );
database

Params::Validate, how to require one of two parameters?

If I have a method that takes either one or the other of two named parameters, exactly one of which must be present, is there a way to handle that with Params::Validate?
$store->put( content_ref => $stringref );
or
$store->put( path => $path_to_file );
I'm not seeing it in the docs, but it seems like an obvious use case, so I thought I should ask.
You can use callbacks to achieve something along those lines:
#!/usr/bin/env perl
use strict;
use warnings;
package My::Class;
use Params::Validate;
use YAML;
sub new { bless {} => shift }
sub _xor_param {
my $param = shift;
return sub { defined($_[0]) and not defined($_[1]->{$param}) }
}
my %validation_spec = (
content_ref => {
'default' => undef,
callbacks => {
"Provided only if no 'path' is given"
=> _xor_param('path')
},
},
path => {
'default' => undef,
callbacks => {
"Provided only if no 'content_ref' is given"
=> _xor_param('content_ref')
},
},
);
sub put {
my $self = shift;
validate(#_, \%validation_spec);
print Dump \#_;
}
package main;
my $x = My::Class->new;
$x->put(path => 'some path');
$x->put(content_ref => \'some content');
$x->put(path => 'another_path', content_ref => \'some other content');
Output:
---
- path
- some path
---
- content_ref
- !!perl/ref
=: some content
The 'content_ref' parameter ("SCALAR(0xab83cc)") to My::Class::put did not pass
the 'Provided only if no 'path' is given' callback
at C:\temp\v.pl line 37
My::Class::put(undef, 'path', 'another_path', 'content_ref',
'SCALAR(0xab83cc)') called at C:\temp\v.pl line 47

default/replacable values in Bread::Board

I found myself instantiating the same objects in numerous tests, so I'm trying to replace this constant setup by using Bread::Board. Most of the time I will want the dependencies to have the same configuration. But occasionally I may want to have an instance created with parameters that are not the default. I'd also like to be able to change this after I've created an instance of the container. e.g. I'd normally want
my $c = Test::Container->new;
my $bar = $c->resolve( service => 'bar' ); # where bar depends on foo
but sometimes what I really need is something like
my $bar = $c->resolve( service => 'bar', {
services => {
foo => { init => 'arg' }
}
}
so that service foo is initialized differently while creating the instance of bar.
This was provided to me by Jesse Luehrs (Doy) on #moose and appears that it'll do what I want.
#!/usr/bin/env perl
use v5.14;
use strict;
use warnings;
package Container {
use Moose;
use Bread::Board;
extends 'Bread::Board::Container';
has '+name' => (default => 'Container');
sub BUILD {
my $self = shift;
container $self => as {
service foo => (
block => sub {
my $s = shift;
$s->param('foo_val');
},
parameters => {
foo_val => { isa => 'Str' },
},
);
service bar => (
block => sub {
my $s = shift;
$s->param('foo')->inflate(foo_val => $s->param('foo_val')) . 'BAR';
},
dependencies => ['foo'],
parameters => {
foo_val => { isa => 'Str', default => 'FOO' },
},
);
};
}
}
my $c = Container->new;
warn $c->resolve(service => 'bar');
warn $c->resolve(service => 'bar', parameters => { foo_val => 'baz' });

How can I construct a moose object from a hash generated from one of the attributes?

I have a couple of packages:
package FOO;
use Moose;
has 'obj' => (is=>'ro');
sub hash {
my $self = shift;
return $self->make_hash($self->obj};
}
and another package extending FOO:
package FOOBAR;
use Moose;
extends 'FOO';
has [qw/val1 val2/] => (is => 'rw');
sub BUILD {
my ($self) = #_;
$self->val1($self->hash->{val1});
$self->val2($self->hash->{val2});
}
Basically I want to do FOOBAR->new(obj=>$obj); and use a hash generated from $obj to populate the attributes specified in FOOBAR (~20 or so attributes)
Is using 'BUILD' like this a good way of solving it?
Why? Then you end up with two copy of the data. Delegate instead.
has obj => (
is => 'ro',
handles => {
val1 => sub { my $self = shift; my $obj = $self->obj; ... },
val2 => sub { my $self = shift; my $obj = $self->obj; ... },
},
);
If the accessors are practically identical, you can do something like
sub make_obj_accessor {
my ($name) = #_;
return sub {
my $self = shift;
my $obj = $self->obj;
... $name ...
};
}
has obj => (
is => 'ro',
handles => {
(map make_obj_accessor($_), qw(
val1
val2
))
},
);
Of course, if you really only have a hash, all you need is
FOOBAR->new( %hash )