Beefy Boxes and Bandwidth Generously Provided by pair Networks
Just another Perl shrine

Unhappy returns

by tlm (Prior)
on Oct 10, 2005 at 01:33 UTC ( #498672=perlmeditation: print w/replies, xml ) Need Help??

Dear monks,

I wrote this bug last week. It's not a particularly nasty bug, but it looks so innocent I thought I'd share, particularly for the benefit of brothers and sisters who are getting the hang or using map.

In essence, the code was meant to produce an array of numbers from an array of objects, like this:

my @row = map $_->value, @$arr;
But some of the elements in @$arr could be undefined, and of those that were defined, some could have the string 'NA' or undef returned by the value method. I wanted all these cases to show up as undef in the LHS, so I wrote something like this:
for my $arr ( @results ) { ... my @row = map val( $_ ), @$arr; ... } sub val { my $o = shift; if ( defined $o ) { my $v = $o->value; return $v if defined $v and $v ne 'NA'; } return; }
Spot the bug? It's in the second return statement of val. Without an argument, it causes val to return undef in scalar context, but return the empty list in list context. As it happens, map evaluates its first argument in list context. Bottom line: even though, in every iteration, the size of @$arr was the same, the size of @row would sometimes be smaller.

Here's a simple illustration of the same thing:

sub foo { if ( $_[ 0 ] % 3 } { return $_[ 0 ]; # argument divisible by 3 } return; } print join( ':', map foo( $_ ), 1..5 ), "\n"; __END__ 1:2:4:5
Note that the output has 4 elements, while the input had 5. If one wants those undefs in the output from map, one either has to force the scalar context
print join( ':', map scalar foo( $_ ), 1..5 ), "\n"; __END__ 1:2::4:5
or change the last statement in foo to an explicit return undef; . I'd pick the former approach for flexibility, the latter one for clarity.

It is worth noting, however, that, contrary to map, grep evaluates its first argument in scalar context:

print join( ' ', grep sub { wantarray ? 0 : 1 }, 1..3 ), "\n"; __END__ 1 2 3

Update: Twice I referred to "second argument" (of map/grep) when I meant "first argument". I'm still scratching my head over the origins of this braino, but thanks to itub for pointing it out.

the lowliest monk

Replies are listed 'Best First'.
Re: Unhappy returns
by Aristotle (Chancellor) on Oct 10, 2005 at 01:56 UTC

    It is worth noting, however, that, contrary to map, grep evaluates its second argument in scalar context

    And that is perfectly expected, because while map accumulates a list from successive evaluations of the expression, grep evaluates the expression as a boolean – which always implies scalar context.

    Makeshifts last the longest.

Re: Unhappy returns
by ysth (Canon) on Oct 10, 2005 at 04:34 UTC
    It is worth noting, however, that, contrary to map, grep evaluates its second argument in scalar context:
    And contrary to grep and map, foreach evaluates its body in void context:
    $ perl sub ctx { print wantarray ? "list" : defined wantarray ? "scalar" : "v +oid" } @x = (1); print "map:"; map ctx, @x; print "\ngrep:"; grep ctx, @x; print "\nfor:"; ctx for @x; __END__ map:list grep:scalar for:void
      And contrary to grep and map, foreach evaluates its body in void context
      Isn't the comparison a little unfair? map and grep are operators, whereas for is a looping construct


      Feel the white light, the light within
      Be your own disciple, fan the sparks of will
      For all of us waiting, your kingdom will come

        Well, map and grep are usually seen as functions, but there's no denying that both map and grep are looping constructs.

        But ultimely, what is the difference between an operator, a function or a syntax construct from a programmers aspect of view (other than using it to answer "where do I find this in the manual")? Would you code differently if map or grep were reclassified as a syntax or looping construct, or foreach as a function?

        Perl --((8:>*
Re: Unhappy returns
by Errto (Vicar) on Oct 10, 2005 at 04:01 UTC
    When I first learned about map in Perl, I assumed it behaved the same way as map in other languages I was using at the time - Haskell and ML. In these languages, the mapped function takes one argument and returns one value. This is equivalent to Perl's map using scalar context, which it perfectly well could have done. This was (presumably) an intentional design decision on Larry's part. Had it been the other way, we could guarantee that map would always return a list of exactly the same length as its argument list, and issues like the one you describe wouldn't arise. On the other hand, we wouldn't have the flexibility to return longer or shorter lists if appropriate. So it's a tradeoff.
      It's a tradeoff, and I'm glad Larry made the decision map can return lists of arbitrary length. My prime use of this feature is to create hashes:
      my %hash = map {($_, 1)} qw /foo bar baz/;
      Perl --((8:>*

        Yes – though that still only returns a fixed number of elements per evaluation. What about flattening data structures?

        my @list = map @$_, @$lol;

        I think that feature of map is one of the most useful things to have around for those occasions it’s called for.

        Makeshifts last the longest.

        Using map is a lot of overhead for just assigning to a hash.
        use Benchmark; timethese(1000, { 'map1' => sub { my %str = map {($_,1)} (0..1024); }, 'arry' => sub { my %str = (); @str{0..1024} = (1)x1025; } }); Benchmark: timing 1000 iterations of arry, map1... arry: 1 wallclock secs ( 1.36 usr + 0.00 sys = 1.36 CPU) @ 73 +5.29/s (n=1000) map1: 3 wallclock secs ( 3.11 usr + 0.00 sys = 3.11 CPU) @ 32 +1.54/s (n=1000)
Re: Unhappy returns
by dragonchild (Archbishop) on Oct 10, 2005 at 17:26 UTC
    Your first mistake was using map as a function and not a keyword. You should have written
    for my $arr ( @results ) { ... my @row = map { val( $_ ) } @$arr; ... }
    The bug is still there, but it's clearer now that you're working in list context. At that point, you can fix it either by returning an explicit undef from val() (the bad way) or modifying your map body to be val( $_ ) || undef (the good way).

    My criteria for good software:
    1. Does it work?
    2. Can someone else come in, make a change, and be reasonably certain no bugs were introduced?

Log In?

What's my password?
Create A New User
Node Status?
node history
Node Type: perlmeditation [id://498672]
Approved by GrandFather
Front-paged by grinder
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others cooling their heels in the Monastery: (6)
As of 2021-01-21 10:58 GMT
Find Nodes?
    Voting Booth?