Mythoranium
Mythoranium

Reputation: 195

in perl, is it bad practice to call multiple subroutines with default arguments?

I am learning perl and understand that it is a common and accepted practice to unpack subroutine arguments using shift. I also understand that it is common and acceptable practice to omit function arguments to use the default @_ array.

Considering these two things, if you call a subroutine without arguments, the @_ can (and will, if using shift) be changed. Does this mean that calling another subroutine with default arguments, or, in fact, using the @_ array after this, is considered bad practice? Consider this example:

sub total { # calculate sum of all arguments
    my $running_sum;
    # take arguments one by one and sum them together
    while (@_) {
       $running_sum += shift;
    }
    $running_sum;
}

sub avg { calculate the mean of given arguments
    if (@_ == 0) { return }
    my $sum = &total; # gets the correct answer, but changes @_
    $sum / @_ # causes division by zero, since @_ is now empty
}

My gut feeling tells me that using shift to unpack arguments would actually be bad practice, unless your subroutine is actually supposed to change the passed arguments, but I have read in multiple places, including Stack Overflow, that this is not a bad practice.

So the question is: if using shift is common practice, should I always assume the passed argument list could get changed, as a side-effect of the subroutine (like the &total subroutine in the quoted example)? Is there maybe a way to pass arguments by value, so I can be sure that the argument list does not get changed, so I could use it again (like in the &avg subroutine in the quoted text)?

Upvotes: 17

Views: 2059

Answers (7)

ysth
ysth

Reputation: 98398

You should avoid using the &func; style of calling unless you have a really good reason, and trust that others do the same.

To guard your @_ against modification by a callee, just do &func() or func.

Upvotes: 3

amon
amon

Reputation: 57640

In general, shifting from the arguments is ok—using the & sigil to call functions isn't. (Except in some very specific situations you'll probably never encounter.)

Your code could be re-written, so that total doesn't shift from @_. Using a for-loop may even be more efficient.

sub total {
  my $total = 0;
   $total += $_ for @_;
  $total;
}

Or you could use the sum function from List::Util:

use List::Util qw(sum);

sub avg { @_ ? sum(@_) / @_ : 0 }

Using shift isn't that common, except for extracting $self in object oriented Perl. But as you always call your functions like foo( ... ), it doesn't matter if foo shifts or doesn't shift the argument array.
(The only thing worth noting about a function is whether it assigns to elements in @_, as these are aliases for the variables you gave as arguments. Assigning to elements in @_ is usually bad.)

Even if you can't change the implementation of total, calling the sub with an explicit argument list is safe, as the argument list is a copy of the array:

(a) &total — calls total with the identical @_, and overrides prototypes.
(b) total(@_) — calls total with a copy of @_.
(c) &total(@_) — calls total with a copy of @_, and overrides prototypes.

Form (b) is standard. Form (c) shouldn't be seen, except in very few cases for subs inside the same package where the sub has a prototype (and don't use prototypes), and they have to be overridden for some obscure reason. A testament to poor design. Form (a) is only sensible for tail calls (@_ = (...); goto &foo) or other forms of optimization (and premature optimization is the root of all evil).

Upvotes: 20

Joel Berger
Joel Berger

Reputation: 20280

Here are some examples where the careful use of @_ matters.

1. Hash-y Arguments

Sometimes you want to write a function which can take a list of key-value pairs, but one is the most common use and you want that to be available without needing a key. For example

sub get_temp {
  my $location = @_ % 2 ? shift : undef;
  my %options = @_;
  $location ||= $options{location};
  ...
}

So now if you call the function with an odd number of arguments, the first is location. This allows get_temp('Chicago') or get_temp('New York', unit => 'C') or even get_temp( unit => 'K', location => 'Nome, Ak'). This may be a more convenient API for your users. By shifting the odd argument, now @_ is an even list and may be assigned to a hash.

2. Dispatching

Lets say we have a class that we want to be able to dispatch methods by name (possibly AUTOLOAD could be useful, we will hand roll). Perhaps this is a command line script where arguments are methods. In this case we define two dispatch methods one "clean" and one "dirty". If we call with the -c flag we get the clean one. These methods find the method by name and call it. The difference is how. The dirty one leaves itself in the stack trace, the clean one has to be more cleaver, but dispatches without being in the stack trace. We make a death method which gives us that trace.

#!/usr/bin/env perl

use strict;
use warnings;

package Unusual;

use Carp;

sub new { 
  my $class = shift;
  return bless { @_ }, $class;
}

sub dispatch_dirty { 
  my $self = shift;
  my $name = shift;
  my $method = $self->can($name) or confess "No method named $name";
  $self->$method(@_);
}

sub dispatch_clean { 
  my $self = shift;
  my $name = shift;
  my $method = $self->can($name) or confess "No method named $name";
  unshift @_, $self;
  goto $method;
}

sub death { 
  my ($self, $message) = @_;
  $message ||= 'died';
  confess "$self->{name}: $message";
}

package main;

use Getopt::Long;
GetOptions 
  'clean'  => \my $clean,
  'name=s' => \(my $name = 'Robot');

my $obj = Unusual->new(name => $name);
if ($clean) {
  $obj->dispatch_clean(@ARGV);
} else {
  $obj->dispatch_dirty(@ARGV);
}

So now if we call ./test.pl to invoke the death method

$ ./test.pl death Goodbye
Robot: Goodbye at ./test.pl line 32
    Unusual::death('Unusual=HASH(0xa0f7188)', 'Goodbye') called at ./test.pl line 19
    Unusual::dispatch_dirty('Unusual=HASH(0xa0f7188)', 'death', 'Goodbye') called at ./test.pl line 46

but wee see dispatch_dirty in the trace. If instead we call ./test.pl -c we now use the clean dispatcher and get

$ ./test.pl -c death Adios
Robot: Adios at ./test.pl line 33
    Unusual::death('Unusual=HASH(0x9427188)', 'Adios') called at ./test.pl line 44

The key here is the goto (not the evil goto) which takes the subroutine reference and immediately switches the execution to that reference, using the current @_. This is why I have to unshift @_, $self so that the invocant is ready for the new method.

Upvotes: 2

edgar.holleis
edgar.holleis

Reputation: 5001

I found the following gem in http://perldoc.perl.org/perlsub.html:

"Yes, there are still unresolved issues having to do with visibility of @_ . I'm ignoring that question for the moment. (But note that if we make @_ lexically scoped, those anonymous subroutines can act like closures... (Gee, is this sounding a little Lispish? (Never mind.)))"

You might have run into one of those issues :-(

OTOH amon is probably right -> +1

Upvotes: 0

Krishnachandra Sharma
Krishnachandra Sharma

Reputation: 1342

I tried a simple example:

#!/usr/bin/perl

use strict;

sub total {

    my $sum = 0;
    while(@_) {
        $sum = $sum + shift;
    }
    return $sum;
 }

sub total1 {

my ($a, $aa, $aaa) = @_;

return ($a + $aa + $aaa);
}

my $s;
$s = total(10, 20, 30);
print $s;
$s = total1(10, 20, 30);
print "\n$s";

Both print statements gave answer as 60.

But personally I feel, the arguments should be accepted in this manner:

 my (arguments, @garb) = @_;

in order to avoid any sort of issue latter.

Upvotes: 0

gaussblurinc
gaussblurinc

Reputation: 3692

Refs:

sub refWay{
    my ($refToArray,$secondParam,$thirdParam) = @_;
    #work here
}

refWay(\@array, 'a','b');

HashWay:

sub hashWay{
   my $refToHash = shift; #(if pass ref to hash)
   #and i know, that:
   return undef unless exists $refToHash->{'user'};
   return undef unless exists $refToHash->{'password'};   

   #or the same in loop:
   for (qw(user password etc)){
       return undef unless exists $refToHash->{$_};
   }
}

hashWay({'user'=>YourName, 'password'=>YourPassword});

Upvotes: 0

Matt Melton
Matt Melton

Reputation: 2643

Perl is a little too lax sometimes and having multiple ways of accessing input parameters can make smelly and inconsistent code. For want of a better answer, try to impose your own standard.

Here's a few ways I've used and seen

Shifty

sub login
{
    my $user = shift;
    my $passphrase = shift;
    # Validate authentication    
    return 0;
}

Expanding @_

sub login
{
    my ($user, $passphrase) = @_;
     # Validate authentication   
    return 0;
}

Explicit indexing

sub login 
{
    my user = $_[0];
    my user = $_[1];
    # Validate authentication
    return 0;
}

Enforce parameters with function prototypes (this is not popular however)

sub login($$)
{
    my ($user, $passphrase) = @_;   
    # Validate authentication
    return 0;
}

Sadly you still have to perform your own convoluted input validation/taint checking, ie:

return unless defined $user;
return unless defined $passphrase;

or better still, a little more informative

unless (defined($user) && defined($passphrase)) {
    carp "Input error: user or passphrase not defined";
    return -1;
}

Perldoc perlsub should really be your first port of call.

Hope this helps!

Upvotes: 2

Related Questions