
Perl でクロージャ。
ある正規表現にマッチする部分を削除する機能を実装したい。しかし正規表現の部分は場面場面で異なる。
このような単機能の(結果として1つのサブルーチンが得られれば良い)場合にはクロージャがぴったりだ。
#! /usr/bin/perl -w
use strict;
# クロージャを生成する関数
sub genSqueezer {
my $regex = shift; # genSqueezer は正規表現を引数に呼ばれ、
# 文字列を引数に取る無名関数の中に $regex の中身をセットする
return sub {
my $str = shift;
return $str =~ s/$regex//gr;
}
}
my $underscore_squeezer = genSqueezer( qr/_+/ );
# これは genSqueezer が返してきた無名関数へのリファレンスである。
# そして、内部で使う正規表現が qr/_+/ だということを覚えている。
my $str = "__foo__bar__baz__";
my $squeezed_str = $underscore_squeezer->($str);
print "$squeezed_str\n";
# -> foobarbaz
genSqueezer をひとつ作っておけば、関数全体を繰り返し書かなくても、クロージャ生成時の正規表現を変えるだけでいろいろなバリエーションの squeezer を作ることができる。
my $space_squeezer = genSqueezer( qr/\s+/ );
つまり使いドコロとしては、
- スカラー変数に関数リファレンスをバインドする毎にパラメータを設定したい(雛形を元にしたいろんなバリエーションがほしい、など)
- 同じことを普通の関数定義やクラス定義で実装するよりもコードが改善する(リファクタリングの価値がある)
リファクタリングの価値があるかどうかは状況次第なのでなんとも言えないけど、例えば(上の例を引き継ぐと)スクイーズしたい文字がアンスコに限られてるなら、はじめから素直に、
sub underscore_squeeze {
my $str = shift;
return $str =~ s/_+//gr;
}
という関数をひとつ定義してしまえばいいわけだし…。
逆にスクイーズするための正規表現がめまぐるしく変わるなら、
sub squeeze {
my ($regex, $str) = @_;
return $str =~ s/$regex//gr;
}
と、常に正規表現と対象文字列の2つを引数で受け取るようにしたほうがいいと思う。
個人的な仕事の話を持ち出すと、あるフォーマットAから別の複数のフォーマット*へ文字列を変換する場合、
- フォーマットAの文字列から「不要な部分を削除する」という大枠は変わらないが
- 変換先のフォーマットによって、HTML タグを削除したり、空白文字を削除したりなど、「何を削除するか」はビジネスロジックによって異なる
ような状況なら、クロージャでの対応は妥当かと思う(例で言えば genSqueezer 関数を共有モジュールに定義しておいて、各フォーマット毎に use してインスタンスを作成する)。
最後に、「Effective Perl」の中で紹介されていたお気に入りのクロージャの例を。
これ仕事でかなり使い回してる。
package Compose;
use strict;
use warnings;
use Exporter;
our @ISA = qw/ Exporter /;
our @EXPORT = qw/
compose
/;
# compose(\&sub1, \&sub2) とした場合、sub1 -> sub2 の順番で実行される
sub compose {
my (@codeRefs) = @_;
sub {
my $str = shift;
for my $codeRef (@codeRefs) {
$str = $codeRef->($str);
}
return $str;
};
}
1;
言語は違うけど「On Lisp」にも同様の compose 関数が紹介されてた(Lisp の compose のほうが言語内での汎用性はずっと高いけど)。
使い方は、ある文字列に対して順番に当てなきゃいけない関数処理を定義しておいて、それをまとめてひとつの関数であるかのように動作させる。
#! /usr/bin/perl -w
use strict;
use Compose qw/ compose /;
# 処理の対象になる文字列
# 空白はタブ文字
my $str = <<'EOT';
1 aaa
2 bbb
3 ccc
EOT
# 処理1
# 各データをダブルクォーテーションで囲む
my $quote = sub {
my $str = shift;
return $str =~ s/(\S+)/"$1"/gr;
};
# 処理2
# セパレータをカンマに置き換える
my $tab_to_comma = sub {
my $str = shift;
return $str =~ s/\t/,/gr;
};
# 処理3
# ヘッダーを付ける
my $add_header = sub {
my $str = shift;
return "id,name\n" . $str;
};
my $editor = compose($quote, $tab_to_comma, $add_header);
my $edited_str = $editor->($str);
print $edited_str;
# id,name
# "1","aaa"
# "2","bbb"
# "3","ccc"
実際は、(標準|CPAN|自作)モジュール群の中に単機能の関数が散らばっていて、それをその時に必要な分だけ必要な順番に(まさしく)compose して使う。
関数を実行する順番が関係あるところも味噌で、例えば上の例なら、処理1と処理2は逆転しても問題ないが、処理3は最後に実行する必要がある(仮に処理3を最初に実行したら、処理1でヘッダーもダブルクォートで囲まれてしまう)。
概念的なところ、文法的なところはすっ飛ばしてますが、何かしら参考になれば幸いです。
