HaskellのlensをPerlに移植したData::Focus 0.01をリリース

HaskellのlensライブラリのAPIと実装をてきとうにパクってきて、Data::FocusというPerlモジュールを作った。

現状、Haskellのコンパクトなlens実装であるlens-family-coreパッケージに近い作りになっている。lensパッケージは複雑すぎてまだ全容を捉えきれていない。

lensについて一旦忘れると、Data::Focusは複雑に入れ子になったデータ構造へアクセスするためのモジュールである。Data::Diverの親戚と言える。

use feature qw(say);
use Data::Focus qw(focus);

my $target = {
    foo => [0, 1, { bar => "buzz" }]
};

say focus($target)->get("foo", 2, "bar"); ## => buzz

この程度ならData::Diverと変わらない。実際、Data::Diverではこう書ける。

use Data::Diver qw(Dive);
say Dive($target, "foo", 2, "bar");

Data::Diverでは"foo"や2といったキー部分にundefやarray-refを与えることでもう少しトリッキーなデータアクセスも可能としている。

一方、Data::Focusはこの考え方をさらに発展させ、Data::Focus::Lensを継承するクラスのオブジェクトをキーとして与えると、そのLensの実装に応じてデータアクセスの方法を様々に変化させることができる。これにより、ハッシュや配列のtraverseを行ったり、ハッシュや配列以外のオブジェクトへアクセスしたりといったことが可能となる。


以下の例では、Data::Focus::Lens::HashArray::Allレンズを使うことで配列の全要素にアクセスしている。

use Data::Dumper;
$Data::Dumper::Indent = 1;
$Data::Dumper::Quotekeys = 0;
$Data::Dumper::Sortkeys = 1;

use Data::Focus qw(focus);
use Data::Focus::Lens::HashArray::All;

my $all_lens = Data::Focus::Lens::HashArray::All->new;

my $target = [
    {name => "john", age => 15},
    {name => "mary", age => 17},
    {name => "lily", age => 21},
];

my @names = focus($target)->list($all_lens, "name");
print Dumper \@names;
## => $VAR1 = [
## =>   'john',
## =>   'mary',
## =>   'lily'
## => ];

set()メソッドによって値をセットすることができる。

focus($target)->set($all_lens, "father", "robert");
print Dumper $target;
## => $VAR1 = [
## =>   {
## =>     age => 15,
## =>     father => 'robert',
## =>     name => 'john'
## =>   },
## =>   {
## =>     age => 17,
## =>     father => 'robert',
## =>     name => 'mary'
## =>   },
## =>   {
## =>     age => 21,
## =>     father => 'robert',
## =>     name => 'lily'
## =>   }
## => ];

over()メソッドはデータの現在値を参照し、書き換える。

focus($target)->over($all_lens, "name", sub { uc($_[0]) });
print Dumper $target;
## => $VAR1 = [
## =>   {
## =>     age => 15,
## =>     father => 'robert',
## =>     name => 'JOHN'
## =>   },
## =>   {
## =>     age => 17,
## =>     father => 'robert',
## =>     name => 'MARY'
## =>   },
## =>   {
## =>     age => 21,
## =>     father => 'robert',
## =>     name => 'LILY'
## =>   }
## => ];

なお、Lensオブジェクト以外のもの(素の文字列など)を与えると、現状ではData::Focus::Lens::HashArray::Indexオブジェクトに変換される。これは単にハッシュや配列にインデックスでアクセスするレンズである。

今のところハッシュと配列を対象とするレンズしか作っていないが、例えば文字列をtargetとして部分文字列に注目するレンズとか、データベースにアクセスしにいくレンズなども作れるだろう(後者は実用上あまり価値はないと思うが)。

Data::Focusのもう一つの著しい特徴としては、immutableなオブジェクトを更新できるという点がある。

「immutableなオブジェクトを更新する」とは矛盾しているようにも思えるが、更新した結果であるオブジェクトを新たに作る、ということである。

ハッシュと配列を対象にするレンズでは、immutableオプションを有効にすることで対象をimmutableとみなして各種操作を行うことができる。

use Data::Focus qw(focus);
use Data::Focus::Lens::HashArray::Index;

sub lens {
    return Data::Focus::Lens::HashArray::Index->new(
        index => $_[0], immutable => 1
    );
}

my $target = [
    {
        items => ["apple", "orange", "melon"],
    },
    {
        items => ["strawberry"],
    },
];

my $result = focus($target)->set(lens(1), lens("items"), lens(1), "grape");

print Dumper $target, $result;
## => $VAR1 = [
## =>   {
## =>     items => [
## =>       'apple',
## =>       'orange',
## =>       'melon'
## =>     ]
## =>   },
## =>   {
## =>     items => [
## =>       'strawberry'
## =>     ]
## =>   }
## => ];
## => $VAR2 = [
## =>   $VAR1->[0],
## =>   {
## =>     items => [
## =>       'strawberry',
## =>       'grape'
## =>     ]
## =>   }
## => ];

このように、set()メソッドは$targetには影響を与えず、更新の結果を$resultとして返している。

上の例のset()メソッドは、$target->[0]以下の要素にはアクセスしていない。Data::Focus::Lens::HashArray系のレンズでは、たとえimmutableオプションを有効にしていても、アクセスしていない部分に関してはクローンを作らずにオブジェクトを共有する。

print "target[0]: $target->[0]\n";
print "result[0]: $result->[0]\n";
## => target[0]: HASH(0x1c02b28)
## => result[0]: HASH(0x1c02b28)

このように、immutableオブジェクトが多重に入れ子になったデータ構造をメモリ効率よく書き換えたい場合、Data::Focusは特に有効である。