Pragmatic Perl 21

pragmaticperl.com

Выпуск 21. Ноябрь 2014

Другие выпуски и форматы журнала всегда можно загрузить с pragmaticperl.com. С вопросами и предложениями пишите на почту .

Комментарии к каждой статье есть в html-версии. Подписаться на новые выпуски можно по ссылке pragmaticperl.com/subscribe.

Авторы статей: Олеся Кузьмина, Дмитрий Шаматрин, Владимир Леттиев

Обложка: Марко Иванык

Корректор: Андрей Шитов

Выпускающий редактор: Вячеслав Тихановский

Ревизия: 2014-11-29 16:25

© «Pragmatic Perl»

От редактора

В прошлом месяце модулю DBI исполнилось 20 лет! Уверен, что каждый из Perl-программистов когда-либо использовал эту библиотеку.

Объявлена ежегодная номинация на награду White Camel за вклад в развитие Perl-сообщества. Предложить свою кандидатуру можно прямо брайану ди фою.

Традиционно в конце года проходит Perl-воркшоп Saint Perl в Санкт-Петербурге. В этом году это шестое мероприятие, и в название закралась подстрока «Perl 6» (нельзя здесь не провести параллель с недавним анонсом о том, что на конференции FOSDEM в начале следующего года будет объявлено, что Perl 6 появится в продакшене в 2015 году). Дата санкт-петербургского воркшопа выбирается максмально близкой ко дню рождения перла — 18 декабря. В планах организаторов на этот год — проведение не только дня с докладами, но и хакатона. Мероприятие пройдет 20 и 21 декабря 2014 года. Сайт конференции: event.yapcrussia.org/saintperl6/.

Для каждой статьи в интернет-версии журнала есть комментарии. Авторы постоянно следят за ними. Присоединяйтесь к обсуждению интересующих вас материалов. Комментарии можно оставлять без регистрации.

Друзья, журнал ищет новых авторов. Не упускайте такой возможности! Если у вас есть идеи или желание помочь, пожалуйста, свяжитесь с нами.

Приятного чтения.

■ Вячеслав Тихановский

Тестирование в Perl. Практика

Следущая статья из цикла «Тестирование в Perl». На этот раз практические рекомендации и примеры

В прошлой статье Тестирование в Perl. Лучшие практики были перечислены особенности юнит-тестирования в Perl, как стоит писать и группировать тесты и т.д. Однако тем, кто не применял тестирование и не сталкивался с проблемами, трудно оценить и хорошие практики. На этот раз постараемся на практических примерах рассмотреть, как же тестировать классы.

В качестве задачи возьмем модуль логирования, который может логировать в stderr и в файл. Модуль должен поддерживать уровни логирования: error, warn и debug, а в качестве формата: <дата> <уровень> <сообщение>.

Начальная структура модуля:

lib/
    Logger.pm
t/
    logger.t

Где lib директория с модулем, а t — директория с тестами.

Изначально модуль выглядит следующим образом:

package Logger;
use strict;
use warnings;

sub new {
    my $class = shift;

    my $self = {};
    bless $self, $class;

    return $self;
}

1;

А тест следующим образом:

use strict;
use warnings;

use Test::More;
use Logger;

subtest 'creates correct object' => sub {
    isa_ok(Logger->new, 'Logger');
};

done_testing;

Запускаем тесты с помощью prove:

$ prove t
t/logger.t .. ok
All tests successful.

На данный момент все тесты проходят. Но на самом-то деле особо ничего и не тестируется.

Вначале реализуем установку уровней логирования. Например, нам нужно, чтобы по умолчанию уровень был error. Пишем тест:

subtest 'has default log level' => sub {
    my $logger = Logger->new;

    is $logger->level, 'error';
};

Запускаем тесты.

$ prove t
t/logger.t .. 1/? Can't locate object method "level" via package "Logger"

Как видим, у логгера нет такого метода. Добавляем метод:

package Logger;
use strict;
use warnings;

sub new {
    my $class = shift;

    my $self = {};
    bless $self, $class;

    return $self;
}

sub level {
    my $self = shift;

    return 'error';
}

1;

Конечно, мы не хотим, чтобы метод level всегда возвращал одно и тоже значение, нам нужно, чтобы оно как-то сохранялось. Напишем тест, который будет проверять, что устанавливаемое значения сохраняется.

subtest 'sets log level' => sub {
    my $logger = Logger->new;

    $logger->set_level('debug');

    is $logger->level, 'debug';
};

Запускаем тесты:

$ prove t
t/logger.t .. 1/? Can't locate object method "set_level" via package "Logger"

Снова нет нужного метода, добавляем:

sub set_level {
        my $self = shift;
}

Теперь метод есть, но тесты все еще не проходят:

t/logger.t .. 1/?
    #   Failed test at t/logger.t line 22.
    #          got: 'error'
    #     expected: 'debug'
    # Looks like you failed 1 test of 1.

Теперь метод level возвращает неправильное значение. Самое время реализовать сохранение.

ckage Logger;
use strict;
use warnings;

sub new {
    my $class = shift;

    my $self = {};
    bless $self, $class;

    return $self;
}

sub set_level {
    my $self = shift;
    my ($new_level) = @_;

    $self->{level} = $new_level;
}

sub level {
    my $self = shift;

    return $self->{level};
}

1;

Запускаем тесты:

$ prove
t/logger.t .. 1/?
    #   Failed test at t/logger.t line 14.
    #          got: undef
    #     expected: 'error'
    # Looks like you failed 1 test of 1.

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

sub level {
    my $self = shift;

    return $self->{level} || 'error';
}

Теперь тесты проходят.

Далее необходимо проверить, что можно установить только разрешенные уровни логирования. Если устанавливается неизвестный уровень логирования, будем бросать исключение. Для проверок исключений воспользуемся модулем Test::Fatal. Новый тест будет выглядеть следующим образом:

subtest 'throws exception when invalid log level' => sub {
    my $log = Logger->new;

    ok exception { $log->set_level('unknown') };
};

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

subtest 'not throws when known log level' => sub {
    my $log = Logger->new;

    for my $level (qw/error warn debug/) {
        ok !exception { $log->set_level($level) };
    }
};

Если запустить тесты, то они ожидаемо завалятся:

$ prove t
t/logger.t .. 1/?
    #   Failed test at t/logger.t line 29.
    # Looks like you failed 1 test of 1.

Но второй тест проходит и без какого либо кода. В данном случае мы знаем, почему так происходит, но часто бывает иначе. Пишется тест, пишется код. Запускается тест и проходит. Программист думает, что все отлично. Но на самом деле его функционал неправильный. Поэтому всегда важно перед написанием кода запустить тест и убедиться, что он не проходит.

Реализуем проверку списка уровней:

package Logger;
use strict;
use warnings;

use Carp qw(croak);
use List::Util qw(first);

sub new {
    my $class = shift;

    my $self = {};
    bless $self, $class;

    return $self;
}

sub set_level {
    my $self = shift;
    my ($new_level) = @_;

    croak('Unknown log level')
      unless first { $new_level eq $_ } qw/error warn debug/;

    $self->{level} = $new_level;
}

sub level {
    my $self = shift;

    return $self->{level} || 'error';
}

1;

В качестве исключений бросается croak из модуля Carp, потому как он логичнее указывает на причину ошибки. Для поиска в списке используется first из модуля List::Util, который в отличие от обычного grep не будет дальше бежать по списку, а остановится при первом совпадении.

Далее напишем тест для метода log. Он должен печатать в stderr. Так как тестирование автоматическое, нужно как-то перехватить это вывод. Воспользуемся модулем Capture::Tiny, который отлично справляется с этой задачей.

use Capture::Tiny qw(capture_stderr);

subtest 'prints to stderr' => sub {
    my $log = Logger->new;

    my $stderr = capture_stderr {
        $log->log('error', 'message');
    };

    ok $stderr;
};

Запускаем тест и убеждаемся, что он не проходит, и реализуем данный функционал:

sub log {
    my $self = shift;
    my ($level, $message) = @_;

    print STDERR $message;
}

Теперь тесты проходят, но, конечно, это не то, что нам нужно, нам нужно получить отформатированный вывод, для этого напишем соответствующий тест:

subtest 'prints formatted line' => sub {
    my $log = Logger->new;

    my $stderr = capture_stderr {
        $log->log('error', 'message');
    };

    like $stderr, qr/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \[error\] message/;
};

Тесты не проходят:

$ prove t
t/logger.t .. 1/?
    #   Failed test at t/logger.t line 58.
    #                   'message'
    #     doesn't match '(?^:\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \[error\] message)'

Для форматирования воспользуемся Time::Piece. Код будет выглядеть следующим образом:

use Time::Piece;

sub log {
    my $self = shift;
    my ($level, $message) = @_;

    my $time = Time::Piece->new->strftime('%Y-%m-%d %T');
    print STDERR $time, " [$level] ", $message;
}

Теперь тесты проходят.

Понятно, что здесь тоже нужно реализовать проверку того, что передается правильный level, но мы переложим эту проверку на компилятор, реализовав отдельные методы для каждого уровня логирования. В итоге мы получим модифицированные тесты:

subtest 'prints to stderr' => sub {
    my $log = Logger->new;

    my $stderr = capture_stderr {
        $log->error('message');
    };

    ok $stderr;
};

subtest 'prints formatted line' => sub {
    my $log = Logger->new;

    my $stderr = capture_stderr {
        $log->error('message');
    };

    like $stderr, qr/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \[error\] message/;
};

Не нужно забывать, что нужно проверить все комбинации, поэтому тесты еще раз модифицируем:

subtest 'prints to stderr' => sub {
    my $log = Logger->new;

    for my $level (qw/error warn debug/) {
        my $stderr = capture_stderr {
            $log->$level('message');
        };

        ok $stderr;
    }
};

subtest 'prints formatted line' => sub {
    my $log = Logger->new;

    for my $level (qw/error warn debug/) {
        my $stderr = capture_stderr {
            $log->$level('message');
        };

        like $stderr, qr/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \[$level\] message/;
    }
};

Реализуем подобный фукнционал, спрятав метод log:

sub error { shift->_log('error', @_) }
sub warn  { shift->_log('warn',  @_) }
sub debug { shift->_log('debug', @_) }

sub _log {
    my $self = shift;
    my ($level, $message) = @_;

    my $time = Time::Piece->new->strftime('%Y-%m-%d %T');
    print STDERR $time, " [$level] ", $message;
}

Глядя на код, я заметил ошибку — отсутствие перевода строки. Чтобы убедиться в том, что это ошибка, вначале напишем тест:

subtest 'prints to stderr with \n' => sub {
    my $log = Logger->new;

    for my $level (qw/error warn debug/) {
        my $stderr = capture_stderr {
            $log->$level('message');
        };

        like $stderr, qr/\n$/;
    }
};

Теперь запускаем тесты, чтобы убедиться, что эта ошибка воспроизводится. Теперь исправляем:

print STDERR $time, " [$level] ", $message, "\n";

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

Для чего же вообще реализовывали уровни логирования? Конечно, чтобы при выставленном уровне логировать только те события, уровень которых выше текущего. Т.е. в режиме debug должны логироваться error, warn и debug. В режиме warn — error и warn, а в режиме error только error. Чтобы упростить тесты, составим таблицу ожидаемых значений на тестовые данные. Например, тест на то, что все логируется при нужном уровне, будет выглядеть следующим образом:

subtest 'logs when level is higher' => sub {
    my $log = Logger->new;

    my $levels = {
        error => [qw/error/],
        warn  => [qw/error warn/],
        debug => [qw/error warn debug/],
    };

    for my $level (keys %$levels) {
        $log->set_level($level);
        for my $test_level (@{$levels->{$level}}) {
            ok capture_stderr {
                $log->$test_level('message');
            };
        }
    }
};

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

subtest 'not logs when level is lower' => sub {
    my $log = Logger->new;

    my $levels = {
        error => [qw/warn debug/],
        warn  => [qw/debug/],
    };

    for my $level (keys %$levels) {
        $log->set_level($level);
        for my $test_level (@{$levels->{$level}}) {
            ok !capture_stderr {
                $log->$test_level('message');
            };
        }
    }
};

Здесь нет проверки debug, потому как при этом режиме все логируется. Убеждаемся, что тесты не проходят и пишем реализацию. Во-первых, каждому уровню присваиваем свое значение, во-вторых, перед логированием проверяем, допускает ли текущий уровень логирование.

Во время реализации столкнулся с тем, что при запуске тестов непонятно, что ломается, поэтому добавил сообщения:

ok !capture_stderr {
    $log->$test_level('message');
}, "not log '$test_level' when '$level'";

Теперь сообщения стали понятнее:

    #   Failed test 'not log 'warn' when 'error''
    #   at t/logger.t line 109.

После реализации оказалось, что ломаются многие старые тесты, как, например, вот этот:

subtest 'prints to stderr' => sub {
    my $log = Logger->new;

    for my $level (qw/error warn debug/) {
        my $stderr = capture_stderr {
            $log->$level('message');
        };

        ok $stderr;
    }
};

Понятно, что нужно выставить самый высокий уровень логирования debug. Также и в других тестах.

Измененный код:

my $LEVELS = {
    error => 1,
    warn  => 2,
    debug => 3
};

...

sub set_level {
    my $self = shift;
    my ($new_level) = @_;

    croak('Unknown log level')
      unless first { $new_level eq $_ } keys %$LEVELS;

    $self->{level} = $new_level;
}

sub _log {
    my $self = shift;
    my ($level, $message) = @_;

    return unless $LEVELS->{$level} <= $LEVELS->{$self->level};

    my $time = Time::Piece->new->strftime('%Y-%m-%d %T');
    print STDERR $time, " [$level] ", $message, "\n";
}

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

sub _build_logger {
    my $logger = Logger->new;
    $logger->set_level('debug');
    return $logger;
}

На данном этапе логгер полностью протестирован. Чтобы в этом убедиться, запустим Devel::Cover:

$ PERL5OPT=-MDevel::Cover prove t
----------------------------------- ------ ------ ------ ------ ------ ------
File                                  stmt   bran   cond    sub   time  total
----------------------------------- ------ ------ ------ ------ ------ ------
lib/Logger.pm                        100.0  100.0  100.0  100.0    3.4  100.0

Теперь необходимо реализовать логгер, который будет логировать в файл. Чтобы избежать дублирования, в данном случае воспользуемся шаблонными методами, но все зависит от конкретной задачи, и существует большое количество способов. Вначале переименуем логгер в LoggerStderr. Теперь создадим LoggerFile, который будет копией LoggerStderr, также скопируем тест. Теперь директория выглядит следующим образом:

lib/
    LoggerFile.pm
    LoggerStderr.pm
t/
    logger_file.t
    logger_stderr.t

В logger_file.t подправим тесты, которые проверяют, что лог записался в stderr на проверку, что запись была произведена в файл. Вместо Capture::Tiny, напишем собственную функцию, которая будет читать из файла:

sub _slurp {
    my $file = shift;
    return do { local $/; open my $fh, '<', $file or die $!; <$fh> };
}

Для тестирования записи в файл, будет создавать временные файлы с помощью File::Temp, и тесты будут выглядит следущим образом:

subtest 'prints to file' => sub {
    my $file = File::Temp->new;
    my $log = _build_logger(file => $file->filename);

    for my $level (qw/error warn debug/) {
        $log->$level('message');

        my $content = _slurp($file->filename);

        ok $content;
    }
};

Как видно в конструктор передается имя файла и _build_logger приобретает следующий вид:

sub _build_logger {
    my $logger = LoggerFile->new(@_);
    $logger->set_level('debug');
    return $logger;
}

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

sub _log {
    my $self = shift;
    my ($level, $message) = @_;

    return unless $LEVELS->{$level} <= $LEVELS->{$self->level};

    my $time = Time::Piece->new->strftime('%Y-%m-%d %T');

    open my $fh, '>>', $self->{file} or die $!;
    print $fh $time, " [$level] ", $message, "\n";
    close $fh;
}

И тесты, и реализации содержат много дублирования. Вначале избавимся от дублирования в реализациях, выделив базовый класс с шаблонным методом _print, который будет реализован в LoggerFile и LoggerStderr. Во время рефакторинга постоянно запускаем тесты, чтобы убедиться, что ничего не сломалось.

Базовый класс:

package LoggerBase;
use strict;
use warnings;

use Carp qw(croak);
use List::Util qw(first);
use Time::Piece;

my $LEVELS = {
    error => 1,
    warn  => 2,
    debug => 3
};

sub new {
    my $class = shift;
    my (%params) = @_;

    my $self = {};
    bless $self, $class;

    $self->{file} = $params{file};

    return $self;
}

sub set_level {
    my $self = shift;
    my ($new_level) = @_;

    croak('Unknown log level')
      unless first { $new_level eq $_ } keys %$LEVELS;

    $self->{level} = $new_level;
}

sub level {
    my $self = shift;

    return $self->{level} || 'error';
}

sub error { shift->_log('error', @_) }
sub warn  { shift->_log('warn',  @_) }
sub debug { shift->_log('debug', @_) }

sub _log {
    my $self = shift;
    my ($level, $message) = @_;

    return unless $LEVELS->{$level} <= $LEVELS->{$self->level};

    my $time = Time::Piece->new->strftime('%Y-%m-%d %T');

    my $text = join '', $time, " [$level] ", $message, "\n";

    $self->_print($text);
}

sub _print { ... }

1;

LoggerStderr:

package LoggerStderr;
use strict;
use warnings;

use base 'LoggerBase';

sub _print {
    my $self = shift;
    my ($message) = @_;

    print STDERR $message;
}

1;

LoggerFile:

package LoggerFile;
use strict;
use warnings;

use base 'LoggerBase';

sub new {
    my $self = shift->SUPER::new(@_);
    my (%params) = @_;

    $self->{file} = $params{file};

    return $self;
}

sub _print {
    my $self = shift;
    my ($message) = @_;

    open my $fh, '>>', $self->{file} or die $!;
    print $fh $message;
    close $fh;
}

1;

Так как появился новый класс, его тоже нужно тестировать. Как тестировать базовые классы? В самом тесте создается тестовый класс наследующий тестируемый базовый класс. А из тестов наследников удаляются дублирующие тесты. Некоторые тесты все равно останутся продублированными, но обо всем по порядку. Вначале напишем тест на базовый класс:

use strict;
use warnings;

use Test::More;
use Test::Fatal;

subtest 'creates correct object' => sub {
    isa_ok(LoggerTest->new, 'LoggerTest');
};

subtest 'has default log level' => sub {
    my $logger = LoggerTest->new;

    is $logger->level, 'error';
};

subtest 'sets log level' => sub {
    my $logger = LoggerTest->new;

    $logger->set_level('debug');

    is $logger->level, 'debug';
};

subtest 'not throws when known log level' => sub {
    my $log = LoggerTest->new;

    for my $level (qw/error warn debug/) {
        ok !exception { $log->set_level($level) };
    }
};

subtest 'throws exception when invalid log level' => sub {
    my $log = LoggerTest->new;

    ok exception { $log->set_level('unknown') };
};

sub _build_logger {
    my $logger = LoggerTest->new(@_);
    $logger->set_level('debug');
    return $logger;
}

done_testing;

package LoggerTest;
use base 'LoggerBase';

sub _print { }

Тесты, выделенные в базом тесте, удаляем из других файлов.

На данный момент и в logger_stderr.t, и в logger_file.t остались тесты, которые практически одинаковы, но сильно завязаны на реализацию. Например, в тесте:

subtest 'not logs when level is lower' => sub {
    my $log = _build_logger();

    my $levels = {
        error => [qw/warn debug/],
        warn  => [qw/debug/],
    };

    for my $level (keys %$levels) {
        $log->set_level($level);
        for my $test_level (@{$levels->{$level}}) {
            ok !capture_stderr {
                $log->$test_level('message');
            }, "not log '$test_level' when '$level'";
        }
    }
};

на самом деле нужно проверить, что метод _print не вызывается, а не проверять, была ли запись в stderr. Этот тест можно перенести в базовый, немного модифицировав базовый тестовый класс.

package LoggerTest;
use base 'LoggerBase';

sub new {
    my $self = shift->SUPER::new(@_);
    my (%params) = @_;

    $self->{output} = $params{output};

    return $self;
}

sub _print {
    my $self = shift;

    push @{$self->{output}}, @_;
}

Таким образом, передавая в конструктор массив $output мы сможем дальше в тесте проверить, что в него записалось. Теперь тест на проверку правильности форматировния выглядит следующим образом:

subtest 'prints formatted line' => sub {
    my $output = [];
    my $log = _build_logger(output => $output);

    for my $level (qw/error warn debug/) {
        $log->$level('message');

        like $output->[-1], qr/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \[$level\] message/;
    }
};

и их можно удалить из дочерних классов. Также поступаем с тестами, которые не тестируют саму реализацию записи, а внутреннее поведение, реализованное в базовом классе. Финальные тесты выглядят следущим образом:

logger_base.t:

use strict;
use warnings;

use Test::More;
use Test::Fatal;

subtest 'creates correct object' => sub {
    isa_ok(LoggerTest->new, 'LoggerTest');
};

subtest 'has default log level' => sub {
    my $logger = LoggerTest->new;

    is $logger->level, 'error';
};

subtest 'sets log level' => sub {
    my $logger = LoggerTest->new;

    $logger->set_level('debug');

    is $logger->level, 'debug';
};

subtest 'not throws when known log level' => sub {
    my $log = LoggerTest->new;

    for my $level (qw/error warn debug/) {
        ok !exception { $log->set_level($level) };
    }
};

subtest 'throws exception when invalid log level' => sub {
    my $log = LoggerTest->new;

    ok exception { $log->set_level('unknown') };
};

subtest 'prints formatted line' => sub {
    my $output = [];
    my $log = _build_logger(output => $output);

    for my $level (qw/error warn debug/) {
        $log->$level('message');

        like $output->[-1],
          qr/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \[$level\] message/;
    }
};

subtest 'logs when level is higher' => sub {
    my $output = [];
    my $log = _build_logger(output => $output);

    my $levels = {
        error => [qw/error/],
        warn  => [qw/error warn/],
        debug => [qw/error warn debug/],
    };

    for my $level (keys %$levels) {
        $log->set_level($level);
        for my $test_level (@{$levels->{$level}}) {
            $log->$test_level('message');

            ok $output->[-1];
        }
    }
};

subtest 'not logs when level is lower' => sub {
    my $output = [];
    my $log = _build_logger(output => $output);

    my $levels = {
        error => [qw/warn debug/],
        warn  => [qw/debug/],
    };

    for my $level (keys %$levels) {
        $log->set_level($level);
        for my $test_level (@{$levels->{$level}}) {
            $log->$test_level('message');

            ok !$output->[-1], "not log '$test_level' when '$level'";
        }
    }
};

sub _build_logger {
    my $logger = LoggerTest->new(@_);
    $logger->set_level('debug');
    return $logger;
}

done_testing;

package LoggerTest;
use base 'LoggerBase';

sub new {
    my $self = shift->SUPER::new(@_);
    my (%params) = @_;

    $self->{output} = $params{output};

    return $self;
}

sub _print {
    my $self = shift;

    push @{$self->{output}}, @_;
}

logger_stderr.t:

use strict;
use warnings;

use Test::More;
use Test::Fatal;
use Capture::Tiny qw(capture_stderr);
use LoggerStderr;

subtest 'creates correct object' => sub {
    isa_ok(LoggerStderr->new, 'LoggerStderr');
};

subtest 'prints to stderr' => sub {
    my $log = _build_logger();

    for my $level (qw/error warn debug/) {
        my $stderr = capture_stderr {
            $log->$level('message');
        };

        ok $stderr;
    }
};

subtest 'prints to stderr with \n' => sub {
    my $log = _build_logger();

    for my $level (qw/error warn debug/) {
        my $stderr = capture_stderr {
            $log->$level('message');
        };

        like $stderr, qr/\n$/;
    }
};

sub _build_logger {
    my $logger = LoggerStderr->new;
    $logger->set_level('debug');
    return $logger;
}

done_testing;

logger_file.t:

use strict;
use warnings;

use Test::More;
use Test::Fatal;
use File::Temp;
use LoggerFile;

subtest 'creates correct object' => sub {
    isa_ok(LoggerFile->new, 'LoggerFile');
};

subtest 'prints to file' => sub {
    my $file = File::Temp->new;
    my $log = _build_logger(file => $file->filename);

    for my $level (qw/error warn debug/) {
        $log->$level('message');

        my $content = _slurp($file);

        ok $content;
    }
};

subtest 'prints to stderr with \n' => sub {
    my $file = File::Temp->new;
    my $log = _build_logger(file => $file);

    for my $level (qw/error warn debug/) {
        $log->$level('message');

        my $content = _slurp($file);

        like $content, qr/\n$/;
    }
};

sub _slurp {
    my $file = shift;
    my $content = do { local $/; open my $fh, '<', $file->filename or die $!; <$fh> };
    return $content;
}

sub _build_logger {
    my $logger = LoggerFile->new(@_);
    $logger->set_level('debug');
    return $logger;
}

done_testing;

В завершение реализуем фабрику, которая создает тот или иной логгер. Нужно протестировать, что возращается нужный объект, и что при неизвестном запрашиваемом логгере бросается ошибка. Тест может выглядеть следующим образом:

use strict;
use warnings;

use Test::More;
use Test::Fatal;
use Logger;

subtest 'creates stderr logger' => sub {
    my $logger = Logger->build('stderr');

    isa_ok $logger, 'LoggerStderr';
};

subtest 'creates file logger' => sub {
    my $logger = Logger->build('file');

    isa_ok $logger, 'LoggerFile';
};

subtest 'throws when unknown logger' => sub {
    ok exception { Logger->build('unknown') };
};

done_testing;

И сама фабрика:

package Logger;

use strict;
use warnings;

use Carp qw(croak);
use LoggerStderr;
use LoggerFile;

sub build {
    my $class = shift;
    my ($type, @args) = @_;

    if ($type eq 'stderr') {
        return LoggerStderr->new(@args);
    } elsif ($type eq 'file') {
        return LoggerFile->new(@args);
    }

    croak('Unknown type');
}

1;

Таким образом мы написали с помощью TDD-методологии простейший логгер. Надеюсь, что процесс был понятным, а если нет — спрашивайте в комментариях!

■ Вячеслав Тихановский

GUI-приложения на Perl с помощью wxWidgets

Рассмотрены основы написания GUI-приложений с помощью wxWidgets

Написание GUI-приложений на Perl ничем не отличается от написания на других динамических языках. Все зависит от качества оберток над оригинальными библиотеками и их своевременного обновления.

Почему wxWidgets?

wxWidgets является по-настоящему кросс-платформенным GUI-инструментарием. В отличие от, например, Qt, который тоже вроде бы является кросс-платформенным, wxWidget на запускаемых платформах использует родные библиотеки, а не пытается их эмулировать. Таким образом приложения выглядят не cовсем одинаково, но совершенно привычно для своей графической оболочки.

Более того, поддержка у wxWidgets для Perl является полной, качественной и современной. Например, популярный редактор Padre тоже написан с помощью wxWidgets. Имея под рукой большой открытый и свободный проект, всегда можно подглядеть, как реализуются те или иные виджеты, можно на конкретном примере быстро научиться.

Настройка окружения

Для разработки я использую платформу GNU/Linux. Поэтому дальше приведена установка wxWidgets именно для моей ОС. Проще всего устанавливать библиотеки из дистрибутивных пакетов, на примере Debian:

# apt-get install libwx-perl

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

$ cpanm Wx

Первое приложение

Стандартным первым приложением будет окно на котором написано “Hello, world!”:

use strict;
use warnings;

use Wx;

my $app = Wx::SimpleApp->new;
my $frame = Wx::Frame->new(undef, -1, "Hello, world!");
$frame->Show;
$app->MainLoop;

Wx::Frame это окно, у которого может быть изменен размер, у него может быть статусная строка, а также меню. Т.е. это окно общего назначения. Кроме Wx::Frame существует также Wx::Dialog и другие. Для добавления визуальных элементов можно воспользоваться различными классами и методами wxWidgets. Например, чтобы добавить кнопку, которая закрывает приложение, напишем следующий код:

use strict;
use warnings;

use Wx;
use Wx::Event;

my $app    = Wx::SimpleApp->new;
my $frame  = Wx::Frame->new(undef, -1, "Hello, world!");

my $button = Wx::Button->new($frame, -1, 'Close');
Wx::Event::EVT_BUTTON($frame, $button, sub { $frame->Close(1) });

$frame->Show;
$app->MainLoop;

Все методы, события и т.д. хорошо описаны в документации wxWidgets. Не будем на этом подробно останавливаться. Рассмотрим общий подход в написании GUI-приложений.

Генераторы XML-интерфейса

Для небольшого приложения это вполне допустимо, также как и для простого Perl-скрипта можно все записать в один файл. Однако это очень неудобно для больших приложений с большим количеством окон, диалогов, сложной логикой.

Для графических приложений применяют тот же подход, что и для веб-приложений — MVC, или разделение бизнес-логики, отображения и управления. Так же можно поступить и с GUI-программой.

Для построения интерфейса лучше воспользоваться специальными программами, генерирующими XML-настройки (XRC-файлы), которые, будучи загруженными в приложении, преобразуются в графический интерфейс. Это гораздо легче поддерживать и проще изменять.

Для wxWidgets существует несколько таких генераторов. Наиболее полным является wxFormBuilder. Он позволяет создавать всевозможные элементы, указывать их расположение на окнах и т.д.

Например, XRC-файл для предыдущего приложения может выглядеть так:

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<resource xmlns="http://www.wxwindows.org/wxxrc" version="2.3.0.1">
    <object class="wxFrame" name="MainFrame">
        <style>wxDEFAULT_FRAME_STYLE|wxTAB_TRAVERSAL</style>
        <size>500,300</size>
        <title></title>
        <centered>1</centered>
        <aui_managed>0</aui_managed>
        <object class="wxButton" name="CloseButton">
            <label>Close</label>
            <default>0</default>
        </object>
    </object>
</resource>

Теперь загрузим эту конфигурацию в приложении:

use strict;
use warnings;

use Wx;
use Wx::XRC;
use Wx::Event;

my $app = Wx::SimpleApp->new;

my $xrc = Wx::XmlResource->new();
$xrc->InitAllHandlers();
$xrc->Load('example.xrc');

my $frame = Wx::Frame->new;
$xrc->LoadFrame($frame, undef, 'MainFrame');

Wx::Event::EVT_BUTTON(
    $frame,
    Wx::XmlResource::GetXRCID('CloseButton'),
    sub { $frame->Close(1) }
);

$frame->Show;
$app->MainLoop;

Для того, чтобы достать объект Wx::Frame, используем метод LoadFrame:

my $frame = Wx::Frame->new;
$xrc->LoadFrame($frame, undef, 'MainFrame');

А для того, чтобы достать объект кнопки, вначале получаем ее id по имени:

Wx::XmlResource::GetXRCID('CloseButton')

А затем привязываем событие.

Всегда стоит понятным образом называть все элементы, а не оставлять что-то вроде m_Button_1, это сделает управляющий код существенно понятнее.

Разделение на классы

Как и любое Perl-приложение, разделение на отдельные пакеты и классы упрощает понимание кода, делает возможным тестирование отдельных классов, а также позволяет легко наследовать и менять поведение графических элементов.

Разделим наше приложение на несколько составляющих.

Основной класс приложения

package MyApp;

use strict;
use warnings;

use base 'Wx::SimpleApp';

use Wx;
use Wx::XRC;
use MyMainFrame;

sub OnInit {
    my $self = shift;

    my $xrc = Wx::XmlResource->new();
    $xrc->InitAllHandlers();
    $xrc->Load('app.xrc');

    my $frame = MyMainFrame->new;
    $xrc->LoadFrame($frame, undef, 'MainFrame');

    $frame->Show;
}

1;

Задача основого класса выполнить загрузку настроек, создать основное окно и показать его.

Класс основного окна

package MyMainFrame;

use strict;
use warnings;

use base 'Wx::Frame';

use Wx;
use Wx::Event;
use Wx::XRC;

sub new {
    my $self = shift->SUPER::new(@_);

    Wx::Event::EVT_BUTTON(
        $self,
        Wx::XmlResource::GetXRCID('CloseButton'),
        sub { $self->Close(1) }
    );

    return $self;
}

1;

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

Скрипт запуска

Скрипт запуска app.pl является входной точкой для запуска приложения.

#!/usr/bin/env perl

use strict;
use warnings;

use Wx;
use MyApp;

MyApp->new->MainLoop;

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

app.pl
app.xrc
lib/
    MyApp.pm
    MyMainFrame.pm

Можно у каждого класса сделать какой-то уникальный префикс, например GUI, чтобы их отличать от обычных Perl-классов, которые будут заниматься логикой приложения.

Генераторы Perl-кода

wxFormBuilder, как было уже указано, может генерировать XML-представление, однако он позволяет гененировать и код. К сожалению, из коробки нет поддержки генерации Perl-кода. Однако, разработчики Padre реализовали собственный генератор Perl-кода из файла проекта wxFormBuilder. Для этого потребуются два модуля: FBP и FBP::Perl. Написав простейший скрипт для создания классов, получим на выходе дерево файлов, где каждый — это отдельное окно:

#!/usr/bin/env perl

use strict;
use warnings;

my ($project_file, $root_dir) = @ARGV;
die "Usage: <project_file> <dir>\n" unless $project_file && $root_dir;
die "'$project_file' does not exist\n" unless -e $project_file;
die "'$root_dir' does not exist\n"     unless -d $root_dir;

use File::Slurp qw(write_file);
use File::Spec::Functions qw(catfile);
use File::Path qw(make_path);
use File::Basename qw(dirname);
use FBP;
use FBP::Perl;

my $fbp = FBP->new;
$fbp->parse_file($project_file);

my $project = $fbp->project;

my $generator = FBP::Perl->new(project => $project);

foreach my $form ($project->forms) {
    my $content = $generator->flatten($generator->frame_class($form));

    write_class($form->name, $content);
}

foreach my $dialog ($project->dialogs) {
    my $content = $generator->flatten($generator->dialog_class($dialog));

    write_class($dialog->name, $content);
}

sub write_class {
    my ($class_name, $content) = @_;

    my $path = join('/', split /::/, $class_name) . '.pm';
    $path = catfile($root_dir, $path);

    my $dir = dirname($path);
    make_path($dir);

    write_file $path, $content;
}

Плюс этого подхода еще и в том, что нет необходимости вручную привязывать сигналы к элементам. Указав в wxFormBuilder название обработчика, PBP::Perl правильно сгенерирует соответствующий код. Вот как, например, выглядит класс основной формы:

package MyApp::FBP::MainFrame;

use 5.008005;
use utf8;
use strict;
use warnings;
use Wx 0.98 ':everything';

our $VERSION = '0.01';
our @ISA     = 'Wx::Frame';

sub new {
    my $class  = shift;
    my $parent = shift;

    my $self = $class->SUPER::new(
        $parent,
        -1,
        '',
        wxDefaultPosition,
        [ 500, 300 ],
        wxDEFAULT_FRAME_STYLE | wxTAB_TRAVERSAL,
    );

    $self->{CloseButton} = Wx::Button->new(
        $self,
        -1,
        "Close",
        wxDefaultPosition,
        wxDefaultSize,
    );

    Wx::Event::EVT_BUTTON(
        $self,
        $self->{CloseButton},
        sub {
            shift->OnClick(@_);
        },
    );

    my $bSizer1 = Wx::BoxSizer->new(wxVERTICAL);
    $bSizer1->Add( $self->{CloseButton}, 0, wxALL, 5 );

    $self->SetSizer($bSizer1);
    $self->Layout;

    return $self;
}

sub OnClick {
    warn 'Handler method OnClick for event CloseButton.OnButtonClick not implemented';
}

1;

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

Вот так выглядит реализация обработчика события OnClick в дочернем классе:

package MyApp::MainFrame;

use strict;
use warnings;

use base 'MyApp::FBP::MainFrame';

sub OnClick {
    my $self = shift;

    $self->Close(1);
}

1;

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

app.pl
lib/
    MyApp/
        FBP/
            MainFrame.pm
        MainFrame.pm
    MyApp.pm
generate.pl
simple.fbp

simple.fbp это файл проекта wxFormBuilder, а generate.pl — скрипт для генерации.

Модуль MyApp.pm придется тоже написать вручную, но его код предельно прост:

package MyApp;

use strict;
use warnings;

use base 'Wx::SimpleApp';

use MyApp::MainFrame;

sub OnInit {
    my $self = shift;

    my $frame = MyApp::MainFrame->new;

    $frame->Show;
}

1;

А app.pl остается тем же, что и в случае использования XML-представления.

Разделение логики

Как уже упоминалось, при написании графических приложений разделяется собственно представление и логика самого приложения. Идеально, если приложение будет реализовано таким образом, что замена графической библиотеки другой, или же веб-приложением, или консольным вариантом не повлечет каких-либо или, как минимим, никаких серьезных изменений.

Например, если в приложении необходимо сделать запрос к веб-сайту для получения каких-то данных, стоит выделить это в отдельный класс, не зависящий от графической библиотеки. Это позволит гораздо проще его протестировать стандартными средствами. В случае тестирования графических программ это сделать гораздо сложнее.

Впрочем, тестировать графические интерфейсы тоже можно и нужно, как например, с помощью Selenium тестируют и веб-приложения. Для X11 есть набор инструментов Xnee, который позволяет записывать сценарии, а затем выполнять их в автоматическом режиме.

Блокировки

Если писать серьезные приложения на wxWidgets, в скором времени вы столкнетесь с тем, что при выполнении требующих времени задач активное окно блокируется и как будто замирает. Все дело в блокировке. Во время выполнения вашего кода wxWidgets ждет, пока он закончится.

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

Кроме потоков возможно использование асинхронных фреймворков по типу POE. На Linux возможно использование wxGTK, а вместе с ним AnyEvent. В этом случае придется писать событийно-ориентированный код со всеми преимуществами и недостатками последнего.

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

Выполнение задач в отдельном потоке

Самый распространенный и простой способ выполнить задачу в фоновом режиме — использовать потоки. Чтобы использовать потоки с Wx, необходимо, во-первых, убедиться, что perl собран с поддержкой threads:

perl -V | grep threads

А во-вторых, подключить прагму threads ДО подключения Wx (это стоит сделать также в скрипте запуска приложения):

use threads;
use Wx;

Например, при нажатии на кнопку, мы хотим выполнить какой-то код и оповестить главное приложение о результате. Скелет такого приложения будет выглядеть следующим образом:

package MyApp::MainFrame;

use strict;
use warnings;
use threads;
use threads::shared;

use base 'MyApp::FBP::MainFrame';

my $DONE_EVENT : shared = Wx::NewEventType;

sub OnClick {
    my $self = shift;

    Wx::Event::EVT_COMMAND($self, -1, $DONE_EVENT, \&done);

    threads->create(\&work, $self);
}

sub work {
    my $handler = shift;

    # do something

    my @result : shared = ...;

    my $threvent = Wx::PlThreadEvent->new(-1, $DONE_EVENT, \@result);
    Wx::PostEvent($handler, $threvent);

    threads->detach;
    threads->exit;
}

sub done {
    my $self = shift;
    my ($event) = @_;

    my $result = $event->GetData;

    # do something
    ...
}

1;

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

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

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

Пример приложения

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

Модель

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

package MyApp::ModulesFetcher;

use strict;
use warnings;

use JSON ();
use LWP::UserAgent;

my $METACPAN = 'http://api.metacpan.org/v0';

sub new {
    my $class = shift;

    my $self = {};
    bless $self, $class;

    return $self;
}

sub fetch {
    my $self = shift;

    my $ua = LWP::UserAgent->new;

    my $response = $ua->post(
        "$METACPAN/release/_search",
        Content => JSON::encode_json(
            {
                query => {
                    filtered => {
                        query  => {"match_all" => {}},
                        filter => {
                            term => {'release.authorized' => \1},
                        }
                    }
                },
                fields => ['distribution'],
                size => 10,
                from => 0,
                sort => [{date => 'desc'}],
            }
        )
    );

    return () unless $response->is_success;

    my $res = JSON::decode_json($response->decoded_content);

    my @distributions = @{$res->{hits}->{hits}};

    return map { $_->{fields}->{distribution} } @distributions;
}

1;

Более подробно о том, как работать с API MetaCPAN, можно почитать в документации. Там же есть множество примеров.

Отображение

Графическое приложение будет представлять собой одно единственное окно с кнопкой Fetch и списком. При нажатии на кнопку в отдельном потоке создаем объект класса ModulesFetcher и возвращаем результат. Главное графическое приложение обрабатывает результат и заполняет список.

Скелет всего приложения уже описывался нами выше, поэтому здесь покажем только пример использования стороннего модуля:

package MyApp::MainFrame;

use strict;
use warnings;
use threads;
use threads::shared;

use base 'MyApp::FBP::MainFrame';

use MyApp::ModulesFetcher;

my $DONE_EVENT : shared = Wx::NewEventType;

sub OnClick {
    my $self = shift;

    Wx::Event::EVT_COMMAND($self, -1, $DONE_EVENT, \&done);

    threads->create(\&work, $self);
}

sub work {
    my $handler = shift;

    my $fetcher = MyApp::ModulesFetcher->new;
    my @modules = $fetcher->fetch;

    my @result : shared = @modules;

    my $threvent = Wx::PlThreadEvent->new(-1, $DONE_EVENT, \@result);
    Wx::PostEvent($handler, $threvent);

    threads->detach;
    threads->exit;
}

sub done {
    my $self = shift;
    my ($event) = @_;

    my $modules = $event->GetData;

    my $list_ctrl = $self->{ModuleListCtrl};
    $list_ctrl->ClearAll;

    foreach my $module (reverse @$modules) {
        $list_ctrl->InsertStringItem(0, $module);
    }

}

1;

В методе work вызывается метод fetch на объекте модели, затем результат преобразуется в shared-переменную, и создается сигнал главному окну с передачей результата.

В методе done запрашиваем полученные данные у события с помощью метода GetData и затем в цикле добавляем их в объект списка. Перед этим не забываем очистить старые данные.

Выводы

Писать графические приложения на Perl можно вполне успешно. С помощью библиотеки wxWidgets это еще и просто. Правильно разделяя модель и отображение, можно добиться переносимости между различными графическими библиотеками, а также упростить реализацию.

Если читателям интересны подобные статьи, в планах есть написание аналогичного примера с помощью библиотеки Gtk. Жду ваших комментариев.

■ Олеся Кузьмина

Еще немного об асинхронном программировании на Anyevent

В журнале уже были статьи, посвященные асинхронному программированию на Perl. Статьи, бесспорно, весьма достойные и интересные, однако, к сожалению, они отвечают на вопрос “как?”, тогда как для лучшего понимания материала они должны отвечать на еще один вопрос “зачем?”.

У меня много спрашивают касательно AnyEvent, причем очень часто спрашивают такие вещи, которые, в принципе, на AnyEvent написать нельзя.

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

И да, если у вас возникают какие-либо вопросы, мне можно написать, совершенно беспроблемно, на электронную почту, и я постараюсь объяснить непонятные моменты и отвечу на все вопросы.

Итак, поехали.

Историческая справка

Для того, чтобы понять, что оно такое и с чем его едят, нам необходим теоретический минимум. Забегая вперед, скажу, что абсолютной асинхронности, как таковой, вообще не существует. Существует асинхронность относительная.

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

При классическом подходе к программированию обозначается цель, строится алгоритм, затем он декомпозируется на кусочки поменьше, и записывается реализация на определенном языке программирования. Как правило, приложение проектируется определенным образом. Я это написал лишь потому, что прежде чем я продолжу, я хотел бы ответить на вопрос: “Можно ли сделать так, чтобы мое приложение стало асинхронным без переписывания?”. Короткий ответ: “Нет”.

Если приложение проектировалось под синхронную модель, ничего с этим не сделаешь, асинхронным оно, увы, от замены пары строчек кода не станет.

Лирическое отступление

Синхронное программирование

Основой синхронного программирования есть одно простое слово: “Последовательность”. Все действия выполняются строго последовательно, в том порядке, в котором они записаны в исходном коде программы. В настольных приложениях, например, может использоваться многопоточный подход. Он используется для того, чтобы приложение могло делать несколько вещей одновременно. Например, один поток приложения считает сумму, а второй выводит надоедливые рекламные баннеры.

Процессор, например, двухъядерный, сферический и в вакууме, умеет одновременно выполнять две задачи и не задачей более. Но приложений запущено много. Как? А штука в том, что есть такой замечательный механизм — переключение контекста и очередь. Все задачи выстраиваются в очередь и процессор их обрабатывает по две одновременно. Программа, как правило, даже если выглядит так:

for (my $i = 0; $i <= 10; $i++) {
    print "OMFG!!111\n";
}

То вовсе не обязательно, но вероятно, что все итерации цикла будут выполнены в рамках одной задачи. А переключение происходит настолько быстро, что зрительно это увидеть вообще нереально. Если мы напишем приложение, которое будет просто ждать пять секунд, а потом печатать “Hello world”, например, вот так:

sleep 5;
print "Hello world!\n";

То мы просто ничего не делаем пять секунд, а строчка “Hello world” будет напечатана только после того, как эти самые пять секунд пройдут.

Асинхронное программирование

Самым важным и основой основ асинхронного программирование является понятие блокировки. Для того, чтобы объяснить, что такое блокировка, рассмотрим простейший пример приложения, которое получает при помощи GET-запроса данные с узла http://pragmaticperl.com и немного теории:

#!/usr/bin/env perl
use strict;
use warnings;
use LWP::UserAgent;

my $url = "http://pragmaticperl.com/";
my $content = get_content();

sub get_content {
    my $url = shift;
    my $ua = LWP::UserAgent->new();

    my $response = $ua->get($url);
    return $response->content();
}

И еще рассмотрим такой пример:

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

print hello_world();

sub hello_world {
    return "Hello world!\n";
}

Итак, у нас есть два приложения и две функции. Это get_content и hello_world. Между ними есть очень важное отличие: результат работы функции get_content может быть разным, сервер упал, или интернета нет, например. Т.е. при вызове с одинаковыми параметрами несколько раз функция может вернуть разный результат. Функция же hello_world всегда возвращает “Hello world”. Такие функции имеют определенные названия. Итак, get_content — функция с побочными эффектами.

Функцией с побочными эффектами называется такая функция, которая может возвращать разный результат при вызове с одинаковыми параметрами, например, взаимодействуя с внешним миром.

Чистые функции же — такие функции, которые всегда возвращают одинаковый результат при вызове с одинаковыми параметрами. Например, синус — чистая функция, т.к. синус 90 градусов величина постоянная.

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

Вообще, асинхронное выполнение можно организовать несколькими способами:

Многопоточность

Многопроцессность

Событийность

Мы рассмотрим событийно-ориентированное программирование, как наиболее дешевую модель с точки зрения потребления ресурсов. Итак, еще одно теоретическое отступление:

Все мы знаем функцию map. map — функция высшего порядка. Функцией высшего порядка называется такая функция, которая принимает в качестве аргументов или возвращает в качестве результата другую функцию. В Perl функцию высшего порядка написать элементарно. Например:

mySub(sub {
    print "Hello world!\n";
});

sub mySub {
    my $sub = shift;
    $sub->();
}

В этом примере mySub — функция высшего порядка, т.к. принимает функцию в качестве первого и единственного аргумента. Кстати, PSGI-приложение тоже является функцией высшего порядка, т.к.

return sub {
    ...
}

Так вот, событийное программирование — это функции высшего порядка, да и еще с побочными эффектами. Есть такая штука — событийная машина, она генерирует события. И еще одно теоретическое отсутпление:

callback, он же обратный вызов, это действие, которое выполняется по возникновению какого-либо события. Например, запросить данные с google.com, а по получению данных вывести их на стандартный вывод.

Далее. Асинхронное событийно-ориентированное программирование — это функции высшего порядка с побочными эффектами, обратными вызовами поверх событийной машины.

Событийная машина работает следующим образом. У нее есть несколько стадий работы:

Запуск событийной машины.

Регистрация событий, наблюдателей.

Блокировка.

Выполнение.

Если хотя-бы одного элемента нет — ничего не будет. И вот теперь я могу объяснить, что такое блокировка и почему это важно.

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

Суть асинхронного программирования. Суть асинхронного программирования в максимальном использовани имеющихся ресурсов и уменьшении времени простоя в целом, а, как следствие, суммарное время выполнения уменьшается.

Идем в бар

Например, возьмем в качестве примера два простых бара. В первом баре все синхронно, а во втором, соответственно, асинхронно. В первом баре бармен может наливать по одному стакану пива за раз. Не важно, что есть свободные краны, не важно, что от бармена требуется поставить стакан, открыть кран и ждать. Это синхронный подход. Асинхронный подход — бармен ставит стакан, открывает кран, а пока стакан наливается, бармен ставит рядом, к другому крану, еще один стакан. И открывает чипсы, например. При синхронном подходе бармен будет делать все эти действия последовательно. И если каждое действие будет занимать 9 секунд, то в первом случае бармен потратит 27 секунд, как минимум, для того, чтобы выполнить все необходимые манипуляции. В асинхронном баре же эти действия займут, как минимум, 9 секунд, т.к. бармен не ждет, а что-то делает в фоне. Соответственно, мы выигрываем во времени, но проигрываем по нагрузке. За все надо платить, в асинхронном баре бармен больше устает, но у него и зарплата больше, которая зависит от количества налитых стаканов пива и открытых чипсов. Как-то так, да.

Негатив

Любой подход не лишен недостатков. Всякая асинхронщина не исключение. Так получилось, что форкнуть работающую событийную машину задача весьма нетривиальная, потому, как правило, асинхронные решения работают в один процесс и в один поток. Все вроде бы ок, но потенциальная проблема здесь следующая: Если какая-то чистая функция будет выполняться долго, то все будет ждать. Чистые функции не прерываются событийной машиной и не могут работать одновременно с циклом событий. Если неправильно спроектировать асинхронное приложение, то блокировок можно наделать везде и оно будет, как говорят про vim, бибикать и все портить. Отсюда следует очень важный факт, который игнорируется многими.

Важнейший абзац. Нельзя просто так взять и вернуть данные из callback. Они существуют в своем мирке со своими правилами, область видимости функции. Использовать глобальные переменные можно, но не всегда, а там где можно — нежелательно. Единственный способ получить данные в другом месте, вне callback — их туда отправить. Или же заблокироваться и поднять на уровень выше. В правильном асинхронном приложении данные внутри callback не должны влиять на данные, которые находятся на уровень выше. Исключение — чистые данные и чистые функции. Иногда ссылки на данные. Но очень осторожно.

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

Потребление ресурсов. В виду того, что асинхронное приложение, если оно правильно спроектировано, практически не простаивает почем зря, существенно возрастает нагрузка на процессор. Если у вас слабое железо — профита не будет.

Резюмирую

Асинхронное решение вам подходит, если:

приложение активно взаимодействует с внешним миром;

приложение производит мало вычислительной работы;

приложение постоянно работает с какими-либо БД;

приложение чего-то ждет;

приложение — HTTP-сервер.

Асинхронное решение вам не подходит, если:

много матана у вас в приложении;

лениво разбираться с асинхронными решениями;

мало ресурсов.

Несколько советов:

Если не надо блокироваться — не блокируйся.

Если не знаешь, а надо ли здесь колбэк, оно не надо.

Три действия по 30 мс каждое лучше, чем одно по 90 мс.

Сломаться может где-угодно. Программирование на побочных эффектах требует перестраховываться везде.

Если думаешь, нужна проверка или нет, оно же не сломается — проверка нужна, оно сломается.

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

Не полагайся на порядок выполнения колбэков в приложении, он может всегда измениться.

Событийно-ориентированное программирование на Perl. AnyEvent.

Про AnyEvent уже ранее писалось в рамках данного журнала, потому на данном этапе я буду краток. AnyEvent — фреймворк, универсальный интерфейс для построения асинхронных приложений. Грубо говоря, он представляет собой DSL для написания асинхронных приложений, абстрагируясь от событийной машины и цикла событий. Т.е. AnyEvent позволяет с легкостью заменить один цикл событий другим, а приложение, если оно не использовало специфические возможности цикла событий, запустится без изменений кода. AnyEvent — DBI of event loop programming. Более подробно теорию по AnyEvent мы рассмотрим в следующих статьях. Сейчас же займемся практикой.

Немного кода

Как я и обещал, примеры кода будут на Perl. Для того, чтобы почувствовать вкус, мы напишем простое приложение для получения контента двух страниц с просторов интернета. Традиционно, первое приложение — синхронное, чтобы увидеть логику, второе приложение — переписанное на асинхронный лад первое.

#!/usr/bin/env perl
use strict;
use warnings;
use LWP::UserAgent;

my $urls = ['http://justnoxx.name/one.html', 'http://justnoxx.name/two.html'];
my $ua = LWP::UserAgent->new();

for my $url (@$urls) {
    print "GET request: $url\n";
    my $content = $ua->get($url)->content();
    print "Content: $content\n";
}
print "Done.\n";

Вывод данной программы будет иметь вид:

GET request: http://justnoxx.name/one.html
Content: one

GET request: http://justnoxx.name/two.html
Content: two

Done.

Совершенно очевидно и, к тому же, логично, что порядок работы программы не изменится. Допустим, если первый запрос будет занимать 10 с, а второй 1 с, то суммарно время работы скрипта будет примерно равно 11 секундам. Если второй запрос будет занимать 10 секунд, а первый 1, то суммарное время будет равно примерно 11 секундам. От перестановки слагаемых сумма не меняется. В данном случае асинхронное решение нам подходит идеально. Т. к:

Мы работаем с внешним миром.

У нас мало вычислений.

Приложение много простаивает.

Асинхронное приложение будет выглядеть примерно так:

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

# Подключаем AE
use AnyEvent;
# Асинхронный юзер-агент
use AnyEvent::HTTP;

my $urls = ['http://justnoxx.name/one.html', 'http://justnoxx.name/two.html'];

# $cv - переменная состояния
my $cv = AnyEvent->condvar();
# количество запросов
my $count = 0;

for my $url (@$urls) {
    # вывод нам поможет понять, как именно, а что более важно, в каком порядке, выполняется приложение
    print "New event: GET => $url\n";
    # это очень важная строка. my $guard; $guard будет пояснено далее в тексте статьи
    my $guard; $guard = http_get(
        $url, sub {
            # см. предыдущий коммент
            undef $guard;
            my ($content, $headers) = @_;
            print "Content of $url: $content\n";
            $count++;
            # если количество успешных запросов равно размеру списка URLов, отправляем данные, на уровень выше.
            $cv->send("Done.\n") if $count == scalar @$urls;
        },
    );
}

# блокировка и ожидание. Собственной персоной.
my $result = $cv->recv();

print "$result\n";

А вывод вот так:

New event: GET => http://justnoxx.name/one.html
New event: GET => http://justnoxx.name/two.html
Content of http://justnoxx.name/two.html: two

Content of http://justnoxx.name/one.html: one

Done.

А теперь самое важное. Отличия, как и, самое главное, почему это работает.

Я выше писал, что приложение сначала набирает задания в очередь (http_get), а потом начинает выполнять. Это можно увидеть в выводе. Сначала задачи ставятся в очередь, а затем выполняются. Обратите внимание на то, что: GET => http://justnoxx.name/one.html был поставлен в очередь раньше, но первым выполнился GET => http://justnoxx.name/two.html.

$cv — переменная состояния. Если вызывать у нее метод recv — никакие новые события в очередь поступать не будут, т.к. цикл событый решит, и решит весьма справедливо, что дальнейшая работа без данных невозможна. recv ожидает данных, которые можно отправить при помощи метода send этой же переменной. В данном приложении, как только данные получены, блокировка снимается и приложение завершается. Однако, есть тут подводный камень. В одной области видимости может существовать только одна переменная состояния. Иначе цикл событий начинает конкретно паниковать, не понимая, что ему делать. Еще. Нельзя вызывать метод recv без send, или без событий. Вызов в скрипте ->recv, если это единственный вызов во всем скрипте вообще, приведет к массе ошибок и/или загрузке процессора на 100%, зависит от цикла событий.

my $guard; $guard = ... undef $guard;

Подобный прием не редкость. Он используется для того, чтобы сборщик мусора не утилизировал наш callback (увеличиваем счетчик ссылок на 1). Если мы убереи undef $guard, ничего работать не будет. Иногда говорят что мы “замыкаем переменную”. Кстати, я писал про замыкания ранее. Мы можем сделать иначе — объявить в глобальной области видимости переменную-массив и потом при помощи push поместить туда $guard, и тогда наш callback не будет утилизирован.

Затем мы выполняем send-метод, который отправляет на уровень выше результат (“Done.” в нашем случае), который принимает ->recv. Затем программа завершается.

И наконец, если мы уберем $cv->recv();, ничего работать не будет, т.к. нет блокировки. В принципе, для начала достаточно. В следующей статье я остановлюсь более подробно на этих моментах.

В следующей статье цикла мы рассмотрим теорию по AnyEvent, понятие относительной асинхронности, таймеры и концепцию наблюдателей.

Я надеюсь, что данная статья была интересна. До новых встреч.

■ Дмитрий Шаматрин

Обзор CPAN за октябрь 2014 г.

Рубрика с обзором интересных новинок CPAN за прошедший месяц.

Статистика

Новых дистрибутивов — 242

Новых выпусков — 899

Новые модули

Proc::Fork::Control

Модуль Proc::Fork::Control представляет ещё один простой подход к созданию и управлению дочерними процессами. Модуль также позволяет контролировать максимальное количество создаваемых процессов и имеет встроенную процедуру для демонизации процессов.

Mojo::Pg

Себастьян Ридель представил новый модуль Mojo::Pg для работы с базой данных PostgreSQL в веб-приложениях на фреймворке Mojolicious. Сам по себе модуль является тонкой надстройкой к DBD::Pg, которая автоматически контролирует все соединения с базой данных, кэшируя их для повторного использования, а также производит сброс соединений, если форкается новый процесс, позволяя прозрачно работать с одним объектом модуля во всех процессах.

Mojo::Pg позволяет выполнять как блокирующиеся, так и асинхронные запросы к базе данных, с тем лишь ограничением, что запросы выполняются последовательно в рамках одного соединения.

Net::DNS_A

Net::DNS_A — это обёртка к функции стандартной библиотеки C getaddrinfo_a, которая является асинхронным аналогом функции getaddrinfo. Модуль позволяет выполнять асинхронное разрешение DNS-имён.

Authen::SCRAM

Authen::SCRAM — это реализация современного стандарта RFC5802 SCRAM — механизма хранения данных и протокола аутентификации посредством пароля.

Net::HTTP::Knork

Более легковесная реализация спецификации SPORE для описания REST API интерфейсов. В отличие от Net::HTTP::Spore, не имеет зависимости от Moose и возвращает результаты в виде объекта на базе стандартного класса HTTP::Response.

Tree::BK

Реализация структуры данных BK-дерева для выполнения поиска с неточным соответствием. Подобная структура часто используется для проверки правописания слов по словарю.

    use Tree::BK;
    use utf8;

    my $tree = Tree::BK->new();
    $tree->insert_all(qw(Валя Вася Варя Катя Маша));
    $tree->find('Ваня', 1); # Валя, Вася, Варя
    $tree->find('Ваня', 2); # Валя, Вася, Варя, Катя

Panda::Lib

Модуль Panda::Lib является коллекцией оптимизированных высокоскоростных функций, подходящих для использования как в Perl-, так и в XS-коде. В основном это функции для работы с хэшами и массивами: объединение, клонирование, сравнение.

Обновлённые модули

perl 5.18.4

Выпущен четвёртый релиз исправлений для предпоследней стабильной ветки perl-5.18. Релиз 5.18.3 был выпущен с ошибкой и просуществовал лишь несколько часов. В новом релизе исправлено несколько ошибок, в том числе ошибка сегментации памяти в Digest::SHA и утечка памяти на платформе Win32 при использовании system или обратных кавычек.

Mojo::Redis 1.00

Первый и последний мажорный релиз модуля для работы с Redis в веб-приложениях Mojolicious Mojo::Redis объявляет модуль устаревшим и рекомендует переходить на на Mojo::Redis2.

ExtUtils::MakeMaker 7.00

Вышел новый мажорный релиз самой старой и по-прежнему популярной системы для сборки перл-модулей EUMM. Релиз содержит множество исправлений, например, наконец-то появилась возможность указывать каталог для инсталляции, имя которого содержит пробелы, поддерживаются опции в кодировке UTF-8, а также файлы и каталоги с символами в кодировке UTF-8. На платформе Windows появилась поддержка для GNU Make.

autovivification 0.13

Обновлён модуль прагмы autovivification, позволяющий отключать автовивификацию в вашем коде. Автор обращает внимание, на то, что требуется тщательно протестировать этот релиз, поскольку потенциально возможно зависание на этапе компиляции. Новая версия должна заметно снизить общее замедление на этапе компиляции, которое вносит использование данной прагмы.

JSON::PP 2.27300

Новый релиз модуля JSON::PP содержит исправление ошибки при разборе JSON на старых версиях perl ≤ 5.8.6.

Panda::Export 2.1.0

Panda::Export — это альтернатива для модуля Exporter, написанная на C, которая позволяет на порядок ускорить импорт констант и функций в процессе компиляции. В новом релизе добавлена поддержка создания констант списком и увеличена скорость создания и импорта констант.

Mojolicious 5.56

В этом месяце традиционно вышло множество релизов веб-фреймворка Mojolicious, включающие множество различных изменений и улучшений. Важным стоит отметить релиз 5.48, в котором было сделано исправление серьёзной уязвимости, позволяющей проводить инъекцию параметров. Всё это привело к поломке обратной совместимости: методы, которые раньше возвращали разные результаты в зависимости от контекста, теперь разделены, например, params/all_params, cookie/all_cookies и т.д. Подробности об атаке можно почитать здесь

В отличие от Mojolicious, другие фреймворки ограничились менее радикальными мерами: в Dancer добавили метод param_array, а Catalyst ограничился упоминанием в документации, что метод params устарел и требует осторожного использования.

Syntax::Feature::Try 1.00

Вышел первый мажорный релиз реализации оператора try/catch/finally для Perl. В отличие от многих других реализаций он может обрабатывать ошибки по их ISA, поддерживает несколько catch-блоков и реализован на основе keyword/parser API (требует perl ≥ 5.14), не требует обязательной точки с запятой после завершающей фигурной скобки. В новом релизе была добавлена поддержка субтипов Moose::Util::TypeConstraints при обработке в catch.

IO::Socket::SSL 2.002

Новый мажорный релиз IO::Socket::SSL можно было смело переименовать в IO::Socket::TLS поскольку поддержка последней версии протокола SSL 3.0 по умолчанию отключена. Это связано с публикацией информации об уязвимости в протоколе SSL 3.0 — POODLE. Также по умолчанию подключаются опции для улучшения PFS в протоколах шифрования.

CSS::Sass 3.0.1

Новый мажорный релиз байндинга к библиотеке libsass для компиляции .sass/.scss-файлов. Новая версия соответствует последней мажорной версии библиотеки libsass.

Web::Scraper 0.38

Неожиданно после двухлетнего перерыва обновился модуль для разбора HTML-страниц с помощью XPath-выражений Web::Scraper. В обновлённой версии включены давно забытые в баг-трекере улучшения в документации и фильтры на основе регулярных выражений. Молодец, Миягава, лучше поздно, чем никогда.

■ Владимир Леттиев

Интервью с Еленой Большаковой

Елена Большакова (Liruoko) — Perl-программист, математик по образованию, соавтор проекта http://perltrap.com.

Как и когда научились программировать?

Поздно и случайно. В девятом классе на информатике был какой-то условный алгоритмический язык, переменные-циклы-условия-блок-схемы. Программы писали в тетрадках, компьютеров, которые могли бы выполнять эти программы, не было. Совсем не интересно.

Летом услышала рассказ одной девочки, как она писала программу для рисования маятника, и как это было сложно. Хотя казалось бы, что такого? Уравнение движения известно из физики.

В десятом классе (уже в другой школе) были компьютеры и начался Бейсик: line, circle, goto. Ага! — теперь я тоже могу делать мультики. Осталась после уроков и запрограммировала качающийся маятник. Кстати, самым сложным оказалось подобрать интервалы перерисовки кадров, чтобы картинка не моргала.

С этого момента программировать стало интересно, вроде как читать Жюль Верна.

После школы — факультет ВМиК МГУ, и там все было серьезно: алгоритмы, структуры данных, оценки сложности, доказательства корректности, методы оптимизации. У меня был собственный sql-сервер, компилятор C-подобного языка в байт-код и его интерпретатор, шелл, Элиза-подобная программа-собеседник. В общем, все было очень здорово.

Но к пятому курсу меня посетила идея, что программирование — слишком сиюминутное занятие: сегодня ты программируешь протокол X под архитектуру Y, а завтра протокол устарел и архитектура мертва. А мне хотелось вечности. И я пошла в аспирантуру на мехмат, «делать математику». Кстати, доказала красивые результаты и защитила диссертацию.

Какой редактор используете?

Vim. Он везде есть и одинаково удобен и при локальном использовании, и на удаленных серверах.

Кстати, а почему не спрашиваете про клавиатуру? Для комфортной работы с текстом хорошая клавиатура важна почти так же, как хороший редактор. У меня две любимые клавиатуры: IBM Model M и Code Keyboard.

Как и когда познакомились с Perl?

Когда была в аспирантуре, знакомый системный администратор рассказывал про свои скрипты и про Perl — язык, похожий на человеческий. Язык, в котором одно и то же можно написать по-разному, и где можно сказать: сделай или умри.

Потом однажды весной наткнулась в Википедии на перловый однострочник, в котором коротким регулярным выражением, состоящим почти из одних единиц, выводился бесконечный список простых чисел. Вот это да! Хочу знать больше. Тот же знакомый сисадмин (к тому времени мы поженились) подсунул Camel Book. Книга очень понравилась с первых страниц, я решила, что это мое, и написала резюме в Яндекс. Тем же летом я вышла на работу в Директ, Perl-разработчиком.

С какими другими языками интересно работать?

Нравится C — он прямой и честный. Еще очень забавный Forth: минималистичный до крайности, но при этом настоящий промышленный язык, не brainfuck какой-нибудь.

Но на самом-то деле мне интересно не с языками работать, а задачи решать. Сейчас мне хватает достойных задач, а вообще было бы интересно заняться чем-нибудь с формальными грамматиками. Или со статистикой. Еще очень увлекательная область — вероятностные структуры данных.

Что, по-вашему, является самым большим преимуществом Perl?

Perl очень пластичный и выразительный, идеально подходит для трансляции мыслей в машинно-читаемый формат. И на нем очень легко начать писать. Я слышала такой рассказ от коллеги: «Друг поставил Perl на компьютер, я наугад написал ‘print “hello”’ — а оно заработало. Так я и попался».

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

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

Что, по-вашему, является самой важной особенностью языков будущего?

Давайте расскажу страшилку: в будущем новые языки будут появляться чаще и умирать быстрее. Новая мобильная или социальная платформа — новый язык, ушла платформа — ушел язык. Никаких стабильных версий, каждые полгода базовый синтаксис радикально меняется. Динамика и драйв, революция каждый день.

Что думаете о будущем Perl?

Судьба Perl зависит от сообщества, то есть и от нас с вами. Это большая ответственность, но это и здорово!

Помогает ли математическое образование в программировании?

А что мы считаем «математическим образованием»? Вызубрить тысячу теорем, сдать пятьдесят зачетов и тридцать экзаменов, а потом забыть все, как страшный сон? По-моему, такое образование не помогает само по себе, но работает как отбор: смог? осилил? Значит, силён.

Если же действовать не по стандартному студенческому «знал, сдал, забыл», а полноценно участвовать в математических исследованиях: работать в научном семинаре, вникнуть в современные результаты в какой-нибудь области, построить контрпример к актуальной гипотезе, доказать что-нибудь новенькое, опубликоваться в хорошем журнале — да, такой математический background полезен для программистов.

Знаете, у компьютерщиков в запасе есть негодные оправдания: «Работает ведь! Так что неважно, насколько аккуратно сделано, эффективно ли, надежно ли». У математиков же нет никаких машин, которые бы воспринимали доказательства. Хочешь утвердить новый результат — убеди людей, как три тысячи лет назад, как у древних греков. Вот эту культуру обсуждения, поиска слабых мест, контраргументов полезно освоить и разработчику.

Вас всегда можно увидеть на различных Perl-мероприятиях, чем они вас привлекают?

Интересно слушать людей из других компаний и проектов.

У нас у всех есть общее — Perl — но задачи и проблемы очень разные. Во время некоторых докладов я думаю «да с таким подходом мы и недели не выжили бы», на других — «смотри-ка, и у них так же», а иногда — «о, вот это интересно, надо изучить подробнее».

В целом, чужой опыт помогает принимать лучшие решения в своей работе.

Расскажите про http://perltrap.com.

Давным-давно мне попалась книжка Стива Уэллина «Как не надо программировать на C++». Там приводятся фрагменты программ, и в каждом есть проблема — иногда более заковыристая, иногда менее. К каждой проблеме дается серия подсказок, обычно довольно ехидных, и наконец объяснение.

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

Книга Уэллина дает отличную возможность попрактиковаться в таком критическом чтении. Это интересно и полезно — и для самоконтроля, и для код-ревью.

Потом, уже в Яндексе, я подумала, что с удовольствием читала бы такую книгу-задачник про Perl, только ее почему-то никто не писал. Если никто другой, тогда, может быть, я? К тому же нашлись коллеги, которым тоже понравилась идея, мы зарегистрировали имя, сделали сайт и стали выкладывать задачи. Вот и все.

Как известно, вы работаете в Яндексе уже продолжительное время. Сколько времени проводите за написанием Perl-кода?

Меньше, чем хотелось бы.

Сейчас я руковожу группой инфраструктурной разработки в Директе. С одной стороны, порядочное время занимает планирование, проектирование, ревью, консультации, а с другой — у нас специфические задачи. Мы обеспечиваем разработчиков, администраторов, тестировщиков, менеджеров эффективными и надежными инструментами. Для этого не требуется много кода, но всегда требуется тщательный анализ, чтобы направить усилия самым выгодным образом.

По крупному счету все хорошо, но бывает, я чувствую себя как в анекдоте: «Дома говорю, что иду на работу, на работе пишу, что задерживаюсь, а сам в парк на скамеечку, достаю ноутбук, и программирую, программирую…»

Стоит ли советовать молодым программистам учить сейчас Perl?

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

На уровне однострочников perl -lane '$sum{$F{3}} += $F[1] END{print "$_ $sum{$_}" for keys %sum}' Perl нужен всем, наравне с grep-ом и make-ом.

А делать ли Perl основной специальностью или оставить инструментом на подхвате — каждый потом разберется сам.

Вопросы от читателей

Будут ли еще модули на CPAN?

Если буду писать что-то общественно-полезное — буду выкладывать.

Согласны ли со мнением, что вы «самый крутой перловик Яндекса»? :)

А почему только Яндекса? ^_^

Если серьезно, я считаю своей самой сильной стороной юзабилити command-line-инструментов, я умею делать так, чтобы разработчики и администраторы ошибались реже. А еще я могу брать что-нибудь сложное — программу, систему, идею, текст, процесс — и делать эту вещь проще. Вот!

Почему перестали преподавать?

Это не так. Просто перешла от серийного обучения в аудитории к штучному кураторству над стажерами в Яндексе. И еще пишу статьи для PragmaticPerl ^_^

Как устроиться работать в Яндекс?

Во-первых, захотеть.

Во-вторых, отправить резюме.

В-третьих, хорошо показать себя на собеседованиях.

Первые два пункта просты: если нет желания — то и говорить не о чем, а резюме — механическая работа. Что касается собеседований… В разных командах и для разных позиций требования отличаются, но определенно полезно логически мыслить, уметь работать над задачей, которая не решается с первого взгляда, чувствовать сложность алгоритомов, понимать unix-like-системы — форки, процессы, сокеты, все такое. Не помешают базы данных.

Удачи!

Спасибо журналу «Pragmatic Perl» за приглашение, это было неожиданно и приятно.

■ Вячеслав Тихановский