Sub::Pipe で UNIX pipe みたいな関数適用をするB!

こんにちは、s?fujiwara です。

本日は UNIX pipe みたいな関数適用ができる Sub::Pipe をご紹介します。

おことわり

現在 CPAN に上がっている Sub::Pipe のコードは dankogai さんから寄贈していただいたものですが、元になった実装は 自分の blog で書いたもの なので、ここでは自分のモジュールとして紹介させていただきます。

これはなに?

Sub::Pipe は非常に小粒なモジュールです。どれぐらい小粒かというと、本質部分は

package Sub::Pipe;
use overload '|' => sub { $_[0]->( $_[1] ) };
sub joint(&) { bless $_[0], __PACKAGE__ };

この 3行のみ、というぐらい小粒なわけですが、なかなかおもしろいモジュールじゃないかと思っています。

Sub::Pipe を使うと、UNIX pipe のような記法で適用する関数を作成できます。

use Sub::Pipe;
sub trim {
    joint {
        my $str = shift;
        $str =~ s/^\s+|\s+$//g;
        return $str;
    }
}
$foo = " foo " | trim;   # is $foo, "foo"

これは文字列の前後の空白を除去する trim という関数を用意した例です。

joint は | で適用できるオブジェクトを返すので、変数に代入して使ったり

$uc = joint { uc $_[0] };
"foo" | $uc;   # FOO

直接 joint { CODE } を適用することも可能です。

"foo" | joint { uc $_[0] }; # FOO

引数を取りたい場合はこう書きます。

sub replace {
    my ($regex, $to) = @_;
    joint {
        my $str = shift;
        $str =~ s/$regex/$to/g;
        return $str;
    };
}

"abcdefg" | replace("abc", "ABC");  # ABCdefg

なにがうれしいの?

さて、これでは単に変態的な記法が追加されただけではないか、という疑問はごもっともです。

これはもともと Text::MicroTemplate (T::MT)というテンプレートエンジン内で、Template-Toolkit (TT) のようなフィルタ記法が使いたくて考案したものでした。

TT では

[% value | repalce("foo", "bar") | uri %]

のように、値の後ろに | でフィルタを並べて適用、という記法が使えます。

この記法は表示される対象である値が先頭に書かれているのが読みやすい。

T::MT は Perl がそのまま書けるのでとても柔軟で高速なのですが、以下のように書かざるを得ないため、対象の値が何なのかが一見して分かりにくいなあと思ったのでした。

<?= uri( replace( $value, "foo", "bar" ) ) ?>

ここで Sub::Pipe を使えば

<?= $value | replace("foo", "bar") | uri ?>

のようにできますよね。

仕組み

さて、Sub::Pipe はどのような仕組みでこの (変な) 記法を実装しているのかといいますと、『演算子オーバーロード』を使っています。詳しくは perldoc overload をご参照ください。(日本語訳は 5.6.1 のものですが こちらにあります)

ソースコードをみていきましょう。

sub joint(&) { bless $_[0], __PACKAGE__ };

まず joint はコードリファレンスを引数に取り、それを bless した値を返すことで、関数の挙動をオブジェクト化します。

オブジェクトを作る場合はハッシュリファレンスを bless することが多いと思いますが、リファレンスならなんでも bless 可能なんですね。

そして演算子オーバーロードを使うと、あるパッケージのオブジェクトに対して、ある演算子が適用された場合の挙動を変更することができます。

use overload '|' => sub { $_[0]->( $_[1] ) };

ここでは通常 bit 演算の OR に使われる二項演算子 | を overload しているので、Sub::Pipe のオブジェクトに対して | が実行されると、sub { $_[0]->( $_[1] ) } が呼び出されます。

そこで、第1引数 $_[0] (これは bless されたコードリファレンスであるオブジェクト自身) に対して、第2引数 $_[1] (これは | の演算が適用される値です) を引数に与えて実行、という処理をしています。ややこしいですね!

A | B という演算なのに $_[0] が B で $_[1] が A なのはどうしてかというと、オブジェクトメソッドの最初の引数は常にそのパッケージのオブジェクトである必要から、引数の順序が入れ替わっているためです。

入れ替わっているかどうかは、$_[2] の値で判断できます。真なら入れ替わっていて、偽なら入れ替わっていない、という意味になりますがここも詳しくは perldoc overload の "Calling Conventions for Binary Operations" の項をご覧ください。

# Sub::Pipe の場合はオブジェクト同士を | で演算することはありえないため、常に入れ替わっているものとみなして扱っています

他に overload を使っているモジュールは?

例えば DateTime がそうです。日付を < や > で比較したり、+ で DateTime::Duration を足したりできるのは overload のおかげです。

他にも、オブジェクトを足したり引いたり比較したりできるモジュールは多々ありますよね。

まとめ

以上 UNIX pipe のような記法で関数適用を可能にする Sub::Pipe の紹介でした。

正直、普通のスクリプト内で使うのは止めたほうがいいでしょうが、テンプレート内の DSL としてはなかなか使えるんじゃないかと思っています。

明日は myfinder さんです。お楽しみに!