CJ7
CJ7

Reputation: 23295

How to have sub use $_ when parameter omitted?

How can I get a perl sub to use $_ when the parameter is omitted, like chr does? Is this the best way?

my @chars = map { chr } @numbers;  # example

my @trimmed_names = map { trim } @names;

sub trim
{
    my $str = shift || $_;
    $str =~ s/^\s+|\s+$//g;
    return $str;
}

Upvotes: 2

Views: 115

Answers (2)

zdim
zdim

Reputation: 66924

The $_ is directly seen in a sub called in its scope, so you can indeed just use it

sub trim { s/^\s+|\s+$//gr }   # NOTE: doesn't change $_

where with /r modifier the changed string is returned and original isn't changed, crucial here.

However, this can be tricky and can (easily) result in subtle and very hard-to-find bugs. Here is one ready example. If we changed the $_ in the sub during processing, like

sub trim {           # WARNING: caller's data changed
    s/^\s+|\s+$//g;
    return $_;
}

then the elements of @names in the caller have been changed, what is generally not expected. This is because the changed upper-scope $_ itself is aliased in map's body. As $_ is a convenient default for many things we'd have to keep track of everything used in the sub. So I'd indeed first copy $_, or safer yet localize it, in the sub and work with that.

Finally, in order to use either a passed parameter or $_ (at the point of the call)

sub trim {
    my $str = @_ ? shift : $_;   #/
    $str =~ s/^\s+|\s$//gr;
}

my @trimmed_names = map { trim } @names;  # may omit () if sub declared before

This is because the visibility of $_ is unrelated to the argument list in @_ so one can also pass arguments. Here we also get the (much) safer copying of $_.

The shift || $_ from the question would dismiss a 0 or '' (empty string) in @_, what is in principle valid input; the shift // $_ would dismiss an undef, also a possible input. Thanks to ikegami's comment on this. Thus explicitly test whether there is anything in @_.

While passing a variable that's undef isn't valid here it may be valid input in general. More to the point, the premise here is to use an argument if provided, so we should do that and then (hopefully) detect an error from the calling code (if passing undef shouldn't have happened), instead of quietly side-stepping it, by switching to $_.

So, my answer is a qualified "yes" -- that's one way to do it; but I may find it uncomfortable to work with a codebase where user's subs mix scopes. This example trim in map is perfectly safe as it stands, but where else may such a function wind up used? Why not just pass arguments?

Note: In order to be able to call a user-defined sub without parenthesis we must have it declared in the source before the point of invocation so that the interpreter knows what that bareword (trim) is, since without parens it doesn't have any hints.


I think it's worth recalling at this point that arguments to a sub are aliased, not copied, so if elements of @_ themselves are changed then caller's data gets changed. This isn't directly related to $_ but the behavior can be.

Upvotes: 3

ikegami
ikegami

Reputation: 386471

You can use the _ prototype.

sub trim(_) { $_[0] =~ s/^\s+|\s+\z//rg }

Otherwise, you can simply use $_ if no arguments were provided.

sub trim { ( @_ ? $_[0] : $_ ) =~ s/^\s+|\s+\z//rg }

Either way,

say for map trim, @strings;
  -or-
say for map trim($_), @strings;

Upvotes: 1

Related Questions