例の関数型Perlプログラミング

この記事では、 AnyEvent :: HTTPを使用して壊れたリンクを検索するスクリプトの例を使用して、関数型プログラミングを検討します 。 次のトピックについて説明します。









匿名ルーチン



無名サブルーチンは通常のサブルーチンと同じように宣言されますが、 sub



キーワードとプログラムブロックの開始ブラケットの間に名前はありません。 さらに、この形式の記述は式の一部とみなされるため、ほとんどの場合、匿名サブルーチンの宣言はセミコロンまたは他の式区切り文字で終了する必要があります。







 sub { ...   ... };
      
      





たとえば、渡される値を3倍にするサブルーチンを実装します。







 my $triple = sub { my $val = shift; return 3 * $val; }; say $triple->(2); # 6
      
      





無名ルーチンの主な利点は、「データとしてのコード」の使用です。 つまり、コードを変数に保存し(たとえば、コールバックの場合は関数に渡します)、さらに実行します。







また、コールバックとの組み合わせを含め、匿名ルーチンを使用して再帰を作成できます。 たとえば、Perlバージョン5.16.0



で登場し、現在のサブルーチンへのリンクを取得できる__SUB__



トークンを使用して、階乗計算を実装します。







 use 5.16.0; my $factorial = sub { my $x = shift; return 1 if $x == 1; return $x * __SUB__->($x - 1); }; say $factorial->(5); # 120
      
      





壊れたリンクを見つける問題を検討するとき、コールバックと組み合わせて再帰を使用する例を以下に示します。







閉鎖



ウィキペディアに記載されているとおり







クロージャーは最初のクラスの関数であり、その本体には、周囲のコードでこの関数の本体の外側で宣言され、そのパラメーターではない変数への参照があります。

実際、クロージャーはOOPのクラスの類似物です。機能とデータを接続し、一緒にパッケージ化します。 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でクロージャーを使用し、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



クラスを宣言します。









Perlの例では、配列への参照と匿名関数を受け入れるfor_each



関数を作成します。 次に、配列を調べて匿名関数(クロージャー)を実行し、配列の次の要素をパラメーターとして渡します。







for_each



関数を使用する場合、最初にゼロに初期化される接辞変数$sum



定義します。 次に、配列参照とクロージャ関数をfor_each



関数for_each



渡します。この関数では、配列の各要素を$sum



変数に$sum



ます。 for_each



関数をfor_each



後、 $sum



変数には配列の合計が含まれます。







C ++でのPerlの例のクロージャー関数の類似物は、コードに示されているように、ラムダの使用です。 Perlの例では、関数に渡されるクロージャー関数はコールバックまたはコールバック関数とも呼ばれます。







コールバック関数



for_each



例がfor_each



に、コールバック関数は、実行可能コードを他のコードのパラメーターの1つとして渡すことです。 多くの場合、渡された関数はクロージャーのように機能します。 字句変数にアクセスでき、プログラムコードの他のコンテキストで定義でき、親関数(クロージャー/コールバックが渡された関数)からの直接呼び出しにアクセスできません。







実際、コールバック関数は、関数の多態性に類似しています。つまり、構造は同じですが、実行可能なサブタスクによって特定の場所でのみ異なる一連の関数を作成する代わりに、より汎用的な関数を作成できます。 ファイルから読み取り、ファイルに書き込むタスクの例を考えてみましょう。 これを行うには、Perlを使用して、リーダーとライターの2つの関数を作成します(例は、 異種データを解析するための Mikhail Ozerov Lazyイテレーターによるプレゼンテーションから取得しました )。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
      
      





コードを分析しましょう:









次に、AnyEvent :: HTTPを使用して壊れたリンクを見つけるという複雑で実用的なタスクを検討します。これは、上記のトピック(匿名ルーチン、クロージャー、コールバック関数)を使用します。







壊れたリンクを見つけるタスク



壊れたリンク(応答コード4xxおよび5xxのリンク)を検索する問題を解決するには、サイトクロールを実装する方法を理解する必要があります。 実際、サイトはリンクグラフです。 URLは、外部ページと内部ページの両方にリンクできます。 サイトをクロールするには、次のアルゴリズムを使用します。







 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



(開始ページのURL)、 $cnt



(ダウンロードするURLの数)を初期化し、ハッシュ$to_be_scanned



とグラフ$g



作成します。







次に、 scan_website,



関数を作成します。 scan_website,



関数にscan_website,



ダウンロードおよびコールバック用のURLの最大数の制限を渡します。







 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



ハッシュからURLを追加します。







 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



hash %urls



へのリンクとコールバックを渡します。







 process_page(\%urls, sub { ... });
      
      





process_page



関数で、受信したハッシュとコールバックを保存します。







 sub process_page { my ($current_page_urls, $cb) = @_;
      
      





その後、URLハッシュをループしてペア(parent_url => current_urls)



を取得し、現在のURLのリスト(current_urls)を(parent_url => current_urls)



ます







 while (my ($parent_url, $current_urls) = each %$current_page_urls) { for my $current_url (@$current_urls) {
      
      





ページからのデータの受信を検討する前に、少し余談します。 ページを解析してURLを取得するための基本的なアルゴリズムは、このURLが内部か外部かに関係なく、1つのHTTP GETメソッドを想定しています。 この実装では、2つのHEADおよびGET呼び出しを使用して、サーバーの負荷を次のように削減しました。









そのため、まずAnyEvent :: HTTPモジュールのhttp_head



関数を実行し、現在のURL、要求パラメーター、コールバックを渡します。







 $cv->begin; http_head $current_url, %params, sub {
      
      





コールバックでは、ヘッダー(HTTPヘッダー)を取得します







 my $headers = $_[1];
      
      





ここから実際のURL(リダイレクト後のURL)を取得します







 my $real_current_url = $headers->{URL};
      
      





次に、ペア(current_url => real_current_url)



%urls_with_redirects



ハッシュに%urls_with_redirects



ます。







 $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; #      }
      
      





それ以外の場合、サイトが内部でWebページである場合、







  elsif ( #   ($start_page_url_root eq $url_normalization->root_domain($real_current_url)) #   - && ($headers->{'content-type'} =~ m{^text/html}) ) {
      
      





次に、 http_get



関数を実行します。 http_get



関数に、上記で受け取った実際の現在のURL、リクエストパラメータ、コールバックを転送します。







 $cv->begin; http_get $real_current_url, %params, sub {
      
      





http_get関数のコールバックで、ページのヘッダーと本文http_get



取得し、ページをデコードします。







 my ($content, $headers) = @_; $content = content_decode($content, $headers->{'content-type'});
      
      





Web :: Queryモジュールを使用して、ページ解析とURL取得を実行します。







 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
      
      





次に、リンクのルートドメインを取得します(または「失敗」に入れます)







 my $root_domain = $url_normalization->root_domain($href) || 'fails';
      
      





その後、 $new_urls



の構造を$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



をいくつかの部分に分割します。つまり、配列に保存する内部URLとドメインに分割し、配列に保存する外部URLに分割します。 この構造により、次のようにサイトの負荷を減らすことができます。 %urls



ハッシュを構築する際の上記の$do



クロージャーに示すように、内部URLの$max_connects ( )



各ドメインごとに1つの外部URLを選択します。 したがって、 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} //= []
      
      





そして、これらの構造の1つを$urls



変数に保存します。これを次に使用して、 $new_urls



構造に書き込みます。







 push @$urls, $href; # add it to to_be_scanned set
      
      





この場合、リンクを使用して複雑なデータ構造を作成および操作します。 変数$urls



$new_urls



の構造を参照するため、変数$urls



が変更されると、 $new_urls



構造が変更され$new_urls



。 Perlでのデータ構造とアルゴリズムの詳細については、「Jon Orwant-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->(); }
      
      





この呼び出しは、すべての外部サイトが現在のURL $current_urls



リストにある場合に必要ですが、 $to_be_scanned



まだ$to_be_scanned



$to_be_scanned



。 この呼び出しがなければ、 http_head



http_head



実行して$current_urls



のリストをhttp_head









process_page



関数のコールバックで、結果の構造$new_urls



を保存し$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;
      
      





次に、グラフ要素の数がURLの数の制限以上かどうかを確認し、匿名サブルーチンへのリンクを削除して$cv->send()



ます。







 if (scalar($g->vertices) >= $count_url_limit) { undef $do; $cb->(); $cv->send; }
      
      





それ以外の場合、チェックするURLがあれば、







  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:









おわりに



Perl, , — , . Perl C++, (callbacks) Perl - C++. AnyEvent::HTTP, .








All Articles