Проблемы с динамическим импортом переменных в пространство имен

У меня есть массив хешей в Perl. Я хотел бы пройтись по массиву и выполнить некоторый код, указанный в строке для каждого из хэшей (примечание: я не хотел бы обсуждать этот подход или соображения его безопасности; давайте сосредоточимся на том, как это можно сделать). Пример реализации может выглядеть так:

use 5.014;
use strict;
use warnings;

my $list = [
  {foo => "foo1"},
  {foo => "foo2"},
  {foo => "foo3"},
  {foo => "foo4"},
];

my $code = q{say $item->{foo}};

foreach my $item (@$list) {
  eval qq{$code};
}

Как и ожидалось, это печатает:

foo1
foo2
foo3
foo4

Все идет нормально. Однако мне бы хотелось, чтобы структура данных массива и хэша была прозрачной для фрагмента кода, т. е. фрагмент кода должен иметь возможность использовать $foo вместо $item->{foo} и т. д. Поэтому я пытаюсь импортировать хэш-записи в пространство имен main. (Опять же, я хотел бы обсудить, как это сделать, а не стоит ли мне это делать.)

use 5.014;
use strict;
use warnings;

my $list = [
  {foo => "foo1"},
  {foo => "foo2"},
  {foo => "foo3"},
  {foo => "foo4"},
];

my $code = q{say $foo};

foreach my $item (@$list) {
  foreach my $key (keys %$item) {
    $main::{$key} = \$item->{$key};
  }

  {
    no strict "vars";
    eval qq{$code};
  }
}

Я ожидал, что это сработает, но это не так (полностью). Этот код печатает:

Use of uninitialized value $foo in say at (eval 1) line 1.

foo2
foo3
foo4

По какой-то причине первый элемент обрабатывается неправильно.

Я попробовал несколько способов обойти это или узнать больше о том, что не так. Следующие изменения все работают (возможно, с предупреждением о том, что $foo используется только один раз):

  1. замените eval qq{$code} на литерал say $foo
  2. замените my $code = q{say $foo} на my $code = q{say ${$main::{foo}}} (однако $main::foo недостаточно)
  3. используйте переменную $foo где-то раньше, например. поставьте { no strict "vars"; my $bar = $foo; } перед внешним циклом foreach
  4. явно создайте переменную пакета, вставив eval sprintf('our $%s;', $key); перед $main::{$key} = \$item->{$key};

Вариант 4 — это то, что я бы сделал сейчас. Это кажется немного хакерским, но делает свою работу. Однако мне все же хотелось бы понять, зачем это необходимо и есть ли лучший способ это исправить.

ОБНОВЛЕНИЕ: Как указывает TLP, более простой способ добиться этого — использовать символическую ссылку вместо манипуляций с таблицей символов. Смотрите мой собственный ответ на вопрос. Однако мне все же хотелось бы понять, почему подход с таблицей символов не работает «из коробки».

Ага, понятно. Вы думаете, что $main::{$key} должно быть таким же, как $foo?

TLP 02.07.2024 14:45

Вместо этого попробуйте ${$key}. С включенными параметрами no strict vars.

TLP 02.07.2024 14:47

Что касается $main::{$key}: Да, $key содержит строку foo и, следовательно, $main::{$key} относится к записи foo в таблице символов. Что касается ${$key}: Хороший вопрос! Я явно думал слишком сложно. (Однако в данном случае это no strict 'refs'.)

PowerGnom 02.07.2024 14:53

Я до сих пор не могу заставить его работать. Я должен сказать, что, хотя это интересно с академической точки зрения, это хак и очень плохой дизайнерский выбор. Это очень похоже на проблему XY. Т.е. это неправильное решение вашей проблемы.

TLP 02.07.2024 14:56

@TLP Решение с использованием вашего предложения см. в моем собственном ответе на вопрос. Что касается проблемы XY, то я сознательно не стал добавлять к вопросу свои аргументы в пользу выбора такого подхода, поскольку посчитал это слишком большим отступлением. Короче говоря, причина в том, что и фрагмент кода, и данные известны только во время выполнения, а структура данных должна быть прозрачной для человека, пишущего фрагмент кода.

PowerGnom 02.07.2024 15:24
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
5
92
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Объявите $foo перед присвоением ему:

#!/usr/bin/perl
use warnings;
use strict;
use feature qw{ say };

my $list = [ map { +{foo => "foo$_"} } 1 .. 4 ];

our $foo;
my $code = q{say $foo};

for my $item (@$list) {
    for my $key (keys %$item) {
        $main::{$key} = \$item->{$key};
    }
    eval $code;
}

Затем вы можете выбросить eval и вместо этого использовать ссылки на код для повышения безопасности:

#!/usr/bin/perl
use warnings;
use strict;
use feature qw{ say };

my $list = [ map { +{foo => "foo$_"} } 1 .. 4 ];

our $foo;
my $code = sub { say $foo };

for my $item (@$list) {
    for my $key (keys %$item) {
        $main::{$key} = \$item->{$key};
    }
    $code->();
}

Спасибо за ваш ответ. Что касается объявления $foo: Да, это работает, но foo теперь жестко запрограммирован. Я знаю только во время выполнения, какие переменные необходимы. Следовательно, подход eval (также используется our в моем варианте 4 в вопросе. Что касается ссылки на код: опять же, это работает только в том случае, если код, который нужно выполнить, известен во время компиляции.

PowerGnom 02.07.2024 15:14
Ответ принят как подходящий

Символьная ссылка — более простой способ, чем манипулирование таблицей символов. Вот адаптация кода в вопросе, который решает проблему:

use 5.014;
use strict;
use warnings;

my $list = [
  {foo => "foo1"},
  {foo => "foo2"},
  {foo => "foo3"},
  {foo => "foo4"},
];

my $code = q{say $foo};

foreach my $item (@$list) {
  foreach my $key (keys %$item) {
    no strict 'refs';
    ${$key} = $item->{$key};
  }

  {
    no strict 'vars';
    eval qq{$code};
  }
}

Важно отметить, что приведенное выше решение создает копию значений в структуре данных вместо создания псевдонима. Это имеет значение, если код в $code изменяет значения. Пример модифицированного кода ниже демонстрирует это. Он также содержит (закомментированную) манипуляцию с фиксированной таблицей символов, которая вместо этого создает псевдоним, тем самым позволяя изменить исходную структуру данных с помощью $code. См. также ответ зажима.

use 5.014;
use strict;
use warnings;

my $list = [
  {foo => "foo1"},
  {foo => "foo2"},
  {foo => "foo3"},
  {foo => "foo4"},
];

my $code = q{$foo .= '_bar'; say $foo};

foreach my $item (@$list) {
  foreach my $key (keys %$item) {
    no strict 'refs';
    ${$key} = $item->{$key};  # copy
    # *{$key} = \$item->{$key};  # alias
  }

  {
    no strict 'vars';
    eval qq{$code};
  }

  say $item->{foo};
}

Не совсем ясно, хотите ли вы иметь псевдоним или копию значения в $main::foo. Синтаксис псевдонима будет *foo = \$bar (назначить скалярную ссылку на typeglob), см. perlmod:

use 5.014;
use strict;
use warnings;

my $list = [
  {foo => "foo1"},
  {foo => "foo2"},
  {foo => "foo3"},
  {foo => "foo4"},
];
my $code = q{say $foo};
foreach  my $item (@$list) {
  foreach my $key (keys %$item) {
    no strict 'refs';
    *{"main::$key"} = \$item->{$key}; # alias
    # *{$key} = \$item->{$key};       # alias
    # ${$key} = $item->{$key};        # value
  }

  {
    no strict 'vars';
    eval qq{$code};
  }
}

Вы правы, я не уточнил в вопросе, ищу ли я псевдоним или копию, потому что в моем конкретном случае использования это не имеет большого значения. Однако очевидно, что это может быть очень важно, поэтому спасибо, что указали на это. Спасибо также за исправление назначения typeglob, это то, что я искал изначально. Я также изменил свой ответ, включив в него ваши замечания.

PowerGnom 03.07.2024 10:20

Я бы поместил всю структуру цикла в отдельный блок глобальной области видимости с no strict 'refs'; в качестве первой строки, просто чтобы сделать ее более понятной тем, кому понадобится читать код в будущем. Я бы также заменил foreach() на for(), что в наши дни гораздо более идиоматично. Хотя строка no strict 'refs' выполняется только один раз во время компиляции, в том виде, в каком вы ее написали, она имеет два уровня глубины. Кроме того, у вас есть проблема с отступами, которая еще больше скрывает вещи. И последнее: no strict; — чрезвычайно опасное предложение.

stevieb 03.07.2024 11:14

@stevieb: исправлены отступы и возвращены строгие переменные. Остальные ваши замечания мне кажутся делом вкуса. Если в кодовой базе OPs foreach предпочтительнее for, я не вижу причин это менять. Также для меня имеет смысл иметь nostrict 'refs' в области, максимально близкой к тому месту, где необходим его эффект.

clamp 03.07.2024 18:03

Другие вопросы по теме