Perl: сопоставление с первым элементом списков

Задача: построить хэш с помощью карты, где ключи — это элементы заданного массива @a, а значения — это первые элементы списка, возвращаемого некоторой функцией f($element_of_a):

my @a = (1, 2, 3);
my %h = map {$_ => (f($_))[0]} @a;

Все в порядке, пока f() не вернет пустой список (это абсолютно правильно для f(), и в этом случае я бы хотел присвоить undef). Ошибка может быть воспроизведена с помощью следующего кода:

my %h = map {$_ => ()[0]} @a;

сама ошибка звучит как «Нечетное количество элементов в назначении хеша». Когда я переписываю код так, что:

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 вообще не жалуется.

Итак, как мне решить эту проблему — получить первые элементы списка, возвращаемого функцией f(), когда возвращаемый список пуст?

Версия Perl — 5.12.3.

Спасибо.


person indexless    schedule 20.01.2012    source источник
comment
Оберните вызов f так, чтобы, когда он возвращает пустой список, вы вводили undef или иначе первый элемент возвращаемого списка.   -  person Jonathan Leffler    schedule 20.01.2012


Ответы (3)


Я только что немного поиграл, и кажется, что ()[0] в контексте списка интерпретируется как пустой список, а не как скаляр undef. Например, это:

my @arr = ()[0];
my $size = @arr;
print "$size\n";

печатает 0. Таким образом, $_ => ()[0] примерно эквивалентно просто $_.

Чтобы исправить это, вы можете использовать функцию scalar для принудительного скалярного контекста:

my %h = map {$_ => scalar((f($_))[0])} @a;

или вы можете добавить явное undef в конец списка:

my %h = map {$_ => (f($_), undef)[0]} @a;

или вы можете обернуть возвращаемое значение вашей функции в истинный массив (а не просто в плоский список):

my %h = map {$_ => [f($_)]->[0]} @a;

(Лично мне больше всего нравится последний вариант.)


Особое поведение фрагмента пустого списка описано в разделе "Slices" в perldata:

Фрагмент пустого списка по-прежнему остается пустым списком. […] Это упрощает написание циклов, которые завершаются, когда возвращается нулевой список:

while ( ($home, $user) = (getpwent)[7,0]) {
    printf "%-8s %s\n", $user, $home;
}
person ruakh    schedule 20.01.2012
comment
Я отредактировал ссылку на документацию, которая объясняет, почему ()[0] возвращает пустой список вместо undef. Если вы не одобряете, пожалуйста, не стесняйтесь отменить мою правку (или, что еще лучше, улучшить ее). - person derobert; 20.01.2012
comment
@derobert: я полностью одобряю. Большое тебе спасибо! - person ruakh; 21.01.2012
comment
Здесь нет ничего плохого в анализе, но это много линейного шума. Лично я бы предпочел, чтобы f обрабатывал пограничный случай, так как это делает код более удобным для сопровождения. Конечно, если нет контроля над определением f, то это совсем другое дело - person Zaid; 21.01.2012
comment
@Zaid: У нас недостаточно информации, чтобы сказать наверняка, но я склонен не согласиться. f предназначен для возврата списка значений; предположительно эта строка кода, которая отбрасывает все значения, кроме первого, является скорее исключением, чем правилом, и, предположительно, это вызвало бы головную боль при обслуживании, если бы каждый второй вызов f должен был явно проверять случай, когда он возвращал undef, и переводить это обратно в пустой список. (Обратите внимание, что OP пишет, что для f() абсолютно правильно [to] return[] пустой список. Это означает, что f в настоящее время имеет значимое, связное определение.) - person ruakh; 21.01.2012
comment
Вместо маршрута change f есть также маршрут wrap f. Конечно, можно сделать sub g { my $n = shift; ( f($n) )[0] // undef } # or any of the alternative ways to write this, если вы часто делаете эту карту. Или, если это с кучей функций, вы можете сделать версию g более высокого порядка, чтобы динамически обертывать вещи. - person derobert; 21.01.2012
comment
дероберт, большое спасибо за ответ. Не могли бы вы пояснить, почему длина слайса пустого списка ()[...] равна нулю, а длина []->[...] (я имею в виду контекст списка) равна единице? Да, у меня длина 1 не только для []->[0], но и для []->[0, 1], и любого списка индексов. - person indexless; 22.01.2012
comment
Я только что проверил еще одну форму, @{[]}[...], и обнаружил, что длина этого фрагмента равна длине .... Скажем, для @s = @{[]}[1, 1, 1], scalar(@s) равно 3 :-) - person indexless; 22.01.2012
comment
Понял: []->[...] slice читается в скалярном контексте, поэтому возвращает только последний элемент, а @{[]}[...] читается в контексте списка и возвращает список, построенный из указанных элементов. - person indexless; 22.01.2012

Я поддерживаю предложение Джонатана Леффлера - лучше всего было бы решить проблему с самого начала, если это вообще возможно:

sub f {

    # ... process @result

    return @result ? $result[0] : undef ;
}

Явное undef необходимо для решения проблемы с пустым списком.

person Zaid    schedule 20.01.2012

Во-первых, большое спасибо всем ответившим! Теперь я чувствую, что должен предоставить фактические детали реальной задачи.

Я разбираю файл XML, содержащий набор элементов, каждый из которых выглядит так:

<element>
    <attr_1>value_1</attr_1>
    <attr_2>value_2</attr_2>
    <attr_3></attr_3>
</element>

Моя цель — создать хэш Perl для элемента, который содержит следующие ключи и значения:

('attr_1' => 'value_1',
 'attr_2' => 'value_2',
 'attr_3' =>  undef)

Давайте подробнее рассмотрим элемент <attr_1>. XML::DOM::Parser CPAN модуль, который я использую для парсинга, создает для них объект класса XML::DOM::Element, давайте для их ссылки дадим имя $attr. Имя элемента легко получить с помощью $attr->getNodeName, но для доступа к тексту, заключенному в теги <attr_1>, нужно сначала получить все дочерние элементы <attr_1>:

my @child_ref = $attr->getChildNodes;

Для <attr_1> и <attr_2> элементов ->getChildNodes возвращает список, содержащий ровно одну ссылку (на объект класса XML::DOM::Text), а для <attr_3> возвращает пустой список. Для <attr_1> и <attr_2> я должен получить значение $child_ref[0]->getNodeValue, а для <attr_3> я должен поместить undef в результирующий хэш, так как там нет текстовых элементов.

Таким образом, вы видите, что реализация функции f (метод ->getChildNodes в реальной жизни) не может контролироваться :-) В результате получается код, который я написал (подпрограмма снабжена списком ссылок XML::DOM::Element для элементов <attr_1>, <attr_2> и <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;
}
person indexless    schedule 21.01.2012
comment
Я хотел бы, чтобы вы упомянули об этом заранее. Вы получите только такой хороший ответ, как вопрос, который вы задаете. Жаль, что эта информация не была доступна раньше. - person Zaid; 22.01.2012
comment
Почему? Я полагаю, что у меня есть идеальные ответы, которые позволили мне прояснить многие моменты, касающиеся списков и срезов :-) - person indexless; 22.01.2012