北海道苫小牧市出身の初老PGが書くブログ

永遠のプログラマを夢見る、苫小牧市出身のおじさんのちらしの裏

高階関数とlocalは相性がいい

太古の昔、localよりmyを使え! とみんなが口を酸っぱくして言っていたものだから忘れがちですが、正しい使い方をすればlocalはすごい便利です。

まとめ

最初にまとめを書いておきます。(なので、最後にまとめはありません!)

  • localは便利
  • メソッド間に渡ってlocalの効力を持たせたければ、高階関数的なAPIにする
  • Schemeのcall-withなAPIがいいお手本になる

localの使い道

localは、共有している変数(特にグローバル変数)を「一時的」に変更したい時に使います。この、「一時的」ってのがミソです。他の操作と共有する変数の変更は、実行中のスクリプト全体に大きな影響を与えるため、まっとうな開発者であればなるべくやりたくないことです。しかし、localをつけて変数にアクセスすると、ブロックを出た時にもとの値に戻ることが保証されます。

以下は、Devel::PerlySense::Utilのソースからの引用ですが、典型的なlocalの使い方です。ファイル読み込みのレコード区切り文字を表す特殊変数$/*1を一時的にundefにすることで、ファイル全体を一度で読み込んでいます。*2

sub slurp {
	 my ($file) = @_;
    open(my $fh, "<", $file) or return undef;
    local $/;
    return <$fh>;
}   

localの欠点

localが使いにくい局面があります。それは、共有する変数を使う操作が一つの関数で完結しない場合です。

例えば、トランザクションを提供するメソッドは、begin() と commit() としたいでしょう。ただ、PerlDBIではデフォルトで AutoCommit が有効になっていますので、これではトランザクションを扱えません。そこで、begin()の中で、

local $self->dbh->{ AutoCommit };

なんてやって、トランザクションの間はAutoCommit を切りたいと思うかもしれません。ところが、begin() から戻ってしまうと、localの魔法は切れてしまうので AutoCommit の値は元に戻ってしまいます。よって、この実装はうまく行きません。

localと高階関数

メソッドから出ずに begin() から commit() の間に書かれた処理を終わらせることができれば、先ほどのトランザクションを実現する機能を実現出来そうです。しかし、果たしてそんな夢みたいなことができるのでしょうか?

そこで登場するのが、高階関数です。関数を渡し、ブロック内で処理を終わらせればいいのです。具体的には、

begin();
exec_in_transaction1();
exec_in_transaction2();
exec_in_transaction3();
commit();

こんな風に書きたかった処理*3を、代わりに、

do_transaction( sub{
	exec_in_transaction1();
	exec_in_transaction2();
	exec_in_transaction3();
});

こう書くようなインタフェースに変えます。実際にこの考えで実装された関数が、Class::DBIのpodに出てきます。

  sub do_transaction {
    my $class = shift;
    my ( $code ) = @_;
    # Turn off AutoCommit for this scope.
    # A commit will occur at the exit of this block automatically,
    # when the local AutoCommit goes out of scope.
    local $class->db_Main->{ AutoCommit };

    # Execute the required code inside the transaction.
    eval { $code->() };
    if ( $@ ) {
      my $commit_error = $@;
      eval { $class->dbi_rollback }; # might also die!
      die $commit_error;
    }
  }

Schemeのcall-with-XXXXXXXX

Scheme(Gauche)には、 call-with-input-file や call-with-current-continuation(call/cc) のように、 call-with と名前がつく関数がたくさんあります。例えば、 call-with-input-file は入力ポートを引数にとる手続きを受け取り、ファイルを開いた状態でその手続きに渡して実行してくれます。使用例は以下です。

(call-with-input-file "/tmp/test.txt" 
   ( lambda (p) (print (read-char p)) )
)

このように、call-withシリーズの手続きは、まず最初に渡された手続きが呼べる状態に色々お膳立てをしておいて、それから渡された手続きを実行する、と言う動きをします。先ほどの高階関数を使った例はこの考え方と似ています。

もう一つPerlでの例を見てみましょう。LWPでSquidのプロクシ経由でhttpsアクセスをする場合、こちらで説明したように $ENV{HTTPS_PROXY} を指定する必要があります。この環境変数を local で変更したいのですが、get_user_agent() 等と言う関数でやろうとすると先ほどのトランザクションの例と同じようにうまく行きません。

そこで、call-with の考え方を元に以下の関数を用意します。call_with_ua は、 LWP::UserAgentを引数にとる1引数の関数を受け取ります。

sub call_with_ua{
	my ($func) = @_;
	my $ua = new LWP::UserAgent;
	local $ENV{HTTPS_PROXY} = 'http://url.to.squid:80';
	$ua->proxy('http', 'http://url.to.squid:80');
	return $func->($ua);
}

この関数の利用は、以下のように行います。LWP::UserAgentを引数にとる関数を渡すと、プロクシが利用出来るようにお膳立てされたオブジェクトが渡って来て、それを利用すればプロクシ経由のアクセスができると言うスンポーです。

my $url = 'https://www.century21.co.jp/app/login/login.pl';
call_with_ua(sub{
	my ($ua) = @_;
	my $res = $ua->get($url);
	print $res->content;
});

応用: さらに高階関数を被せる

WWW::MobileCarrierJP*4 は、残念ながら外からプロクシ指定済みのエージェントを指定することができません。 が、グローバル変数をうまく使うとプロクシ経由でアクセスさせられます。


このモジュールはWeb::Scraper を使っていますが、Web::Scraperにはデフォルトで利用するユーザエージェントオブジェクトを指定するための$Web::Scraper::UserAgent と言うグローバル変数のフックがあります*5。これを使います。

以下のように、Web::Scraperをプロクシ経由でアクセスするようにした状態で処理をする関数、を作ります。もちろん、ここでも $Web::Scraper::UserAgent を local 指定して変更し、局所的にしか影響を与えないようにします。

sub call_with_proxied_scraper{
	my ($func) = @_;
	call_with_ua(sub{
		my ($ua) = @_;
		local $Web::Scraper::UserAgent = $ua;
		return $func->();
	});
}

この関数からWWW::MobileCarrierJPを利用する処理*6をcallしてもらえば、プロクシ経由でWWW::MobileCarrierJPが使えます。

call_with_proxied_scraper(sub {
	print map {"$_->{ip}\n"} 
	                        @{ WWW::MobileCarrierJP::DoCoMo::CIDR->scrape };
});

注意事項

この手法は便利ですが、やり過ぎるとコードを難読化してしまいます。また、localによる、共有する変数の局所化がどこまで必要かも検討事項です。例えば、バッチ処理で WWW::MobileCarrierJP だけを利用してすぐ終了するようなスクリプトであれば、 無理に local 使わずに $Web::Scraper::UserAgent を直接変えても害はないでしょう。

参考書

Perlでの高階関数の利用については、多分、↓に色々出てるんですが、オレは積ん読状態です(コラ。後で読んでみます。

Higher-order Perl: A Guide To Program Transformation
Mark Jason Dominus
1558607013

*1:デフォルト値は当然\n

*2:余談ですが、closeがないのが$fhがundefとなって参照が消えるとファイルが閉じられるからです。このような動きに関する記述はIO::Handleのソース内に見られます。

*3:例外処理を考えるとeval噛ませた方が無難です

*4:THIS SOFTWARE IS STILL UNDER ALPHA STATUS.DON'T USE ME :) とのこと

*5:ただし、 $scraper->user_agent( $ua ) と言う指定ができるので、普段はこっちを使いましょう

*6:関数名から考えた場合、正確には Web::Scraper を利用する処理