Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Perl, should a function do the wantarray dance, or can we expect the caller to use map?

Tags:

map

perl

On the (much appreciated) perlmonks site, I found the following snippet that trims the spaces from both sides of a string:

sub trim {
  @_ = $_ if not @_ and defined wantarray;
  @_ = @_ if defined wantarray;
  for (@_ ? @_ : $_) { s/^\s+//, s/\s+$// }
  return wantarray ? @_ : $_[0] if defined wantarray;
}

I don't understand why the author goes to all the trouble of checking wantarray almost every line. Why not just trim the string, and have the programmer use map when he is passing an array?

What is the difference between that trim, called like this:

my @test_array = ( 'string1', ' string2', 'string3 ', ' string4 ');
my @result = trim(@test_array);

Or a simple trim, called like this when one needs to trim an array:

my @test_array = ( 'string1', ' string2', 'string3 ', ' string4 ');
my @result = map { trim($_) } @test_array;
like image 579
Konerak Avatar asked Jan 16 '12 08:01

Konerak


4 Answers

First of all it's better if you abstract that map away:

#e.1.
sub trim
{
    my @ar = @_;
    for (@ar) { s/^\s+//, s/\s+$// };
    return wantarray ? @ar : $ar[0];
}

Second, consider the above example and compare it with:

#e.2.
sub trim
{
    for (@_) { s/^\s+//, s/\s+$// };
}

what's the difference?

e.1. returns a new trimmed array, while e.2. modifies the original array.

Ok now, what does the original cryptic subroutine do?

It auto-magically (yeaah, it's Perl) modifies the original array if you are not assigning the return value to anything OR leaves the original array untouched and returns a new trimmed array if you are assigning the return value to another variable.

How?

By checking to see if wantarray is defined at all. as long as the function is on the right hand side and the return value is assigned to a variable "defined wantarray" is true (regardless of scalar/array context).

like image 98
Nylon Smile Avatar answered Nov 17 '22 21:11

Nylon Smile


Breaking this down line by line since it hasn't been:

sub trim {
  @_ = $_ if not @_ and defined wantarray;
     # if there are no arguments, but a return value is requested
     # then place a copy of $_ into @_ to work on

  @_ = @_ if defined wantarray;
     # if the caller expects a return value, copy the values in @_ into itself 
     # (this breaks the aliasing to the caller's variables)

  for (@_ ? @_ : $_) { s/^\s+//, s/\s+$// }
     # perform the substitution, in place, on either @_ or $_ depending on
     # if arguments were passed to the function

  return wantarray ? @_ : $_[0] if defined wantarray;
     # if called in list context, return @_, otherwise $_[0]
}

I agree that the code gets a bit tedious with all of the wantarray checks, but the result is a function that shares a level of flexibility with Perl's builtin functions. The net result of making the function "smart" is to clean up the call site (avoiding looping constructs, temp variables, repetition, ...) which depending on the frequency the function is used can meaningfully improve code readability.

The function could be simplified a little bit:

sub trim {
    @_ = @_ ? @_ : $_ if defined wantarray;

    s/^\s+//, s/\s+$// for @_ ? @_ : $_;

    wantarray ? @_ : shift
}

The first two lines can be rolled into one, since they are doing the same thing (assigning to @_) just with different source values. And there is no need for the outer return ... if defined wantarray check at the end, since returning a value in void context doesn't do anything anyway.

But I would probably change the last line to wantarray ? @_ : pop since that makes it behave like a list (last element in scalar context).

Once all is said and done, this allows the following calling styles to be used:

my @test_array = ( 'string1', ' string2', 'string3 ', ' string4 ');

my @result = trim @test_array;
my $result = trim $test_array[0];
trim @test_array; # in place trim

and even still supports the call site loop:

my @result = map trim, @test_array;

or more verbosely as:

my @result = map trim($_), @test_array;

and it can be used inside a while loop similar to chomp

while (<$file_handle>) {
    trim; 
    # do something
}

Opinions about dwimmery in Perl are mixed. I personally like it when functions give me the flexibility to code the caller in a way that makes sense, rather than working around a function's rigid interface.

like image 26
Eric Strom Avatar answered Nov 17 '22 21:11

Eric Strom


Probably the author wanted to mimic the behavior of the standard chomp function. There is no need to do this in your own function.

man perlfunc 
chomp VARIABLE 
chomp( LIST ) 
chomp 

[...] If you chomp a list, each element is chomped. [...]

like image 5
kubanczyk Avatar answered Nov 17 '22 20:11

kubanczyk


Note that this is exactly how Text::Trim is implemented. See its description on various use-cases. Plays with wantarray allows to distinguish various contexts and implement different semantics for each.

Personally I prefer just single semantics as it is easier to understand and use. I would avoid using $_ default variable or modification in place, in line with Nylon Smile's example 1.

like image 1
bvr Avatar answered Nov 17 '22 21:11

bvr