Perl: grep演算子でコードの密度を高める

以前に、「XML::Atomで任意のはてなユーザの はてなブックマークをコンソールにリストアップするテスト」というエントリーを書きました。そこで紹介したコードの中に、前から気になっていた部分がありました。それは以下の「foreach my $link(@links) {[...]}」というブロックです。無駄にforeachでループさせている所がカッコ悪いというか、Perlらしくないです。

[...]
my $bm_count = 1;
for (;;) {
    &print_progress($feed_url);
    
    my $feed    = $client->getFeed($feed_url) or warn $client->errstr;
    my $entries = $feed->entries;
    my @links   = $feed->link;
    
    foreach my $entry ($feed->entries) {
        &print_entry($entry, $bm_count);
        exit if (++$bm_count > $limit);
    }
    
    $feed_url = '';
    # ここが冗長な表現. もっとすっきりしたい
    foreach my $link(@links) {
        $feed_url = $link->href if ($link->rel eq 'next');
    }
    last if ($feed_url eq '');
}
[...]

「続・初めてのPerl」を読んで、grep演算子なるものを知りました。grep演算子を使えばforeachを無くせることに気がつきました。そこで、以下のように改善しました。自分自身としては、foreach版よりもgrep版の方がすっきりしていて好きですね。

$feed_url = (grep {$_->rel eq 'next'} @links)[0]->href;

簡単に説明しておきます。links配列には、XML::Atom::Linkオブジェクトが格納されています。そのlinks配列の中からXML::Atom::Linkのインスタンス(メンバー)変数relが'next'であるものをgrepで選択しています。はてなブックマークAtomフィードでは、rel要素の属性が'next'であるものは1つしかありません。ですので、grepで選択されたリストの先頭を「[0]」で取得し、それのインスタンス(メンバー)変数hrefを取得しています。

1点だけ注意というか余談です。上の説明で「XML::Atom::Linkのインスタンス(メンバー)変数rel」と言いましたが、正確にはXML::Atom::Linkにはインスタンス変数は定義されていません。AUTOLOADというPerlの高度な機能を使って、あたかもXML::Atom::Linkのインスタンス変数にアクセスしているかのように見せかけています。AUTOLOADを活用することで、インスタンス変数が多いクラスにおけるgetter/setterの悪夢から開放されます。

$ perldoc -m XML::Atom::Link

で一度、XML::Atom::Linkがどのような実装になっているのかを見てみると面白いかと思います。PERLDOC_PAGER環境変数vimemacsを指定しておくとカラー表示されて見やすくなります。

続・初めてのPerl - Perlオブジェクト、リファレンス、モジュール

続・初めてのPerl - Perlオブジェクト、リファレンス、モジュール

追記 (11/19 22:55)

id:tokuhiromさんからトラックバックを頂きました。貴重なご意見ありがとうございました。

- $feed_url = (grep {$_->rel eq 'next'} @links)[0]->href;
+ use List::Utils;
+ $feed_url = (first {$_->rel eq 'next'} @links)->href;

俺はこっちの方が好き。どっちでもいいんだけどさ。「オレは最初の一個を取りたいんだぜ感」が強調されるので。

こんなモジュールがあったのですね。知りませんでした。
早速、List:Utilをインストールして、サンプルコードを書いてみました。以下のコードを実行すると「4」が表示されます。

なるほど。確かに読みやすいですね。

use strict;
use warnings;
use List::Util qw(first);

my @list = qw/1 2 3 4 5/;

print first { $_ > 3 } @list;