Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Am I missing some semantics of $_ here?

Tags:

perl

During profiling, I came across this function in List::UtilsBy:

sub rev_nsort_by(&@) {
    my $keygen = shift;
    my @keys = map { local $_ = $_[$_]; scalar $keygen->( $_ ) } 0 .. $#_;
    return map { $_[$_] } sort { $keys[$b] <=> $keys[$a] } 0 .. $#_;
}

rev_nsort_by does a reverse numeric sort based on some key predicate, for example:

my @objects = load_objects_from_database();
# sort by rating, highest first
@objects = rev_nsort_by { $_->rating } @objects;

I understand perfectly why rev_nsort_by, as shown above, works as intended, but I'm wondering why it's so complex. Specifically, I wonder why

my @keys = map { local $_ = $_[$_]; scalar $keygen->( $_ ) } 0 .. $#_;

was not written as

my @keys = map { scalar $keygen->( $_ ) } @_;

which looks functionally equivalent to me. Am I missing some corner-case behavior of $_ here, which the longer version amounts for in some way?

like image 232
Stefan Majewsky Avatar asked Apr 24 '13 14:04

Stefan Majewsky


People also ask

How do I know the type of variable in Groovy?

You can use the getClass() method to determine the class of an object. Also, if you want to check if an object implements an Interface or Class, you can use the instanceof keyword. That's it about checking the datatype of an object in Groovy.

How do I assign a variable in Groovy script?

Variables in Groovy can be defined in two ways − using the native syntax for the data type or the next is by using the def keyword. For variable definitions it is mandatory to either provide a type name explicitly or to use "def" in replacement. This is required by the Groovy parser.

How do I use not equal in Groovy?

In groovy, the ==~ operator (aka the "match" operator) is used for regular expression matching. != is just a plain old regular "not equals".

What is def in Groovy script?

The def keyword is used to define an untyped variable or a function in Groovy, as it is an optionally-typed language.


1 Answers

There is a subtle edge case here: Inside foreach loops, or map expressions, the default variable $_ is aliased to the original value. E.g.

@nums = 1..5;
@foo = map { $_ *= 2 } @nums;
# both @foo and @nums contain 2, 4, 6, 8, 10 now.

However, constants aren't valid lvalues, so we couldn't do that like

@foo = map { $_ *= 2 } 1, 2, 3, 4, 5;
# Modification of read-only value

The @_ array too is aliased to the original values, so imagine the following edge cases:

sub buggy (&@) { my $cb = shift; map $cb->($_), @_ };

buggy { $_ *= 2 } 1, 2, 3;   # Modification of read-only value attempted
buggy { $_[0] *= 2} 1, 2, 3; # ditto

my @array = 1 .. 5;
buggy { $_ *= 2 } @array;    # @array now is 2, 4, 6, 8, 10
buggy { $_[0] *= 2 } @array; # ditto

Aliases are transitive, so inner $_[0] is aliased to $_, which is aliased to outer $_[0], which is an alias for the constant 1 / $array[0].

So, what does local $_ = $_[$_] do here?

  • It makes a copy of the value, thus avoiding this insane aliasing behaviour
  • It shows the intent to make $_ visible to the callback.

Ensuring copying semantics (thus avoiding unexpected side effects) feels natural for Perl, so this function is well-designed and not especially overengineered.

(Note: map {local $_ = $_; ...} @_ would have been sufficient to make the copy)

like image 88
amon Avatar answered Oct 22 '22 14:10

amon