Problems using shift function on @_ array in Perl

Viewed 227

It was hoped to use the shift function on the @_ array inside a function so that the function could accept a variable number of parameters. According to p112 in "Programming Perl" by L Wall et al, the arguments passed to a subroutine are concatenated into one flat array, namely @_. It was assumed that this array could be traversed using the shift function or a foreach loop to determine say the minimum of all values passed, even if some of the arguments were themselves arrays and the values were contained as elements in those arrays.

A number of variants were tried and the min2 and min4 variants that used the "for each" loop (which is closest to that on p113 of "Programming Perl") to traverse the @_ array worked. The min1 and min3 variants which used the "shift" function to traverse the @_ array did work properly. Any ideas on what is causing the variants based on the "shift" function to fail?

The four "min" subroutines are given below:

# Using perl5.14.2.exe 

use strict;

sub min {
    my $m=shift @_;
    my $b;
    while ($b = shift @_){
      if ($b < $m) {$m=$b};
    };
    return $m;
};


sub min_v2 {
    my $m = $_[0];
    foreach (@_){
      if ($_ < $m) {$m = $_};
    };
    return $m;
};


sub min_v3 {
    my $m=shift (@_);
    my $b;
    while ($b = shift (@_)){
      if ($b < $m) {$m=$b};
    };
    return $m;
};

sub min_v4 {
#   Similar to Programming Perl 2nd Ed by Larry Wall et al p113
    my $m=shift (@_);
    foreach $b (@_){
      if ($b < $m) {$m=$b};
    };
    return $m;
};

The following code was used to test the routines:

print 'min 10,50,5,1 = ' ,     min (10,50,5,1 ),     "\n" ;
print 'min ((10,50,5),1) = ' , min ((10,50,5),1) ,   "\n" ;
print 'min ((10,50,0),1) = ' , min ((10,50,0),1) ,   "\n" ;

print 'min (30,0) = ',         min (30,0),           "\n";
print 'min_v2 (30,0) = ',      min_v2 (30,0),        "\n";
print 'min_v3 (30,0) = ',      min_v3 (30,0),        "\n";
print 'min_v4 (30,0) = ',      min_v4 (30,0),        "\n";

my @a;
@a = (1,2,3,30);
print '@a = ' , "[ @a ]" , , "\n";
print 'min (@a,0) = '    , min (@a,0) , "\n";
print 'min_v2 (@a,0) = ' , min_v2 (@a,0) , "\n" ;
print 'min_v3 (@a,0) = ' , min_v3 (@a,0) , "\n" ;
print 'min_v4 (@a,0) = ' , min_v4 (@a,0) , "\n" ;

@a = (3,2,1,30);
print '@a = ' , "[ @a ]" , , "\n";
print 'min (@a,0) = '    , min (@a,0) , "\n";
print 'min_v2 (@a,0) = ' , min_v2 (@a,0) , "\n" ;
print 'min_v3 (@a,0) = ' , min_v3 (@a,0) , "\n" ;
print 'min_v4 (@a,0) = ' , min_v4 (@a,0) , "\n" ;

@a = (3,2,1,30);
print '@a = ' , "[ @a ]" , , "\n";
print 'min (@a,2) = '    , min (@a,2) , "\n";
print 'min_v2 (@a,2) = ' , min_v2 (@a,2) , "\n" ;
print 'min_v3 (@a,2) = ' , min_v3 (@a,2) , "\n" ;
print 'min_v4 (@a,2) = ' , min_v4 (@a,2) , "\n" ;

And the following output was generated:

min 10,50,5,1 = 1

min ((10,50,5),1) = 1

min ((10,50,0),1) = 10

min (30,0) = 30

min_v2 (30,0) = 0

min_v3 (30,0) = 30

min_v4 (30,0) = 0

@a = [ 1 2 3 30 ]

min (@a,0) = 1

min_v2 (@a,0) = 0

min_v3 (@a,0) = 1

min_v4 (@a,0) = 0

@a = [ 3 2 1 30 ]

min (@a,0) = 1

min_v2 (@a,0) = 0

min_v3 (@a,0) = 1

min_v4 (@a,0) = 0

@a = [ 3 2 1 30 ]

min (@a,2) = 1

min_v2 (@a,2) = 1

min_v3 (@a,2) = 1

min_v4 (@a,2) = 1
2 Answers

The problem has nothing to do with shift or @_. The problem is that when you shift the value 0 from your list, the while condition evaluates to false, and the loop terminates before you expect it to. Consider this simpler example:

use warnings;
use strict;

my @nums = (50, 30, 0, 1);
my $m = 10;
my $b;
while ($b = shift @nums) {
    print "b=$b m=$m\n";
    if ($b < $m) {$m=$b}
}
print "m=$m\n";

Outputs:

b=50 m=10
b=30 m=10
m=10

The 1st 2 values in the array (50, 30) behave as expected. As soon as you shift the 0 from the array, the loop ends.

The error in the "shift-based" min function (as opposed to "for-each" based) that was identified by @toolic has been corrected using an approach recommended by @ikegami:

sub minb {
    my $m=shift @_;
    my $b;

#   Problem identified by toolic:-    
#   Below while loop exits prematurely if an element of @_ is zero
#   while ($b = shift @_){  

#   Suggestion by ikegami:-
#   Below while loop continues until its length @_ is reduced to zero
    while (@_) {   # i.e length @_ > 0   
      $b = shift @_;

      if ($b < $m) {$m=$b};
    };
    return $m;

Please note that the reason for posting this as an answer rather than a comment was that it did not seem possible to format code properly in the comment block.

Related