perlでのコレクション操作 (前編)

Perl Advent Calendar 2019 の3日目ですよ。
昨日は papix さんの
「[Syntax::Keyword::TryとPerlのキーワードプラグイン (その1)](https://papix.hatenablog.com/entry/2019/12/02/170611)」でした。

---

皆さんはPerlでコレクション操作をどうしてるでしょうか？
※ここでは、コレクションを「配列(リスト)っぽいデータ構造」全般を指す言葉として使っています。

## コレクション操作の2つの方法

### perl標準のリスト

何らかの元データを抽出や変換等の加工を施して処理結果を得たい場合、伝統的なperl標準の方法としては以下のようにリストに対してmap, grep等を適用させます。
※結合順序のために、連結した処理の順序が、記述の **逆順** になることに注意です。

```perl:perlの通常のリスト処理例
my @result =
    map  { ... }  # 変換処理3
    map  { ... }  # 変換処理2
    grep { ... }  # 抽出処理1
    @src;         # 元リスト

foreach (@result) {
    ...  # 逐次処理
}
```

### コレクション モジュールを利用

一方、OOスタイルでやりたい場合は (何らかのライブラリを利用する必要がありますが）
以下のように処理の順に記述することができますね。

```perl:OOスタイルでのコレクション操作例
my $result = $src  # 元コレクション
    ->grep(...)    # 抽出処理1
    ->map(...)     # 変換処理2
    ->map(...);    # 変換処理3

$result->each(...);  # 逐次処理
```


何らかのフレームワークやライブラリを利用している場合など、OOスタイルで記述する場面が多いと思いますので、コレクション操作もOOでかけると処理順のままメソッドチェインで記述できて嬉しい。

```perl:例
    # 商品レポジトリを
    return $app->repos->items

        # 指定タグで検索
        ->find({tag => $req->{tag} })

        # ロジが障害情報に該当していたら除外
        ->grep(sub { !$trouble_info->{$_->deliverer_id} })

        # API用の加工を施す
        ->map(sub {
            +{
                id     => $_->id,
                name   => $_->name,
                url    => $_->url // sprintf('/items/%s', $_->id),
                enable => $_->stock && !$_->suspended,
            }
        })

        # jsonに変換
        ->to_json;
```

## おすすめコレクション モジュール

コレクションのためのモジュールは沢山ありますが、カジュアルに使いやすくて候補になりそうなモジュールは ↓このあたりでしょうか。

* [Data::Perl::Collection::Array](https://metacpan.org/pod/Data::Perl::Collection::Array) 
* [Mojo::Collection](https://metacpan.org/pod/Mojo::Collection)

OOスタイルを前提としたコレクションの実装方式に関して、オブジェクトがHASHベースかARRAYベースのどちらか？という大別ができますが、上記の2つはどちらもARRAYベースのオブジェクトとなっています。
デリファレンスしてそのまま配列として利用できるように実装されていますので、OOスタイルと伝統的スタイルを混在させる場合でも使いやすいと言えるかも。

### Data::Perl::Collection::Array

* ドキュメント: [Data::Perl::Collection::Array](https://metacpan.org/pod/Data::Perl::Collection::Array)
    * [Data::Perl](https://metacpan.org/pod/Data::Perl) に同梱
    * 拡張しやすい

push()やset()など、**破壊的変更を施すメソッド**も一通り一式用意されており、通常の配列でできることはほぼ同名のメソッドで代替操作が可能です。
Data::Perl はPerlの基本的データ型のためのベースクラスモジュールであり、これをベースにカスタマイズして利用する用途を想定しているようです。配列以外にもハッシュやその他の型用のクラスもあります。

```perl:Data&#58&#58Perlの例
use Data::Perl qw(array);

# array()はコレクション作成のショートハンド
my $c = array(1,2,3);

# 値を10倍して、１０より大きな値を絞り込み
my $c2 = $c->map(sub { $_ + 10 };
my $c3 = $c2->grep(sub { $_ > 10 });

# 破壊的操作
$c->push(100);

# 配列化
my @array = $c->all;

# デリファレンスして、通常のリストとして操作
say join ',', @$c;
```

### Mojo::Collection

* ドキュメント: [Mojo::Collection](https://metacpan.org/pod/Mojo::Collection)
    * Webフレームワーク [Mojolicious](https://mojolicious.org/) に同梱

Webフレームワーク Mojolicious で利用されている、必要最小限の機能をもったコレクションモジュール。
Web以外ではあまり利用しないメソッドも含まれていますが、Mojoliciousプロジェクトならそのまま使えるし、シンプルな構造なのでそのまま継承して拡張して使っても良いですね。
なお、破壊的メソッドを用意しておらず、基本的には**イミュータブルなコレクション**として利用することを想定しているようです。

```perl:Mojo&#58&#58Collectionの例
use Mojo::Collection qw(c);

# c()はコレクション作成のショートハンド
my $c = c(1,2,3);

# 値を10倍して、１０より大きな値を絞り込み
my $c2 = $c->map(sub { $_ + 10 };
my $c3 = $c2->grep(sub { $_ > 10 });  

# 配列リファレンス化
my $array = $c->to_array;

# デリファレンスして、通常のリストとして操作
say join ',', @$c;
```


### API比較

なお、どちらのモジュールも、その内部でList::MoreUtils, List::Utilなどのポピューラーなリスト操作用ライブラリを採用しているためか、同名のコレクションメソッドが多く見られます。
参考のため、イミュータブル系な操作に対する各コレクションのメソッドの対比を記述します。
特に太字で記述した箇所は注意が必要です。

| (操作) | (戻り値) | perl標準 | Data::Perl::Collection::Array | Mojo::Collection |
|----------------------------|------------------|---------------|-------------------------------|---------------------------|
| 生成 | コレクション | (1, 2, 3) | **array(1, 2, 3)** | **c(1, 2, 3)** |
| 写像 | コレクション | map | map | map |
| フィルタ | コレクション | grep | grep | grep |
| 集約 | (任意) | - | reduce | reduce |
| ソート | コレクション | sort | sort | sort |
| each | (なし) | for / foreach | - | each ※戻り値=コレクション |
| 最初にマッチ | 要素 | - | first | first |
| 最初にマッチしたインデックス | インデックス | - | first_index | - |
| 逆順 | コレクション | reverse | reverse | reverse |
| シャッフル | コレクション | - | shuffle | shuffle |
| **平坦化** | コレクション | - | **flatten** | - |
| **平坦化(再帰)** | コレクション | - | **flatten_deep** | **flatten** |
| 先頭から取得 | コレクション | - | - | head |
| 末尾から取得 | コレクション | - | - | tail |
| 最先頭要素 | 要素 | - | - | first **※引数なし** |
| 最末尾要素 | 要素 | - | - | last |
| 長さ | 要素数 | scalar | count | size |
| 連結 | 文字列 | join | join | join **※戻り値=Mojo::ByteStream** |
| 空判定 | bool | - | is_empty | - |
| 空要素削除 | コレクション | - | - | compact |
| 重複削除 | コレクション | - | uniq | uniq |
| 複製(shallow) | コレクション | my @b = @a | shallow_clone | c(@$c) |
| **配列化** | 配列 | (N/A) | **elements** / **all** | @$c |
| **配列化(リファレンス)** | 配列リファレンス | (N/A) | - | **to_array** |

---

公開に時間がかかってしまってすみません。
もう少しお話したかったのですが、一旦公開します。
次はハッシュ型のコレクションや、拡張についてお話したいです。

明日は hkoba さんの
「[-nle と .pm から始める Perl 入門とかどうかしら？（前編）](https://hkoba.hatenablog.com/entry/perl-adventcal2019-1204)」です！