Как реализовать assert в Perl?

При попытке реализовать макрос assert() на Perl возникает фундаментальная проблема. Сначала рассмотрим этот код:

sub assert($$) {
   my ($assertion, $failure_msg) = @_;
   die $failure_msg unless $assertion;
}

# ...
assert($boolean, $message);

Хотя это работает, это не похоже на C: в C я бы написал assert($foo <= $bar), но с этой реализацией мне пришлось бы писать assert($foo <= $bar, '$foo <= $bar'), то есть повторение условие как строку.

Теперь интересно, как реализовать этот эффективно. Простой вариант, кажется, передает строку assert() и использует eval для оценки строки, но вы не можете получить доступ к переменным при оценке eval. Даже если бы это сработало, это было бы довольно неэффективно, поскольку условие каждый раз анализируется и оценивается.

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

Другой вариант с использованием assert(sub { $condition }), где, вероятно, проще сделать строку из ссылки на код, считается слишком уродливым.

Конструкция assert(sub { (eval $_[0], $_[0]) }->("condition")); с

sub assert($)
{
    die "Assertion failed: $_[1]\n" unless $_[0];
}

сделал бы, но некрасиво звонить. решение, который я ищу, - это написать условие для проверки только один раз, при этом я могу воспроизвести исходное (не оцененное) состояние и эффективно оценить состояние.

Итак, какие есть более элегантные решения? Очевидно, решения были бы проще, если бы Perl имел макрос или аналогичный синтаксический механизм, позволяющий преобразовывать ввод перед компиляцией или оценкой.

Можно ли это сделать с помощью Filter::Simple?

U. Windl 13.02.2019 11:53
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
1
1 021
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Использовать B:: Депарсировать?

#!/usr/bin/perl
use strict;
use warnings;

use B::Deparse;
my $deparser = B::Deparse->new();

sub assert(&) {
    my($condfunc) = @_;
    my @caller    = caller();
    unless ($condfunc->()) {
        my $src = $deparser->coderef2text($condfunc);
        $src =~ s/^\s*use\s.*$//mg;
        $src =~ s/^\s+(.+?)/$1/mg;
        $src =~ s/(.+?)\s+$/$1/mg;
        $src =~ s/[\r\n]+/ /mg;
        $src =~ s/^\{\s*(.+?)\s*\}$/$1/g;
        $src =~ s/;$//mg;
        die "Assertion failed: $src at $caller[1] line $caller[2].\n";
    }
}

my $var;
assert { 1 };
#assert { 0 };
assert { defined($var) };

exit 0;

Тестовый вывод:

$ perl dummy.pl
Assertion failed: defined $var at dummy.pl line 26.

Обновлен ответ, чтобы использовать синтаксический сахар, то есть àssert { CONDITION };.

Stefan Becker 28.01.2019 16:11

Если бы на выходе был только defined $var (условие, которое фактически не удалось), а не весь блок, в котором выполняется утверждение, это было бы приемлемо.

U. Windl 28.01.2019 22:14

Мне удалось немного изменить вывод, но все же условие должно быть передано как ссылка на КОД. Больше всего оптимизировано условие, поэтому его может быть трудно распознать: { 1 < 2 && 7 == 8 } становится Assertion failed: !1 at /tmp/c.pl line 26..

U. Windl 28.01.2019 23:15

В CPAN есть множество модулей утверждений. Они с открытым исходным кодом, так что довольно легко заглянуть в них и посмотреть, как они сделаны.

Карп::Утвердить — реализация с низким уровнем магии. В его документации есть ссылки на несколько более сложных модулей утверждений, одним из которых является мой модуль PerlX::Утвердить.

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

U. Windl 28.01.2019 22:16
Ответ принят как подходящий

Использовать caller и извлечь строку исходного кода, которая сделала утверждение?

sub assert {
    my ($condition, $msg) = @_;
    return if $condition;
    if (!$msg) {
        my ($pkg, $file, $line) = caller(0);
        open my $fh, "<", $file;
        my @lines = <$fh>;
        close $fh;
        $msg = "$file:$line: " . $lines[$line - 1];
    }
    die "Assertion failed: $msg";
}

assert(2 + 2 == 5);

Вывод:

Assertion failed:  assert.pl:14: assert(2 + 2 == 5);

Если вы используете Carp::croak вместо die, Perl также сообщит информацию о трассировке стека и определит, где было вызвано ошибочное утверждение.

Кажется, полезность caller() недооценена ;-) Мне нравится подход к получению исходной строки, даже если он может показаться немного неэффективным. Однако есть небольшая проблема, если условие не помещается в одну строку; то выводится только первая строка.

U. Windl 28.01.2019 22:39

Возможно, caller() можно было бы улучшить: если я печатаю анонимную подпрограмму (ссылка на код), я получаю вывод, показывающий диапазон строк, а не только первую строку, например «CODE(0x24b57e0) -> &main::__ANON__[/tmp/t.pl:13] in /tmp/t.pl:8-13». Но когда вызов подпрограммы распределяется по нескольким строкам, я просто получаю номер первой строки.

U. Windl 30.01.2019 08:29

Одним из подходов к любым «утверждениям» является использование среды тестирования. Он не такой четкий, как assert в C, но несравненно более гибкий и управляемый, в то время как тесты по-прежнему можно свободно встраивать в код, как операторы assert.

Несколько очень простых примеров

use warnings;
use strict;
use feature 'say';

use Test::More 'no_plan';
Test::More->builder->output('/dev/null');

say "A few examples of tests, scattered around code\n";

like('may be', qr/(?:\w+\s+)?be/, 'regex');
cmp_ok('a', 'eq', 'a ', 'string equality');

my ($x, $y) = (1.7, 13);

cmp_ok($x, '==', $y, '$x == $y');

say "\n'eval' expression in a string so we can see the failing code\n";

my $expr = '$x**2 == $y';
ok(eval $expr, 'Quadratic') || diag explain $expr;  

# ok(eval $expr, $expr);

с выходом

A few examples of tests, scattered around code

#   Failed test 'string equality'
#   at assertion.pl line 19.
#          got: 'a'
#     expected: 'a '
#   Failed test '$x == $y'
#   at assertion.pl line 20.
#          got: 1.7
#     expected: 13

'eval' expression in a string so we can see the failing code

#   Failed test 'Quadratic'
#   at assertion.pl line 26.
# $x**2 == $y
# Looks like you failed 3 tests of 4.

Это просто разрозненные примеры, где последний прямо отвечает на вопрос.

Модуль Тест::Больше объединяет ряд инструментов; есть много вариантов того, как его использовать и как манипулировать выводом. См. Тест::Упряжь и Тест::Строитель (используемые выше), а также ряд руководств и сообщений SO.

Я не знаю, как приведенный выше eval считается «элегантным», но он действительно перемещает вас от единичных и индивидуально заботящихся об операторах assert в стиле C к более легко управляемой системе.

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

На самом деле я не согласен: утверждения служат цели документирования и являются частью кода, в то время как тестовые примеры всегда являются внешними по отношению к коду. Прежде всего, вы не можете проверить некоторые внутренние детали с помощью внешних тестов. (Извините, я также занимался программированием Eiffel, где утверждения на самом деле являются частью языка. Я также играл с JUnit, но в исходниках все же есть утверждения.)

U. Windl 28.01.2019 23:32

@ У.Виндл Хам? В приведенном выше примере является ваша программа (на что указывает несвязанный say ...), и тесты являются ее частью. Я не показываю отдельный (внешний) набор тестов, а тестирую в вашем коде. Этого вполне достаточно для всеassert (которым я пользовался и любил много лет), и даже намного больше, если это необходимо.

zdim 28.01.2019 23:38

Но резюмируя наиболее востребованную функцию: ни один Test:: не может воспроизвести тестовое условие в виде строки; вместо этого вам нужно указать строку для каждого условия теста. Таким образом, в основном вы заменяете assert($a < $b) чем-то вроде assert_less($a, $b), чтобы assert_less() знал все, что ему нужно (операнды и операторы). Таким образом, легко вывести "$a < $b failed" (но все же $a и $b уже оцениваются).

U. Windl 29.01.2019 00:12

@U.Windl Re "не может воспроизвести тестовое условие в виде строки" - добавлен пример этого: установите условие, а затем используйте его как для тестирования, так и для отчета. Он использует eval, поскольку просто так получить то, что вы хотите, невозможно; здесь всего две чистые линии. Это сложная система, которую можно сложить как угодно. (Я также отредактировал ответ.)

zdim 29.01.2019 08:13

@U.Windl В конце концов, это может означать, что вы просто как. Тем не менее, возможно, стоит пожертвовать некоторыми из «элегантных» ради некоторых из «лучших». В современном языке сценариев высокого уровня нам не нужно довольствоваться (старыми добрыми) утверждениями в стиле C. Вы можете использовать немного меньше этого (не так чисто), но гораздо больше другого (тесты как часть управляемой системы). Просто предложил вариант.

zdim 29.01.2019 08:14

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