Как я могу принудительно выйти из подпрограммы / закрытия perl через last / next, чтобы автоматически завершить работу программы? - PullRequest
4 голосов
/ 18 апреля 2019

Имеются следующие полнофункциональные Perl-скрипт и модуль:

tx_exec.pl :

#!/usr/bin/perl

use strict; # make sure $PWD is in your PERL5LIB
# no warnings!

use tx_exec qw(tx_exec);

tx_exec ("normal", sub { return "foobar"; });
tx_exec ("die", sub { die "barbaz\n"; });
tx_exec ("last", sub { last; });
tx_exec ("next", sub { next; });

tx_exec.pm :

package tx_exec;

use strict;
use warnings;

require Exporter;
our @ISA = qw(Exporter);
our @EXPORT_OK = qw(tx_exec);

my $MAX_TRIES = 3;

sub tx_exec {
    my ($desc, $sub, $args) = @_;
    print "\ntx_exec($desc):\n";
    my $try = 0;
    while (1) {
        $try++;
        my $sub_ret;
        my $ok = eval {
            # start transaction
            $sub_ret = $sub->($args);
            # commit transaction
            1;
        };

        unless ($ok) {
            print "failed with error: $@";
            # rollback transaction
            if ($try >= $MAX_TRIES) {
                print "failed after $try tries\n";
                return (undef, undef);
            }
            print "try #$try failed, retrying...\n";
            next;
        }
        # some cleanup
        print "returning (1, ".($sub_ret//'<undef>').")\n";
        return (1, $sub_ret);
    }
}

Я получаю следующий вывод:

$ ./tx_exec.pl
tx_exec(normal):
returning (1, foobar)

tx_exec(die):
failed with error: barbaz
try #1 failed, retrying...
failed with error: barbaz
try #2 failed, retrying...
failed with error: barbaz
failed after 3 tries

tx_exec(last):

tx_exec(next):
# infinite loop

Я понимаю, что происходит, и получаю предупреждение об этом, еслиЯ включаю предупреждения в сценарии, определяющем замыкания.Однако можно ли принудительно заставить программу завершиться с ошибкой / умереть автоматически / идиоматически, когда next / last выйдет из подпрограммы замыкания, как здесь, при следующих строгих обстоятельствах:

  • Передаваемая $subзамыкание, а не простая функция (простая функция умирает на голом next/last в любом случае, что легко обрабатывать)
  • Код библиотеки (tx_exec) и код клиента (вызывающий ее) находятся в отдельныхмодули компиляции и клиент не используют предупреждения .

с использованием perl 5.16.2 (без возможности обновления).

Здесь находится githubgist документирование всех подходов:

  • use warnings FATAL => qw(exiting) не имеет значения в коде библиотеки
  • local $SIG обработчик не работает, если сайт вызоваFATAL => qw(exiting) предупреждение не включено
  • ручное обнаружение работает, но несколько громоздко и повсеместно (нелокализовано)
  • Подход ysth с голым блоком работаетлучше всего, так как ловит последний /затем, полностью локализуя ручное обнаружение и гарантируя, что ничто не может пойти не так (за исключением следующего / последнего с метками, которых легче избежать).

Ответы [ 3 ]

3 голосов
/ 18 апреля 2019

Short Использование next / last в подпрограмме (этот вызывающий объект передается как coderef) вызывает исключение, если оно не находится внутри "блока цикла". Это позволяет легко обрабатывать такое использование с небольшим изменением tx_exec().


Неправильное использование last / next, о котором идет речь в вопросе, немного нюансировано. Сначала от последний

last нельзя использовать для выхода из блока, который возвращает значение, например eval {}, sub {} или do {}, и его нельзя использовать для выхода из операции grep или map.

и для этого в подпрограмме или eval мы получаем предупреждение

Exiting subroutine via last at ...

(и для "eval"), и аналогично для next. Они классифицируются как W в perldiag и могут управляться с использованием / не warnings прагмой. & dagger; Этот факт мешает сделать такое использование смертельным с помощью FATAL => 'exiting' предупреждение или $SIG{__WARN__} крючком.

Однако, если такое использование next или last (в подпрограмме или eval) не имеет "блока цикла" в какой-либо охватывающей области (или стеке вызовов), то это также вызывает исключение . & Dagger; Сообщение

Can't "last" outside a loop block...

и аналогично для next. Он находится в perldiag (поиск по outside a loop), классифицирован как F.

Тогда одно из решений поставленной проблемы состоит в том, чтобы запустить coderef, переданный вызывающей стороной , за пределами блоков цикла , и мы получаем интерпретатор, который проверяет и предупреждает нас (вызывает исключение) использование, вызывающее проблемы. Поскольку цикл while (1) существует только для возможности многократной попытки, это можно реализовать.

Coderef может быть запущен и протестирован с этим исключением в служебной программе

sub run_coderef {
    my ($sub, @args) = @_; 

    my $sub_ret;
    my $ok = eval { $sub_ret = $sub->(@args); 1 };
    if (not $ok) { 
        if ($@ =~ /^Can't "(?:next|last)"/) {  #'
            die $@;                            # disallow such use
        }
        else { return }                        # other error, perhaps retry
    }   
    else { return $sub_ret }
}

который можно использовать как

sub tx_exec {
    my ($sub, @args) = @_; 

    my $sub_ret = run_coderef($sub, @args);

    my $run_again = (defined $sub_ret) ? 0 : 1;

    if ($run_again) { 
        my $MAX_TRIES = 3;
        my $try = 0;

        while (1) { 
            ++$try;
            $sub_ret = run_coderef($sub, @args);

            if ( not defined $sub_ret ) {  # "other error", run again
                if ($try >= $MAX_TRIES) { 
                    print "failed after $try tries\n";
                   return (undef, undef);
                }
                print "try #$try failed, retrying...\n";
                next;
            }
            ...
        }
    }
}   

Этот подход имеет смысл в дизайне: он позволяет создавать исключение для запрещенного использования и локализует обработку в своем собственном подчиненном элементе.

Запрещенное поведение действительно проверяется только при первом запуске, так как после этого run_coderef вызывается вне цикла, и в этом случае (это) исключение не генерируется. Это нормально, поскольку повторные прогоны (для «разрешенных» сбоев) выполняются с той же самой подпрограммой, поэтому достаточно проверить первое использование.

С другой стороны, это также означает, что мы можем

  • запустить eval { $sub_ret = $sub->(@args) ... } непосредственно в while (1), так как мы проверили на неправильное использование last / next при первом запуске

  • Можно добавить дополнительные случаи для проверки в run_coderef, что делает его более округлым средством проверки / контроля. Первый пример - Exiting предупреждения, которые мы можем сделать фатальными и проверить их. Это будет полезно, если в вызывающей стороне включены предупреждения

Этот подход может быть сорван, но вызывающий должен был бы изо всех сил идти к этому концу.

Протестировано с v5.16.3 и v5.26.2.


& dagger; & thinsp; Кстати, вы не можете бороться с решением вызывающего абонента отключить предупреждения. Пусть они будут. Это их код.

& Dagger; & thinsp; Это можно проверить с помощью

perl -wE'sub tt { last }; do { tt() }; say "done"'

где мы получаем

Exiting subroutine via last at -e line 1.
Can't "last" outside a loop block at -e line 

в то время как есть "зацикленный" блок

perl -wE'sub tt { last }; { do { tt() } }; say "done"'

мы видим конец программы, без исключения

Exiting subroutine via last at -e line 1.
done

Дополнительный блок { ... } " семантически идентичен циклу, который выполняется один раз " ( следующий ).

Это можно проверить для eval, напечатав его сообщение в $@.


Исходное сообщение, основанное на ожидании, что будут отправляться только предупреждения

Прагма предупреждений является лексической, поэтому добавьте к каждому комментарию

use warnings FATAL => 'exiting';

в самом сабе (или в eval для более узкого охвата) должно работать с ограничениями

sub tx_exec { 
    use warnings FATAL => "exiting"; 

    my ($sub, $args) = @_; 
    $sub->($args);        
}; 

, так как предупреждение срабатывает в области действия tx_exec. В моем тесте вызов этого с coderef, который не выполняет last/next, сначала выполняется нормально, и он умирает только для последующего вызова с ними.

Или, может реализовать это, используя $SIG{__WARN__} «сигнал» (хук)

sub tx_exec {   
    local $SIG{__WARN__} = sub { 
        die @_ if $_[0] =~ /^Exiting subroutine via (?:last|next)/; 
        warn @_ 
    };

    my ($sub, $args) = @_;
    ...
}
1 голос
/ 19 апреля 2019

Это ручной подход, о котором я упоминал в этом вопросе.Пока что это был единственный подход, который помог мне аккуратно обрабатывать некорректно работающий клиентский код, без каких-либо предположений или ожиданий.local $SIG или use warnings FATAL => 'exiting', , если они работают без ожидания от клиентского кода (в частности, для него включены предупреждения в любой форме).

tx_exec.pl :

#!/usr/bin/perl

use strict;
# no warnings!

use tx_exec qw(tx_exec);

tx_exec ("normal", sub { return "foobar"; });
tx_exec ("die", sub { die "barbaz\n"; });
tx_exec ("last", sub { last; });
tx_exec ("next", sub { next; });

tx_exec.pm :

package tx_exec;

use strict;
use warnings;

require Exporter;
our @ISA = qw(Exporter);
our @EXPORT_OK = qw(tx_exec);

my $MAX_TRIES = 3;

sub tx_exec {
    my ($desc, $sub, $args) = @_;
    print "\ntx_exec($desc):\n";
    my $try = 0;
    my $running = 0;
    while (1) {
        $try++;
        my $sub_ret;
        my $ok = eval {
            # start transaction
            die "Usage of `next` disallowed in closure passed to tx_exec\n" if $running;
            $running = 1;
            $sub_ret = $sub->($args);
            print "sub returned properly\n";
            # commit transaction
            1;
        };

        $running = 0;

        unless ($ok) {
            if ($@ =~ /^Usage of `next`/) {
                print $@;
                return (undef, undef); # don't retry
            }
            print "failed with error: $@";
            # rollback transaction
            if ($try >= $MAX_TRIES) {
                print "failed after $try tries\n";
                return (undef, undef);
            }
            print "try #$try failed, retrying...\n";
            next;
        }
        # some cleanup
        print "returning (1, ".($sub_ret//'<undef>').")\n";
        return (1, $sub_ret);
    }
    print "Usage of `last` disallowed in closure passed to tx_exec\n";
    return (undef, undef);
}

вывод :

tx_exec(normal):
sub returned properly
returning (1, foobar)

tx_exec(die):
failed with error: barbaz
try #1 failed, retrying...
failed with error: barbaz
try #2 failed, retrying...
failed with error: barbaz
failed after 3 tries

tx_exec(last):
Usage of `last` disallowed in closure passed to tx_exec

tx_exec(next):
Usage of `next` disallowed in closure passed to tx_exec
0 голосов
/ 22 апреля 2019

Из-за недостаточного участия @ ysth в написании ответа я пишу лучшее решение, которое я нашел до сих пор, вдохновленное его первой попыткой из комментариев к вопросу.(Я приму ответ ysth, если он отправит его позже).


eval, вызывающий coderef, должен выглядеть следующим образом:

my $ok = eval {
    # start transaction
    my $proper_return = 0;
    {
        $sub_ret = $sub->($args);
        $proper_return = 1;
    }
    die "Usage of `next` or `last` disallowed in coderef passed to tx_exec\n" unless $proper_return;
    # commit transaction
    1;
};

Пустой блокдействуя как цикл, который немедленно выйдет либо на next, либо на last, поэтому независимо от того, приземляемся ли мы после голого блока или внутри него от вызова coderef, мы можем определить, выполнил ли coderef next/last, и действовать ли соответствующим образом.

Подробнее о семантике голых блоков и их взаимодействии с next/last можно найти здесь .

Для читателя оставлено упражнение по обработкередко встречается redo в коде выше.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...