How to specify default values for optional subroutine arguments? - perl

Is there an elegant way to specify default values for subroutine arguments?
Currently, I am using the following approach:
use strict;
use warnings;
func1( "arg1", "arg2", opt1 => "first option", opt2 => 0 );
sub func1 {
my ( $arg1, $arg2, %opt ) = #_;
$opt{opt1} //= "no option";
$opt{opt2} //= 1;
$opt{opt3} //= [];
}
which looks a little bit ugly, when there are many options. I would rather like to do
sub func2 {
my ( $arg1, $arg2, $opt ) = process_args(
opt1 => "no option", opt2 => 1, opt3 => []
);
}
The best I could come up with for this approach was:
sub func2 {
my ( $arg1, $arg2, $opt ) = process_args(
\#_, 2, opt1 => "no option", opt2 => 1, opt3 => []
);
}
sub process_args {
my ($a, $n, %opt_info ) = #_;
my #b = splice #$a, 0, $n;
my %opt = #$a;
for my $key (keys %opt_info) {
$opt{$key} //= $opt_info{$key};
}
return (#b, \%opt);
}
but now I got the other problem, that I must pass \#_ and the number of non-option arguments ( here 2 ), to process_args..

sub func1 {
my $arg1 = shift;
my $arg2 = shift;
my %opt = (
opt1 => 'default',
opt2 => 'default',
#_
);
Or, you can use Params::Validate.

I don't remember seeing a subroutine written expressly to handle subroutine parameters. Do you have a Ruby background?
You can define the options hash by a list of its defaults, followed by anything passed in #_. Like this
use strict;
use warnings;
func1( "arg1", "arg2", opt1 => "first option", opt2 => 0 );
sub func1 {
my ( $arg1, $arg2 ) = splice #_, 0, 2;
my %opts = (
opt1 => "no option",
opt2 => 1,
opt3 => [],
#_,
);
}
Another mechanism, if you want to warn against unsupported paremeters, is to do much as you have, but use delete, and to ensure that the hash is empty afterwards, like this
use strict;
use warnings;
use Data::Dump;
use Carp 'croak';
func1( "arg1", "arg2", opt9 => 9 );
sub func1 {
my ( $arg1, $arg2, %opt ) = #_;
my $opt1 = delete $opt{opt1} // 'no option';
my $opt2 = delete $opt{opt2} // 1;
my $opt3 = delete $opt{opt3} // [];
croak "Unexpected parameters: ", join ',', keys %opt if keys %opt;
}
output
Unexpected parameters: opt9 at E:\Perl\source\args.pl line 16.
main::func1("arg1", "arg2", "opt9", 9) called at E:\Perl\source\args.pl line 7

Try:
# isolate the default options
{
my %default_options = (
opt1 => 'default for opt1',
opt2 => 'default for opt2',
);
sub func1 {
my $arg1 = shift #_;
my $arg2 = shift #_;
# set options
my %opt = %default_options;
if( my %given_opts = #_ ){
for my $key ( keys %opt ){
if( exists $given_opts{$key} ){
$opt{$key} = $given_opts{$key};
}
}
}
# rest of func1
}
}

Related

How can a perl constructor return a value not just a hashref

I want to create a Perl OO module to return a value like DateTime does, but don't know how to it right now. Anyone's help on this will be appreciated.
Below looks like what I wanted:
use DateTime;
use Data::Printer;
my $time = DateTime->now();
print $time . "\n";
print ref $time;
# p $time;
Output:
2022-11-23T13:22:39
DateTime
What I got:
package Com::Mfg::Address;
use strict;
use warnings;
#constructor
sub new {
my ($class) = #_;
my $self = {
_street => shift || "undefined",
_city => shift || "undefined",
_las_state => shift || "undefined",
_zip => shift || "undefined",
};
bless $self, $class;
return $self;
}
#accessor method for street
sub street {
my ( $self, $street ) = #_;
$self->{_street} = $street if defined($street);
return ( $self->{_street} );
}
#accessor method for city
sub city {
my ( $self, $city ) = #_;
$self->{_city} = $city if defined($city);
return ( $self->{_city} );
}
#accessor method for state
sub state {
my ( $self, $state ) = #_;
$self->{_state} = $state if defined($state);
return ( $self->{_state} );
}
#accessor method for zip
sub zip {
my ( $self, $zip ) = #_;
$self->{_zip} = $zip if defined($zip);
return ( $self->{_zip} );
}
sub print {
my ($self) = #_;
printf( "Address:%s\n%s, %s %s\n\n",
$self->street, $self->city, $self->state, $self->zip );
}
1;
# test.pl
#!/usr/bin/perl -w
use strict;
use Data::Printer;
BEGIN {
use FindBin qw($Bin);
use lib "$Bin/../lib";
}
use Com::Mfg::Address;
my $homeAddr = Com::Mfg::Address->new('#101 Road', 'LA', 'CA', '111111');
print $homeAddr;
# $homeAddr->print();
# p $homeAddr;
But this only gives me:
Com::Mfg::Address=HASH(0xb89ad0)
I am curious if print $homeAddr can give me:
something like #101Road-LA-CA-111111 and it really is object like above print $time . "\n";.
I tried to review DateTime source but still have no clue right now.
You're asking how to provide a custom stringification for the object. Use the following in your module:
use overload '""' => \&to_string;
sub to_string {
my $self = shift;
return
join ", ",
$self->street,
$self->city,
$self->state,
$self->zip;
}
This makes
print $homeAddr;
equivalent to
print $homeAddr->to_string();

Perl: Recursive object instantiation with Moose

In the example code below, I am defining a class Person that can have child objects of the same class.
When I invoke the printTree method, I am expecting the following output
Sam Ram Geeta
What I see instead is
SamRamRamRamRamRamRamRamRamRamRamR.....
Any hints on what I am doing wrong and how to achieve my goal?
package Person;
use Moose;
has name => ( is => 'ro' );
my #kids;
sub addChild {
my ( $self, $name ) = #_;
my $k = Person->new( name => $name );
push #kids, $k;
return $k;
}
sub printTree {
my $self = shift;
print $self->name;
$_->printTree foreach ( #kids );
}
no Moose;
package main;
my $s = Person->new( name => "Sam" );
my $r = $s->addChild( "Ram" );
my $g = $s->addChild( "Geeta" );
$s->printTree;
The issue is that #Person::kids does not belong to any one instance, and you effectively end up with
#Person::kids = ($r, $g);
$s->printTree() loops through #Person::kids, calls
$r->printTree() loops through #Person::kids, calls
$r->printTree() loops through #Person::kids, calls
$r->printTree() loops through #Person::kids, calls
...
You need to make it an attribute, e.g.
has kids => (
isa => 'ArrayRef[Person]',
traits => ['Array'],
handles => {
all_kids => 'elements',
push_kids => 'push',
},
default => sub { [] },
);
sub addChild {
my ($self, $name) = #_;
my $k = Person->new(name => $name);
$self->push_kids($k);
return $k;
}
sub printTree {
my ($self) = #_;
print $self->name;
$_->printTree foreach $self->all_kids;
}
You can check perldoc Moose::Meta::Attribute::Native::Trait::Array for other useful handles from the Array trait.

Perl OOP method returns array I cannot loop

Here is what my module looks like:
#!/usr/bin/perl
package Page;
use strict;
use warnings;
use base qw/DBObj/;
our %fields = (
id => undef,
title => '$',
content => '$'
);
sub get_field_names {
my #names = sort keys \%fields;
return \#names;
}
for my $field ( keys %fields ) {
my $slot = __PACKAGE__ ."::$field";
no strict "refs";
*$field = sub {
my $self = shift;
$self->{$slot} = shift if #_;
return $self->{$slot};
}
}
1;
Here the parent module where the strange behaviour occurs
#!/usr/bin/perl
package DBObj;
use strict;
use warnings;
use Data::Dumper;
use DBConn;
sub new {
my $me = shift;
my $class = ref $me || $me;
my $self = {
dbh => new DBConn->new(
dns => '/db.sql',
user => '',
pass => '',
)
};
return bless $self, $class;
}
sub save {
my $self = shift;
my #field_names = #{$self->get_field_names};
print Dumper #field_names;
foreach my $item ( reverse #field_names ) {
print $item;
}
}
sub fill {
my ( $self, $args ) = #_;
foreach my $key ( keys $args ) {
$self->$key( $args->{$key} );
}
}
1;
here is what I am experiencing. This snippet
my #field_names = $self->get_field_names;
print Dumper #field_names;
foreach my $item ( reverse #field_names ) {
print $item;
}
Data::Dumper shows
$VAR1 = [
'content',
'id',
'title'
];
But the foreach loop returns
ARRAY(0x7fc750a26470)
I have a Test::Simple test case where I perform the following test
ok( shift $page->get_field_names eq 'content', 'Page has field content');
so I can shift off an item from the array, but I cannot loop through it which is a puzzle to me.
And please; before you tell me that I shouldn't be doing this and that there is a ton of modules out there I should pick instead, I want to point out; I am doing this our of pure fun, I have been away from Perl for ~10 years and thought it would be fun to play around with it again.
You have made get_field_names return a reference to an array, but you are then putting that reference into an array variable.
Try:
my $field_names = $self->get_field_names;
print Dumper $field_names;
foreach my $item ( reverse #$field_names ) {
print $item;
}
get_field_names returns an arrayref, not an array. Either change its return type by removing the backslash from return \#names; or "cast" its return type to an array by writing:
my #field_names = #{$self->get_field_names};

Pass hash to subroutine inside a subroutine already passed that hash

I am working with passing hashes to various subroutines, and I was wondering how to pass a hash to a subroutine and then pass the same hash inside that subroutine to a different subroutine and so on.
For example, the following code works fine.
use strict;
use warnings;
my %hash = (
key1 => 'value1',
key2 => 'value2',
key3 => 'value3',
key4 => '',
);
print %hash, "\n";
check_it(\%hash);
sub check_it {
my $params = shift;
foreach(keys %{$params}){
if($params->{$_}) {
print "'$_' defined as '$params->{$_}'\n";
}
else {
print "'$_' not defined as '$params->{$_}'. Deleting it.\n";
#delete $params->{$_};
$params->{$_} = 'null';
}
}
for ( my $i = 0 ; $i < 7 ; $i++ ) {
print "looping\n";
&check_tags_again(\%hash);
}
}
sub check_tags_again {
my $hash_now = shift;
#check again...
foreach(keys %{$hash_now}){
print "An element of hash: ", $hash_now->{$_}, "\n";
#if(defined $hash_now->{$_}){ delete $hash_now->{$_};}
}
&check_tags_thrice(\%hash);
}
sub check_tags_thrice {
my $hash_now = shift;
#check again...
foreach(keys %{$hash_now}){
print "An element of hash: ", $hash_now->{$_}, "\n";
#if(defined $hash_now->{$_}){ delete $hash_now->{$_};}
}
}
print "final hash:", %hash, "\n";
BUT, when I run the code that follows:
use strict;
use warnings;
use Data::Dumper;
sub process_data {
my $group_size = 10;
my %HoA = (
flintstones => [ "fred", "barney" ],
jetsons => [ "george", "jane", "elroy" ],
simpsons => [ "homer", "marge", "bart" ],
);
&delete_stuff( \%HoA, $group_size );
print "New group:\n";
print Dumper( \%HoA );
undef %HoA;
}
sub delete_stuff {
my $HoARef = shift;
my $group_size = shift;
print "group size in sub $group_size\n";
for ( my $j = 0 ; $j < $group_size ; $j++ ) {
my $dlted = &delete_other_stuff( \%HoA, $j );
print "deleted? '$dlted'\n";
if ( $dlted == 0 ) {
&presence_check( \%HoA, $j );
}
for ( my $i = 0 ; $i < $group_size ; $i++ ) {
}
}
}
sub delete_other_stuff {
my $HoAref = shift;
my $Dex = shift;
return $deleted;
}
sub presence_check {
my $HoAreF = shift;
my $DeX = shift;
}
I get:
Global symbol "%HoA" requires explicit package name at x.pl line 32.
Global symbol "%HoA" requires explicit package name at x.pl line 35.
Execution of x.pl aborted due to compilation errors.
I'm confused because I think it's doing the same thing as the first, but now it claims that %HoA was never initialized.
In delete_stuff, you don't have %HoA, you have $HoARef. If all the subs are expecting a reference to a hash, then you can just use it:
for ( my $j = 0 ; $j < $group_size ; $j++ ) {
my $dlted = &delete_other_stuff( $HoARef, $j );
print "deleted? '$dlted'\n";
if ( $dlted == 0 ) {
&presence_check( $HoARef, $j );
}
...
}
By the way, we're closing on 20 years of Perl 5. There is no reason to call a sub with explicitly passed parameters with an &, which is a Perl 4 holdover.

Return multiple variables perl

I have this
sub test
{
my ($arg1, $arg2) = #_; # Argument list
code
return ($variable1, $variable2);
}
So, when i call this by
test('text1','text2');
concatenates the two return values in one. How can i call only one at a time?
my $output_choice_1 = ( test('text1','text2') )[0];
my $output_choice_2 = ( test('text1','text2') )[1];
or both at once:
my ( $output_choice_1, $output_choice_2 ) = test('text1','text2');
Though sometimes it makes for clearer code to return a hashref:
sub test {
...
return { 'choice1' => $variable1, 'choice2' => $variable2 };
}
...
my $output_choice_1 = test('text1','text2')->{'choice1'};
Are you asking how to assign the two values returned by a sub to two different scalars?
my ($var1, $var2) = test('text1', 'text2');
I wasn't really happy with what I found in google so posting my solution here.
Returning an array from a sub.
Especially the syntax with the backslash caused me headaches.
#!/usr/bin/perl
use warnings;
use strict;
use Data::Dumper;
sub returnArrayWithHash {
(my $value, my %testHash) = #_;
return ( $value, \%testHash );
}
my %testHash = ( one => 'foo' , two => 'bar' );
my #result = returnArrayWithHash('someValue', %testHash);
print Dumper(\#result) . "\n";
Returns me
$VAR1 = [
'someValue',
{
'one' => 'foo',
'two' => 'bar'
}
];