Perl: mapping to lists' first element
سؤال
Task: to build hash using map, where keys are the elements of the given array @a, and values are the first elements of the list returned by some function f($element_of_a):
my @a = (1, 2, 3);
my %h = map {$_ => (f($_))[0]} @a;
All the okay until f() returns an empty list (that's absolutely correct for f(), and in that case I'd like to assign undef). The error could be reproduced with the following code:
my %h = map {$_ => ()[0]} @a;
the error itself sounds like "Odd number of elements in hash assignment". When I rewrite the code such that:
my @a = (1, 2, 3);
my $s = ()[0];
my %h = map {$_ => $s} @a;
or
my @a = (1, 2, 3);
my %h = map {$_ => undef} @a;
Perl does not complain at all.
So how should I resolve this — get first elements of list returned by f(), when the returned list is empty?
Perl version is 5.12.3
Thanks.
المحلول
I've just played around a bit, and it seems that ()[0]
, in list context, is interpreted as an empty list rather than as an undef
scalar. For example, this:
my @arr = ()[0];
my $size = @arr;
print "$size\n";
prints 0
. So $_ => ()[0]
is roughly equivalent to just $_
.
To fix it, you can use the scalar
function to force scalar context:
my %h = map {$_ => scalar((f($_))[0])} @a;
or you can append an explicit undef
to the end of the list:
my %h = map {$_ => (f($_), undef)[0]} @a;
or you can wrap your function's return value in a true array (rather than just a flat list):
my %h = map {$_ => [f($_)]->[0]} @a;
(I like that last option best, personally.)
The special behavior of a slice of an empty list is documented under “Slices” in perldata
:
A slice of an empty list is still an empty list. […] This makes it easy to write loops that terminate when a null list is returned:
while ( ($home, $user) = (getpwent)[7,0]) { printf "%-8s %s\n", $user, $home; }
نصائح أخرى
I second Jonathan Leffler's suggestion - the best thing to do would be to solve the problem from the root if at all possible:
sub f {
# ... process @result
return @result ? $result[0] : undef ;
}
The explicit undef
is necessary for the empty list problem to be circumvented.
At first, much thanks for all repliers! Now I'm feeling that I should provide the actual details of the real task.
I'm parsing a XML file containing the set of element each looks like that:
<element>
<attr_1>value_1</attr_1>
<attr_2>value_2</attr_2>
<attr_3></attr_3>
</element>
My goal is to create Perl hash for element that contains the following keys and values:
('attr_1' => 'value_1',
'attr_2' => 'value_2',
'attr_3' => undef)
Let's have a closer look to <attr_1>
element. XML::DOM::Parser
CPAN
module that I use for parsing creates for them an object of class XML::DOM::Element
, let's give the name $attr
for their reference. The name of element is got easy by $attr->getNodeName
, but for accessing the text enclosed in <attr_1>
tags one has to receive all the <attr_1>
's child elements at first:
my @child_ref = $attr->getChildNodes;
For <attr_1>
and <attr_2>
elements ->getChildNodes
returns a list containing exactly one reference (to object of XML::DOM::Text
class), while for <attr_3>
it returns an empty list. For the <attr_1>
and <attr_2>
I should get value by $child_ref[0]->getNodeValue
, while for <attr_3>
I should place undef
into the resulting hash since no text elements there.
So you see that f
function's (method ->getChildNodes
in real life) implementation could not be controlled :-) The resulting code that I have wrote is (the subroutine is provided with list of XML::DOM::Element
references for elements <attr_1>
, <attr_2>
, and <attr_3>
):
sub attrs_hash(@)
{
my @keys = map {$_->getNodeName} @_; # got ('attr_1', 'attr_2', 'attr_3')
my @child_refs = map {[$_->getChildNodes]} @_; # got 3 refs to list of XML::DOM::Text objects
my @values = map {@$_ ? $_->[0]->getNodeValue : undef} @child_refs; # got ('value_1', 'value_2', undef)
my %hash;
@hash{@keys} = @values;
%hash;
}