В данной статье будет рассмотрено функциональное программирование на примере скрипта поиска битых ссылок с использованием 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


read_write_file.pl
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++


Reader_base.hpp
#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;
};

Writer_base.hpp
#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;
};

ReaderWriter.hpp
#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);
    }
};

main.cpp
#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, в котором были использованы все вышеописанные возможности функционального программирования.

Поделиться с друзьями
-->

Комментарии (6)


  1. Zanak
    10.04.2017 09:48
    +2

    Извините, но не считаю тему раскрытой:

    1. Не уяснил причину сравнения с плюсами. Это заметно разные языки. Более логично было сравнить, с каким нибудь скриптовым языком.
    2. В perl есть, например, функция sort, которую можно использовать в функциональном стиле для сортировки массивов с чем угодно. Есть функция map, которую любят демонстрировать поклонники функциональной парадигмы. Автор этот момент опустил, хотя для иллюстрации они, на мой взгляд, более подходят, чем пример с замыканием.
    3. Сама по себе механика обратных вызовов не является признаком функциональной парадигмы, потому как давно используется в perl, например при работе с GUI.
    4. В чем прелесть использования FP в perl?

    PS Возможно кому нибудь будет интересна книга: «Higher-Order Perl» by Mark Jason Dominus. Книга раздается в сети бесплатно, и она доступна для скачивания.


    1. TheAthlete
      10.04.2017 10:40
      -1

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


      2. Полностью согласен, что sort/grep/map позволяет работать в функциональном стиле. Вопрос только в том, что блок кода в этих функциях по сути и есть замыкание, просто записаны с использования прототипа map(&@), что позволяет сначала задавать блока кода без sub, а затем массив. Пример, который иллюстрирует, что это и есть замыкание


        perl -MDDP -E 'my @arr = qw/1 2 3/; sub test { my $multiplier = shift;  map { $_ * $multiplier } @arr;} my @nums = test(5); p @nums'

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


      4. Неблокирующее/асинхронное программирование с помощью AnyEvent/EV, Mojo::IOLoop; использование, как вы описали функций sort/grep/map, включая разные модули, например, для работы со списками List::Util, List::MoreUtils. Mojo::DOM/Web::Query — например, в функцию each передается колбек, который выполняется для каждого найденного элемента. В Dancer2/Mojolicious используются колбеки для роутинга, например, пример для Dancer2:


        use Dancer2;
        get '/' => sub { "Hello World" };
        dance;


      «Higher-Order Perl» by Mark Jason Dominus. действительно очень неплохая, и рассказывает о фукнциональном программировании в Perl


      1. Zanak
        10.04.2017 11:11

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

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


        1. TheAthlete
          10.04.2017 11:58
          +1

          perl не является языком ООП. Разные надстройки позволяют достаточно полно эмулировать ООП, но это только эмуляция, и поэтому я сказал о не совсем корректном сравнении.

          Во-первых в данном случае было сравнение функционального программирования в Perl и ООП в C++. Во-вторых, ООП это парадигма, которая не зависит от языка, и может быть реализована в языках, которые нативно эту парадигму не искользуют (взять тот же GTK, который написан на процедурном C, но при этом является объектно-ориентированным)


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

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


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

          на самом деле FP — это не только чистые функции, но и вообще оперирование функциями как значениями (передача функций как параметр, возврат функций)


  1. akzhan
    10.04.2017 13:15

    На самом деле в C++ давно можно писать проще (замыкания, лямбды):

    auto curry = [](auto binary, auto x) {
      return [=](auto y) {
        return binary(x, y);
      };
    };
    curry(mul, 3)(4); // 12
    
    


    Взято из Новые возможности лямбд в C++14


    1. TheAthlete
      10.04.2017 13:41

      Спасибо за хороший пример.
      Действительно, на C++ можно проще, но рамках данной статьи хотелось рассказать о замыканиях и лямбдах в более привычных для знающих ООП терминах. В данном случае Perl можно заменить даже на тот же C++, JavaScript, но суть останется той же.