Hatena::ブログ(Diary)

naoyaのはてなダイアリー

April 19, 2008

Perl のリスト操作を Ruby 風に

Perl の言語組み込みのリスト操作は関数形式で、push(@array, 1, 2) のような記述になります。一つのリストに対して複数の操作をしたい場合などは、関数呼び出しを複数行にわたって書いていくことになり、少々面倒です。しかし Perl は、Perl のリスト実装である配列のリファレンスに bless してメソッドを定義したクラスを作ることができます。この独自に定義したクラスにプリミティブな操作を加えていって、Ruby のように連続したメソッドの呼び出しによるリスト操作を実現することが可能です。

ここでは List::RubyLike という配列クラスを作成します。まずは手始めに配列に bless して、size() メソッドが呼び出せるようにします。以下のようになります。

package List::RubyLike;
use strict;
use warnings;

sub new {
    my ($self, @list) = @_;
    my $class = ref $self || $self;
    bless \@list, $class;
}

sub size {
    my $self = shift;
    scalar @$self;
}

1;

このクラスで以下のテストが通ります。

my $list = List::RubyLike->new;

isa_ok $list, 'List::RubyLike';
is $list->size, 0;

このクラスに付け加える形で、リストに対する基本操作である push() と join() を実装します。

package List::RubyLike;
use strict;
use warnings;
use Params::Validate qw/:all/;

sub new {
    my ($self, @list) = @_;
    my $class = ref $self || $self;
    bless \@list, $class;
}

sub size {
    my $self = shift;
    scalar @$self;
}

sub push {
    my ($self, @list) = @_;
    CORE::push @$self, @list;
    $self;
}

sub join {
    my ($self, $delimiter) = validate_pos(@_, 1, { type => SCALAR });
    join $delimiter, @$self;
}

1;

push が $self を返却しているので、push の返り値には続けて List::RubyLike のメソッドを呼び出すことができます。

これで以下のテストが通ります。push して join と呼び出しが繋がっているところに注目してください。

$list->push(1, 2);
is $list->size, 2;
is $list->push(3)->join(','), '1,2,3';

同様にメソッドを追加していくことで様々なリスト操作が可能になるでしょう。例えば

is $list->push(3)->collect(sub { $_ * $_ })->reduce(sub { $_[0] + $_[1] }), 14;

というように、リストの各要素の自乗を collect (map) で集めて、recduce() でその合計とする、というような式もワンライナーで記述できるようにしたいところです。必要なメソッドを追加していきます。さらに

my $list = list(1, 2)

と、リストの作成を List::RubyLike->new ではなく list() と手軽に行えるようにします。実装は以下のようになります。

package List::RubyLike;
use strict;
use warnings;

use Params::Validate qw/:all/;
use Exporter::Lite;
use List::Util ();

our @EXPORT = qw/list/;

sub list (@) {
    __PACKAGE__->new(@_);
}

sub new {
    my ($self, @list) = @_;
    my $class = ref $self || $self;
    bless \@list, $class;
}

sub size {
    my $self = shift;
    scalar @$self;
}

sub push {
    my ($self, @list) = @_;
    CORE::push @$self, @list;
    $self;
}

sub to_a {
    my $self = shift;
    [ $self->unblessed ];
}

sub join {
    my ($self, $delimiter) = validate_pos(@_, 1, { type => SCALAR });
    join $delimiter, @$self;
}

sub unblessed {
    my $self = shift;
    return @$self;
}

sub dup {
    my $self = shift;
    $self->new( $self->unblessed );
}

sub collect {
    my ($self, $code) = validate_pos(@_, 1, { type => CODEREF });
    my @collected = CORE::map &$code, @{$self->dup};
    wantarray ? @collected : $self->new(@collected);
}

*map = \&collect;

sub reduce {
    my ($self, $code) = validate_pos(@_, 1, { type => CODEREF });
    List::Util::reduce sub { $code->($a, $b) }, @{$self->dup};
}

1;

だいぶ増えてきました。この調子でリスト操作に必要なメソッドをどんどん加えていけばいずれ Ruby のリスト操作と同等の操作すべてを網羅するのも難しくはないでしょう。

演算子 overload を使えば

$list <<= 4; # push(@$list, 4)

といったインタフェースを追加することも可能です。

CPAN には List::ObjectList::oo など、リストをオブジェクト指向的に扱えるようにするモジュールがいくつかあるようです。ただし、いずれも Ruby ほど豊富な操作を提供するわけではなさそうです。autobox を利用して似たようなことをすることも可能です。

DBIx::MoCo のリスト操作

この類のリストクラスがあると何が良いか、ですが、どんなプログラムであれその大部分の処理はリスト操作ですから、利点は自明です。

例えば O/R マッパの DBIx::MoCo は、DB に問い合わせて返却されたレコードオブジェクトを、上記のようなリストクラスとして返却します。従って

my $url = 'http://d.hatena.ne.jp/naoya/';
moco('Entry')->search(where => ["url like ? and count >= ?", "$url%", 5])
    ->collect(sub { $_->url })->join("\n");

というように search で SQL を発行した結果に対するリスト操作を連続して呼び出しながらデータ構造を操作することができます。

現在開発中の新しいはてなブックマークは、データソース層のライブラリに DBIx::MoCo を使っています。はてなブックマークのデータベースを操作し、ある特定の URL に付いたブックマーク一覧から、ユーザーのアイコンリストを取得して出力してみます。以下のようにワンライナーで書けます。

use Hatena::Bookmark::MoCo qw/moco/;

my $url = shift;
print moco('Entry')->retrieve_by_url($url)
    ->bookmarks
    ->grep(sub { $_->user->is_public })
    ->collect(sub { $_->user->profile->image->tag(32) })
    ->join("\n");

このコードを http://d.hatena.ne.jp/naoya/20080415/1208225082 に対して実行すると、結果は以下のようになります。

f:id:naoya:20080419132329p:image

DBIx::MoCo には Ruby ライクなリスト操作機能以外に、memcached を利用した透過的なキャッシュの機能や、Rails の ActiveRecord のような柔軟な SQL の組み立てインタフェース、テストを容易にするための fixture 機能なども搭載されています。YAPC::Asia 2008 でもう少し詳しく紹介したいと思っています。