В данной статье будет рассмотрено функциональное программирование на примере скрипта поиска битых ссылок с использованием 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 <iostream> #include <vector> #include <algorithm> 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<int> 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 <iostream> #include <string> #include <fstream> // для файлового ввода-вывода 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 <iostream> #include <string> #include <fstream> // для файлового ввода-вывода 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, в котором были использованы все вышеописанные возможности функционального программирования.
ссылка на оригинал статьи https://habrahabr.ru/post/326028/
Добавить комментарий