[Из песочницы] Функциональное программирование на Perl в примерах
В данной статье будет рассмотрено функциональное программирование на примере скрипта поиска битых ссылок с использованием AnyEvent: HTTP. Будут рассмотрены следующие темы:
- анонимные подпрограммы;
- замыкания (closures);
- функции обратного вызова (callbacks);
Анонимные подпрограммы
Анонимная подпрограмма объявляется также, как и обычная, но между ключевым словом sub
и открывающей скобкой блока программного кода нет имени. Кроме того, такая форма записи расценивается как часть выражения, поэтому объявление анонимной подпрограммы должно завершаться точкой с запятой или иным разделителем выражения, как и в большинстве случаев:
sub { ... тело подпрограммы ... };
Например, реализуем подпрограмму, утраивающую переданное ей значение:
my $triple = sub {
my $val = shift;
return 3 * $val;
};
say $triple->(2); # 6
Основное преимущество анонимных подпрограмм — использование «кода как данных». Другими словами, мы сохраняем код в переменную (например, передаем в функцию в случае колбеков) для дальнейшего исполнения.
Также, анонимные подпрограммы могут использваться для создания рекурсий, в том числе в сочетинии с колбеками. Например, используя лексему __SUB__
, которая появилась в версии Perl 5.16.0
, и позволяет получить ссылку на текущую подпрограмму, реализуем вычисление факториала:
use 5.16.0;
my $factorial = sub {
my $x = shift;
return 1 if $x == 1;
return $x * __SUB__->($x - 1);
};
say $factorial->(5); # 120
Пример использования рекурсии в сочетании с колбеками будет показан ниже при рассмотрении задачи поиска битых ссылок.
Замыкания (Closures)
Как сказано в википедии
Замыкание — это функция первого класса, в теле которой присутствуют ссылки на переменные, объявленные вне тела этой функции в окружающем коде и не являющиеся ее параметрами.
По сути, замыкание — это аналог класса в ООП: предоставляет функциональность и данные связанные и упакованные вместе. Рассмотрим пример замыкания в Perl и класса в C++:
Perl
sub multiplicator {
my $multiplier = shift;
return sub {
return shift() * $multiplier;
};
}
C++
class multiplicator {
public:
multiplicator(const int &mul): multiplier(mul) { }
long int operator()(const int &n) {
return n * multiplier;
}
private:
int multiplier;
};
Проведем анализ приведенного кода:
объявление приватной переменной:
- Perl:
вначале определяем лексическую (
my
) переменную$multiplier
(my $multiplier = shift;
);- С++:
объявляем переменную
multiplier
типаint
после маркера доступаprivate
;инициализирование приватной переменной:
- Perl:
при создании переменной инициализируем переданным значением;
- C++:
перегружаем конструктор, чтобы он принимал число и в списке инициализации инициализируем переменную
multiplier
;создание подпрограммы, перемножающей переданное ей значение с ранее инициализированной переменной:
- Perl:
возвращаем анонимную подпрограмму, которая принимает на вход параметр и перемножает его с ранее инициализированной переменной
$multiplier
и возвращает полученное значение;- C++:
мы перегружаем оператор вызова функции
()
, который на вход получает параметрn
, перемножает его с переменнойmultiplier
и возвращает значение.
Для использования замыкания в Perl и класса в C++, их нужно определить, т.е. создать объект:
Perl:
- Определение объекта:
my $doubled = multiplicator(2);
my $tripled = multiplicator(3);
- Использование:
say $doubled->(3); # 6
say $tripled->(4); # 12
C++:
- Определение объекта:
multiplicator doubled(2), tripled(3);
- Использование:
cout << doubled(3) << endl; // 6
cout << tripled(4) << endl; // 12
В C++ объект класса, в котором определен оператор определения ()
, зачастую называют функциональным объектом, или функтором. Функциональные объекты чаще всего используются как аргументы для общих алгоритмов. Например, для того, чтобы сложить элементы вектора, можно использовать алгоритм for_each, который применяет переданную функцию к каждому элементу последовательности и класс Sum с перегруженным оперетором ()
, который складывает все элементы последовательности и возвращает сумму. Также, вместо класса Sum можно использовать лямбды, которые появились в C++11.
C++:
#include
#include
#include
using std::cout;
using std::endl;
using std::vector;
class Sum {
public:
Sum() : sum(0) { };
void operator() (int n) { sum += n; }
inline int get_sum() { return sum; }
private:
int sum;
};
int main() {
vector nums{3, 4, 2, 9, 15, 267};
Sum s = for_each(nums.begin(), nums.end(), Sum());
cout << "сумма с помощью класса Sum: " << s.get_sum() << endl;
long int sum_of_elems = 0;
for_each(nums.begin(), nums.end(), [&](int n) {
sum_of_elems += n;
});
cout << "сумма с помощью лямбды: " << sum_of_elems << endl;
return 0;
}
Perl:
sub for_each {
my($arr, $cb) = @_;
for my $item (@$arr) {
$cb->($item);
}
}
my $sum = 0;
for_each [3, 4, 2, 9, 15, 267], sub {
$sum += $_[0];
};
say $sum;
Как видно из примера, в C++ мы объявляем класс Sum
, который содержит:
- приватную переменной
sum
, которая инициализируется в стандартном конструкторе; - перегруженный оператор
()
, который получает каждое значение посделовательности и суммирует в перемменуюsum
; - метод
get_sum
для доступа к приватной переменнойsum
.
В примере на Perl мы создаем функцию for_each
, которая принимает ссылку на массив и анонимную функцию. Далее мы проходим по массиву, и выполняем анонимную функцию (замыкание), передавая ей в качестве параметра очередной элемент массива.
При использовании функции for_each
, мы сначала определяем лескическую переменную $sum
, инициализированную нулем. Затем в функцию for_each
передаем ссылку на массив и функцию-замыкание, в которой мы суммируем каждый элемент массива в переменную $sum
. После выполения функции for_each
в переменной $sum
будет содержаться сумма массива.
Аналогом функции-замыкания из примера на Perl, в C++ является использование лямбд, как показано в коде. В примере на Perl функция-замыкание, передаваемая в функцию, также называется колбеком, или функцией обратного вызова.
Функции обратного вызова (Callback)
Как видно из примера for_each
, функция обратного вызова — это передача исполняемого кода в качестве одного из параметров другого кода. Зачастую, передаваемая функция работает как замыкание, т.е. имеет доступ к лексическим переменным и может быть определена в других контекстах программного кода и быть недоступной для прямого вызова из родительской функции (функции, в которую передали замыкание/колбек).
По сути, функция обратного вызова является аналогом полиморфизма функций, а именно, позволяет создавать функции более общего назначения вместо того, чтобы создавать серию функций, одинаковых по структуре, но отличающихся лишь в отдельных местах исполняемыми подзадачами. Рассмотрим пример задачи чтения из файла и записи в файл. Для этого с помощью Perl создадим две функции reader и writer (за основу был взят пример с презентации Михаила Озерова Ленивые итераторы для разбора разнородных данных), а с помощью C++ мы создадим классы Reader_base, Writer_base, ReaderWriter.
Perl
use strict;
use warnings;
sub reader {
my ($fn, $cb) = @_;
open my $in, '<', $fn;
while (my $ln = <$in>) {
chomp $ln;
$cb->($ln); # выполняем код для работы со строкой
}
close $in;
}
sub write_file {
my ($fn, $cb) = @_;
open my $out, '>', $fn;
$cb->(sub { # передаем анонимную функцию для записи в файл
my $ln = shift;
syswrite($out, $ln.$/);
});
close $out;
}
write_file('./out.cvs', sub {
my $writer = shift; # sub { my $ln = shift; syswrite() }
reader('./in.csv', sub {
my $ln = shift;
my @fields = split /;/, $ln;
return unless substr($fields[1], 0, 1) == 6;
@fields = @fields[0,1,2];
$writer->(join(';', @fields)); # вызываем анонимную функцию для записи в файл
});
});
C++
#pragma once
#include
#include
#include // для файлового ввода-вывода
using std::ifstream; using std::getline;
using std::cout; using std::runtime_error;
using std::endl; using std::cerr;
using std::string;
class Reader_base {
public:
Reader_base(const string &fn_in) : file_name(fn_in) { open(file_name); }
virtual ~Reader_base() { infile.close(); }
virtual void open(const string &fn_in) {
infile.open(fn_in);
// передаем исключение, если файл не открыт для записи
if (! infile.is_open())
throw runtime_error("can't open input file \"" + file_name + "\"");
}
virtual void main_loop() {
try {
while(getline(infile, line)) {
rcallback(line);
}
} catch(const runtime_error &e) {
cerr << e.what() << " Try again." << endl;
}
}
protected:
virtual void rcallback(const string &ln) {
throw runtime_error("Method 'callback' must me overloaded!");
};
private:
ifstream infile;
string line;
string file_name;
};
#pragma once
#include
#include
#include // для файлового ввода-вывода
using std::string; using std::ofstream;
using std::cout; using std::runtime_error;
using std::endl; using std::cerr;
class Writer_base {
public:
Writer_base(const string &fn_out) : file_name(fn_out) { open(file_name); }
virtual ~Writer_base() { outfile.close(); }
virtual void open(const string &fn_out) {
outfile.open(file_name);
if (! outfile.is_open())
throw runtime_error("can't open output file \"" + file_name + "\"");
}
virtual void write(const string &ln) {
outfile << ln << endl;
}
private:
string file_name;
ofstream outfile;
};
#pragma once
#include "Reader.hpp"
#include "Writer.hpp"
class ReaderWriter : public Reader_base, public Writer_base {
public:
ReaderWriter(const string &fn_in, const string &fn_out)
: Reader_base(fn_in), Writer_base(fn_out) {}
virtual ~ReaderWriter() {}
protected:
virtual void rcallback(const string &ln) {
write(ln);
}
};
#include "ReaderWriter.hpp"
int main() {
ReaderWriter rw("charset.out", "writer.out");
rw.main_loop();
return 0;
}
Компилировать следующим образом:
$ g++ -std=c++11 -o main main.cpp
Проведем анализ кода:
чтение из файла:
- Perl:
в функцию
reader
мы передаем имя файла для чтения и колбек. Сначала мы открываем файл на чтение. Затем в цикле итерируемся построчно по файлу, в каждой итерации вызываем колбек, передавая ему очередную строку. После завершения цикла, мы закрываем файл. Если говорить в терминах ООП, то за инициализацию и открытие файла отвечает конструктор, за главный цикл отвечает методmain_loop
, в котором происходит итерация по файлу с вызовом колбека. Закрытие файла происходит в деструкторе. Колбек — это по сути виртуальная метод, который перегружен в потомке и вызван из родителя. Эту аналогию можно проследить в примере на C++.- C++:
мы в конструкторе класса
Reader_base
инициализируем переменнуюfile_name
, открываем файл на чтение. Далее мы создаем виртуальную функцию-членmain_loop
, в котором в цикле обходим файл построчно и передаем строку в функцию-членrcallback
, которая должна быть пергружена в потомке.запись в файл:
- Perl:
в функцию
writer
мы передаем имя файла для записи и колбек. Также, как и в примере с функциейreader
, мы сначала открываем файл на запись. Затем мы вызваем колбек в который передаем другой колбек (замыкание), в котором мы получаем строку и затем записываем ее в файл. После выхода из колбека мы закрываем файл. Если говорить в терминах ООП, то за инициализацию и открытие файла отвечает конструктор. За запись в файл отвечает методwrite
, который получает на вход строку и записывает ее в файл. Затем файл закрывается в деструкторе. Эту аналогию можно проследить в примере на C++.- C++:
мы в конструкторе класса
Writer_base
инициализируем переменнуюfile_name
, открываем файл на запись. Далее мы создаем виртуальню функцию-членwriter
, в который передается строка для записи в файл. Затем файл закрывается в деструкторе.работа с созданными функциями в Perl и классами в C++:
- Perl:
мы сначала вызваеем функцию
writer
, в которую передаем имя файла для записи и колбек. В колбеке мы в переменную$writer
получаем другой колбек, который будет записывать переданную ему строку в файл. Затем мы вызываем функциюreader
, в которую передаем имя файла для чтения и колбек. В колбеке функцииreader
мы получаем очередную строку из файла, работаем с ней, и с помощью колбека$writer
записываем в файл. Как видно из примера, колбек функции reader по сути является замыканием, т.к. содержит ссылку на лексическую переменную$writer
.- C++:
мы создаем класс
ReaderWriter
, который использует множественное наследование и наследует классыReader_base
иWriter_base
. В конструкторе мы инициализируем классыReader_base
иWriter_base
именем файла для чтения и записи соответственно. Затем создаем перегруженный методrcallback
, который получает очередную строку и записывает в файл с помощью методаwrite
классаWriter_base
. Перегруженный методrcallback
соответвоенно вызывается из методаmain_loop
классаReader_base
. Как видно из примера файла main.cpp, для работы с классами создается объектrw
классаReaderWriter
, в конструктор которого передается имена файлов для чтения и для записи. Затем вызываем функцию-членmain_loop
объектаrw
.
Далее рассмотрим комплексную практическую задачу поиска битых ссылок с помощью AnyEvent: HTTP, в котором будут использоваться вышеописанные темы — анонимные подпрограммы, замыкания и функции обратного вызова.
Задача поиска битых ссылок
Для того, чтобы решить задачу поиска битых ссылок (ссылок с кодами ответа 4xx и 5xx), необходимо понять, как реализовать обход сайта. По сути, сайт представляет из себя граф ссылок, т.е. урлы могут ссылаться как на внешние страницы, так и на внутренние. Для обхода сайта будем использовать следующий алгоритм:
process_page(current_page):
for each link on the current_page:
if target_page is not already in your graph:
create a Page object to represent target_page
add it to to_be_scanned set
add a link from current_page to target_page
scan_website(start_page)
create Page object for start_page
to_be_scanned = set(start_page)
while to_be_scanned is not empty:
current_page = to_be_scanned.pop()
process_page(current_page)
Реализация данной задачи лежит в репозитории Broken link checker Рассмотрим скрипт checker_with_graph.pl. Вначале мы инициализируем переменные $start_page_url
(урл стартовой страницы), $cnt
(количество урлов на скачивание), создаем хэш $to_be_scanned
и граф $g
.
Затем создаем функцию scan_website,
в которую передаем ограничение на максимальное количество урлов на скачивание и колбек.
sub scan_website {
my ($count_url_limit, $cb) = @_;
Сначала мы инициализируем хэш $to_be_scanned
стартовой страницей.
# to_be_scanned = set(start_page)
$to_be_scanned->{$start_page_url}{internal_urls} = [$start_page_url];
Полный разбор структуры $to_be_scanned
будет дальше, а сейчас стоить обратить внимание, что ссылка является внутренней (internal_urls).
Далее создаем анонимную функцию и выполняем её. Запись вида
my $do; $do = sub { ... }; $do->();
является стандартной идиомой и позволяет обратиться к переменной $do
из замыкания, например для создания рекурсии:
my $do; $do = sub { ...; $do->(); ... }; $do->();
или удаления циклической ссылки:
my $do; $do = sub { ...; undef $do; ... }; $do->();
В замыкании $do
мы создаем хэш %urls
, в который складываем урлы из хэша $to_be_scanned
.
my %urls;
for my $parent_url (keys %$to_be_scanned) {
my $type_urls = $to_be_scanned->{$parent_url}; # $type_urls - internal_urls|external_urls
push @{$urls{$parent_url}}, splice(@{$type_urls->{internal_urls}}, 0, $max_connects);
while (my ($root_domain, $external_urls) = each %{$type_urls->{external_urls}}) {
push @{$urls{$parent_url}}, splice(@$external_urls, 0, 1);
}
}
Структура хэша %urls
следующая:
{parent_url1 => [target_url1, target_url2, target_url3], parent_url2 => [...]}
Затем мы выполняем функцию process_page
, передавая ей ссылку на хэш урлов %urls
и колбек.
process_page(\%urls, sub { ... });
В функции process_page
мы сохраняем полученный хэш и колбек.
sub process_page {
my ($current_page_urls, $cb) = @_;
После чего мы в цикле проходимся по хэшу урлов, получая пару (parent_url => current_urls)
и далее проходимся по списку текущих урлов (current_urls)
while (my ($parent_url, $current_urls) = each %$current_page_urls) {
for my $current_url (@$current_urls) {
Прежде, чем приступить к рассмотрению получения данных со страниц сделаем небольшое отступление. Базовый алгоритм парсинга страницы и получения с нее урлов предпоолагает один HTTP-метод GET, вне зависимости, внутренний этот урл или внешний. В данной реализации было использовано два вызова HEAD и GET для уменьшения нагрузки на сервера следующим образом:
- HEAD запросы выполняются для всех внешних урлов (вне зависимости, с ошибкой они или нет); для внутренних с ошибкой и для не веб-страниц;
- HEAD и GET запросы выполняются для внутренних веб-страниц без ошибок;
Итак, сначала мы выполняем функцию http_head
модуля AnyEvent: HTTP, передавая ему текущий урл, параметры запроса и колбек.
$cv->begin;
http_head $current_url, %params, sub {
В колбеке мы получаем заголовки (HTTP headers)
my $headers = $_[1];
из которых получаем реальный урл (урл после редиректов)
my $real_current_url = $headers->{URL};
Затем мы сохраняем в хэш %urls_with_redirects
пары (current_url => real_current_url)
.
$urls_with_redirects{$current_url} = $real_current_url if $current_url ne $real_current_url;
Далее, если произошла ошибка (коды статуса 4xx и 5xx), то выводим ошибку в лог и сохраняем заголовок в хэш для дальнейшего использования
if (
$headers->{Status} =~ /^[45]/
&& !($headers->{Status} == 405 && $headers->{allow} =~ /\bget\b/i)
) {
$warn_log->("$headers->{Status} | $parent_url -> $real_current_url") if $warn;
$note_log->(sub { p($headers) }) if $note;
$urls_with_errors{$current_url} = $headers; # для вывода ошибок в граф
}
Иначе, если сайт внутренний и это веб-страница,
elsif (
# сайт внутренний
($start_page_url_root eq $url_normalization->root_domain($real_current_url))
# и это веб-страница
&& ($headers->{'content-type'} =~ m{^text/html})
) {
то выполняем функцию http_get
, которой передаем реальный текущий урл, полученный выше, параметры запроса и колбек.
$cv->begin;
http_get $real_current_url, %params, sub {
В колбеке функции http_get
получаем заголовки и тело страницы, декодируем страницу.
my ($content, $headers) = @_;
$content = content_decode($content, $headers->{'content-type'});
С помощью модуля Web: Query выполняем парсинг страницы и получение урлов.
wq($content)->find('a')
->filter(sub {
my $href = $_[1]->attr('href');
# если в большом содержании содержутся страницы с анкорами каждого раздела статьи, фильтруем их
$href !~ /^#/
&& $href ne '/'
&& $href !~ m{^mailto:(?://)?[A-Z0-9+_.-]+@[A-Z0-9.-]+}i
&& ++$hrefs{$href} == 1 # для фильтрации уже существующих урлов
if $href
})
->each(sub { # for each link on the current page
На каждой итерации метода each
мы получаем в колбеке ссылку
my $href = $_->attr('href');
и преобразовываем ее
$href = $url_normalization->canonical($href);
# если путь на сайте '/', '/contact' и не внешний (//dev.twitter.com/etc)
if ($href =~ m{^/[^/].*}) {
$href = $url_normalization->path($real_current_url, $href) ;
}
$href = $url_normalization->without_fragment($href);
Далее мы проверяем — если в графе нет такой ссылки
unless($g->has_vertex($href)) { # if tarteg_page is not already in your graph
то получаем корневой домен ссылки (либо ставим его в 'fails')
my $root_domain = $url_normalization->root_domain($href) || 'fails';
После чего мы заполняем структуру $new_urls
, которая аналогична структуре $to_be_scanned
и имеет следующий вид:
$new_urls = $to_be_scanned = {
parent_url => {
external_urls => {
root_domain1 => [qw/url1 url2 url3/],
root_domain2 => [qw/url1 url2 url3/],
},
internal_urls => [qw/url url url/],
},
};
В структуре $new_urls
мы создаем пару (parent_url => target_url)
, при этом target_url
делим еще на несколько частей, а именно — разделяем на внутренние урлы, которые сохраняем в массив, и внешние, которые еще делим по доменам и также сохраняем в массив. Данная структура позволяет уменьшить нагрузку на сайты следующим образом — мы за один раз выбираем $max_connects (количество коннектов на хост)
внутренних урлов и по одному внешнему урлу для каждого домена, как и показано в замыкании $do
выше при конструировании хэша %urls
. Соответственно, в начале функции scan_website
мы сохраняли стартовую страницу следующим образом:
$to_be_scanned = {
$start_page_url => {
internal_urls => [$start_page_url],
},
};
т.е. в данном случае, и родительской, и текущей страницей была стартовая страница (в остальных случаях данные страницы различаются).
Конструирование данной структуры происходит следующим образом — если сайт внутренний, то мы создаем структуру
$new_urls->{$real_current_url}{internal_urls} //= []
иначе, если сайт внутренний, то структуру
$new_urls->{$real_current_url}{external_urls}{$root_domain} //= []
и сохраняем одну из этих структур в переменную $urls
, которую далее используем для записи в структуру $new_urls
.
push @$urls, $href; # add it to to_be_scanned set
В данном случае мы используем ссылки для создания и работы со сложныи структурами данных. Переменная$urls
ссылается на структуру$new_urls
, и соответственно при изменении переменной$urls
, происходит изменение структуры$new_urls
. Более подробно про структуры данных и алгоритмы в Perl можно посмотреть в книге «Jon Orwant — Mastering Algorithms with Perl».
Затем мы добавляем в граф пару (real_current_url (parent) => href (current))
.
$g->add_edge($real_current_url, $href);
После чего проверяем структуру $new_urls
— если массивы internal_urls
или external_urls
не пусты, то выводим данные в лог и выполняем колбек, передавая ему структуру $new_urls
if (is_to_be_scanned($new_urls)) {
$debug_log->(($parent_url // '')." -> $real_current_url ".p($new_urls)) if $debug;
$cb->($new_urls);
}
Если мы не попали ни в один из вариантов (ошибка или парсинг внутренней страницы), т.е. сайт внешний и без ошибок, то выполняем колбек
else {
$cb->();
}
Данный вызов коблека нужен в том случае, когда в списке текущих урлов $current_urls
все внешние сайты, но при этом в $to_be_scanned
еще остались урлы. Без этого вызова мы пройдемся по списку $current_urls
, выполнив http_head
, и выйдем.
В колбеке функции process_page
мы сохраняем полученную структуру $new_urls
,
process_page(\%urls, sub {
my $new_urls = shift;
объединяем ее с переменной $to_be_scanned
.
$to_be_scanned = merge($to_be_scanned, $new_urls) if $new_urls;
Далее проверяем — если количество элементов графа больше или равно ограничению количества урлов, то выходим, удаляя ссылку на анонимную подпрограмму и выполняя $cv->send()
.
if (scalar($g->vertices) >= $count_url_limit) {
undef $do;
$cb->();
$cv->send;
}
Иначе, если есть урлы для проверки,
elsif (is_to_be_scanned($to_be_scanned)) {
то рекурсивно вызываем анонимную подпрограмму
$do->();
вызов которой был рассмотрен выше. Данный рекурсивный вызов по сути позволяет в рамках колбеков получить доступ к обновленной структуре $to_be_scanned
из process_page
(эдакая замена цикла в линейном коде).
В качестве бонуса, в скрипте реализован вывод графа с помощью GraphViz в разные форматы — svg, png и т.д. Примеры запуска скрипта:
$ perl bin/checker_with_graph.pl -u planetperl.ru -m 500 -c 5 \
-g -f svg -o etc/panetperl_ru.svg -l "broken link check" -r "http_//planetperl.ru/"
$ perl bin/checker_with_graph.pl -u habrahabr.ru -m 500 -c 5 \
-g -f svg -o etc/habr_ru.svg -l "broken link check" -r "https_//habrahabr.ru/"
$ perl bin/checker_with_graph.pl -u habrahabr.ru -m 100 -c 5 \
-g -f png -o etc/habr_ru.png -l "broken link check" -r "https_//habrahabr.ru/"
где
--url | -u стартовая страница
--max_urls | -m максимальное количество урлов для скачивания
--max_connects | -c количество коннектов на хост
--graphviz | -g создать граф урлов
--graphviz_log_level | -e указать уровень логов при создании графа урлов, см. perldoc Log::Handler
--format | -f выходной формат файлов - png, svg, etc
--output_file | -o относительный путь до файла
--label | -l подпись графа
--root | -r корневой узел для графа - т.к. используется драйвер twopi для создания радиального расположения графа
Также имется возможность управлять выводом логов с помощью переменной окружения PERL_ANYEVENT_VERBOSE, а именно
$ export PERL_ANYEVENT_VERBOSE=n
где n:
- 5 (warn) — вывод ошибок http
- 6 (note) — детальный вывод ошибок http (ссылка на хэш $headers)
- 7 (info) — вывод трассировки вызовов к URLs
- 8 (debug) — вывод списка урлов, выкачанных со страницы
Заключение
В данной статье было рассмотрено функциональное программирование на Perl, в частности, были рассмотрены такие темы — анонимные подпрограммы, замыкания и функции обратного вызова. Было проведено сравнение замыканий в Perl и классов в C++, функций обратного вызова (callbacks) в Perl и перегрузку функций-членов в C++. Также был расмотрен практический пример поиска битых ссылок с использованием AnyEvent: HTTP, в котором были использованы все вышеописанные возможности функционального программирования.